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
- All multi-byte integers and floats are little-endian. Fields are read in the exact order they appear.
- Integer fields are named by signedness and width:
uint8,uint16,uint32, andint16.float32is an IEEE-754 single-precision float. - Strings are the
stringtype: auint16byte length followed by that many UTF-8 bytes. - Arrays are written as
T[count], wherecountnames the preceding field that holds the number ofTelements. That count field and its integer type are shown in each schema. - Raw byte blocks (
uint8[N]) are sized by a length field shown in each schema, such as the imagedataLengththat precedes the imagedata.
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
inspectprints the header and a summary of each section, similar to howfileandreadelfreport structure.packreads a JSON description and its referenced image files, encodes the pixel data, and writes a packed.pchr.unpackwrites a JSON description of the skeleton alongside the atlas images, decoded to PNG.
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.