Format spec · v1

PCHR Format

A compact container for 2D skeletal animation: bones, skinned meshes, vertex weights, and sampled frames, bundled alongside the atlas image.

Overview

PCHR is a compact binary file format for 2D skeletal animation. A single .pchr file stores a skeleton, including bones, skinned meshes, vertex weights, and sampled animation frames, together with the atlas image the meshes are drawn from.

Status: This documents v1, the first public release. PCHR is a single self-contained binary file. It does not use JSON, and no JSON is embedded in the file.

The format is read sequentially from the start of the file. Every variable-length body is preceded by its length or element count, so a reader never needs to seek backward or parse a text payload.

Conventions

File structure

A .pchr file is a flat sequence of fields read from the start of the file. Composite fields reference a type defined under Types.

Field Type Description
magic uint8[4] Magic bytes: "PCHR" (0x50 0x43 0x48 0x52)
version uint32 Format version (currently 1)
fps uint16 Playback frame rate
flags uint16 Reserved for future use, 0 in v1
imageCount uint16 Number of images
images Image[imageCount] Packed images, see Image
boneCount uint16 Number of bones
bones Bone[boneCount] Packed bones, see Bone
meshCount uint16 Number of meshes
meshes Mesh[meshCount] Packed meshes, see Mesh
animationCount uint16 Number of animations
animations Animation[animationCount] Packed animations, see Animation

Types

Image

An atlas image. Meshes reference an image by its index in the images array.

Field Type Description
name string Image name
width uint16 Image width in pixels
height uint16 Image height in pixels
format uint8 Image data format, see Image format
dataLength uint32 Length of data in bytes
data uint8[dataLength] Image data, encoded per format

The dataLength field gives the exact byte length of the data block for every format, so a reader consumes the image without decoding it. The format field selects how data is interpreted, and width and height give the decoded image dimensions.

Image format

Value Name Description
0 png An encoded PNG file. data is the PNG byte stream.
1 rgba8888 Raw pixels, 4 bytes per pixel (red, green, blue, alpha, 8 bits each). data is width × height × 4 bytes, stored row by row, top to bottom, and left to right within each row.

png is the default format and keeps images compact. rgba8888 stores decoded pixels for readers that cannot decode PNG.

Bone

A single bone in the hierarchy. Bones are stored in topological order (parents before children), so a bone’s parent index is always less than its own index.

Field Type Description
name string Bone name
parent int16 Index of parent bone, or -1 for root bones

Mesh

A skinned mesh drawn from one atlas image.

Field Type Description
name string Mesh name
image uint16 Index into the file’s images
vertexCount uint16 Number of vertices
indexCount uint32 Number of triangle indices
vertices Vertex[vertexCount] Packed vertices, see Vertex
indices uint16[indexCount] Triangle list, three indices per triangle

Vertex

A single mesh vertex with its texture coordinate and skinning weights.

Field Type Description
x float32 X position in pixels (bind pose, world space)
y float32 Y position in pixels (bind pose, world space)
u float32 U texture coordinate (0–1)
v float32 V texture coordinate (0–1)
boneIds uint16[4] Indices of the influencing bones
boneWeights float32[4] Weight of each influencing bone, aligned with boneIds

Each vertex is skinned by exactly four bones. boneIds[i] and boneWeights[i] form one influence: the bone at that index and its weight. Unused slots are padded with a weight of 0, which contributes nothing and lets a reader ignore the paired bone index. Four is the common upper bound for GPU skinning, so the fixed width maps directly to a skinning attribute without a per-vertex count.

Vertex positions are in the bind pose, in world-space pixels centered on the world origin. Weights are read as authored and are not renormalized by the format.

Animation

A sampled animation. One Bone transform is stored per bone, per frame.

Field Type Description
name string Animation name
flags uint8 Bit 0 set = looping
frameCount uint16 Number of frames
frames BoneTransform[frameCount × boneCount] Transforms, see Bone transform

Frames are stored back to back with no per-frame prefix. Each frame is exactly boneCount transforms in the same order as the bones array, so a reader multiplies frameCount by boneCount to know how many transforms follow.

Bone transform

A bone’s pose within a single animation frame.

Field Type Description
x float32 X offset from parent (pixels, parent-local space)
y float32 Y offset from parent (pixels, parent-local space)
rotation float32 Rotation relative to parent (degrees)
scaleX float32 Scale perpendicular to the bone
scaleY float32 Scale along the bone (squash and stretch)
flags uint8 Bit 0 set = visible
zOrder int16 Draw order (higher draws on top)

Root bone transforms are in world space (pixels, centered on the origin). Child bone transforms are relative to their parent.

Reading a .pchr file

The file is read start to finish in a single pass. The example below decodes an entire file into plain data structures.

import struct


def read_pchr(path):
    with open(path, "rb") as f:
        data = f.read()
    o = 0

    def take(n):
        nonlocal o
        b = data[o : o + n]
        o += n
        return b

    def uint8():
        return take(1)[0]

    def uint16():
        return struct.unpack("<H", take(2))[0]

    def uint32():
        return struct.unpack("<I", take(4))[0]

    def int16():
        return struct.unpack("<h", take(2))[0]

    def float32():
        return struct.unpack("<f", take(4))[0]

    def string():
        return take(uint16()).decode("utf-8")

    # Image data format ids.
    FORMAT_PNG = 0
    FORMAT_RGBA8888 = 1

    assert take(4) == b"PCHR"
    version = uint32()
    fps = uint16()
    flags = uint16()

    images = []
    for _ in range(uint16()):
        name, width, height, fmt = string(), uint16(), uint16(), uint8()
        data = take(uint32())  # dataLength, then that many bytes
        images.append(
            {"name": name, "width": width, "height": height, "format": fmt, "data": data}
        )

    bones = [{"name": string(), "parent": int16()} for _ in range(uint16())]

    meshes = []
    for _ in range(uint16()):
        name, image = string(), uint16()
        vertex_count, index_count = uint16(), uint32()
        vertices = []
        for _ in range(vertex_count):
            x, y, u, v = float32(), float32(), float32(), float32()
            bone_ids = [uint16() for _ in range(4)]
            bone_weights = [float32() for _ in range(4)]
            influences = [
                (b, w) for b, w in zip(bone_ids, bone_weights) if w > 0
            ]
            vertices.append((x, y, u, v, influences))
        indices = [uint16() for _ in range(index_count)]
        meshes.append(
            {"name": name, "image": image, "vertices": vertices, "indices": indices}
        )

    animations = []
    for _ in range(uint16()):
        name, aflags = string(), uint8()
        frames = []
        for _ in range(uint16()):
            frames.append(
                [
                    {
                        "x": float32(), "y": float32(), "rotation": float32(),
                        "scaleX": float32(), "scaleY": float32(),
                        "flags": uint8(), "zOrder": int16(),
                    }
                    for _ in range(len(bones))
                ]
            )
        animations.append(
            {"name": name, "loop": bool(aflags & 1), "frames": frames}
        )

    return {
        "version": version, "fps": fps, "images": images,
        "bones": bones, "meshes": meshes, "animations": animations,
    }

Tooling

The pchr CLI inspects .pchr files and converts between the binary format and a JSON representation:

pchr inspect character.pchr
pchr pack    character.json -o character.pchr
pchr unpack  character.pchr -o ./out

The JSON is a convenience representation for authoring and inspection. It is not part of the format; a .pchr file never contains JSON. Its structure is described by a JSON Schema.