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.
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
Download
Prebuilt binaries (no dependencies, single file). Download, mark executable, and put it on your PATH:
| Platform | Download |
|---|---|
| macOS (Apple) | pchr-darwin-arm64 |
| macOS (Intel) | pchr-darwin-amd64 |
| Linux (x86-64) | pchr-linux-amd64 |
| Linux (arm64) | pchr-linux-arm64 |
| Windows (x86-64) | pchr-windows-amd64.exe |
| Windows (arm64) | pchr-windows-arm64.exe |
Verify against checksums.txt; see manifest.json for the current version. On macOS/Linux:
curl -Lo pchr https://pchr.nick-glenn.com/downloads/pchr-darwin-arm64
chmod +x pchr
./pchr version
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.