Format: Pachario 2D
Pachario (Paper Character Studio) is an unreleased 2D animation software with an open file format. This add-on supports Pachario’s file format specification as an export format. A runtime for using Pachario animations in Unity, Godot, and other engines is currently in development and will be released in the future.
The add-on exports 2D skeletal animations in two Pachario formats: JSON (.json) and Packed Binary (.pchr). Both contain the same skeleton data — bones, skinned meshes, vertex weights, and sampled animation frames. The only difference is how the atlas image is delivered.
- JSON (
.json) — Plain JSON file. The atlas image is saved alongside it as a separate PNG. - Packed Binary (
.pchr) — A single file containing both the JSON data and the atlas image bytes.
JSON format (.json)
The JSON format exports a plain .json file alongside a sidecar PNG atlas image with a relative path reference.
Schema
{
"version": 1,
"fps": 24,
"images": [
{
"name": "atlas",
"path": "atlas.png",
"width": 1024,
"height": 1024
}
],
"bones": [
{
"name": "root",
"parentIdx": -1
},
{
"name": "spine",
"parentIdx": 0
}
],
"meshes": [
{
"name": "body",
"imageIdx": 0,
"vertices": [
{
"x": 0.0,
"y": 100.0,
"u": 0.5,
"v": 0.5,
"bones": [
{ "idx": 0, "weight": 0.8 },
{ "idx": 1, "weight": 0.2 }
]
}
],
"triangles": [0, 1, 2, 0, 2, 3]
}
],
"animations": [
{
"name": "idle",
"loop": true,
"frames": [
[
{
"x": 0.0,
"y": 0.0,
"rotation": 0.0,
"scaleX": 1.0,
"scaleY": 1.0,
"visible": true,
"zOrder": 0
}
]
]
}
]
}
Field reference
Root
| Field | Type | Description |
|---|---|---|
version | int | Format version (currently 1) |
fps | int | Playback frame rate |
images | array | Atlas images used by meshes |
bones | array | Bone hierarchy (topological order) |
meshes | array | Skinned meshes |
animations | array | Sampled animations |
Image
| Field | Type | Description |
|---|---|---|
name | string | Image name |
path | string | Relative path to image file (.json) or image name (.pchr) |
width | int | Image width in pixels |
height | int | Image height in pixels |
Bone
| Field | Type | Description |
|---|---|---|
name | string | Bone name |
parentIdx | int | Index of parent bone, or -1 for root bones |
Bones are stored in topological order (parents before children), so a bone’s parentIdx is always less than its own index.
Mesh
| Field | Type | Description |
|---|---|---|
name | string | Mesh object name |
imageIdx | int | Index into the images array |
vertices | array | Vertex data (position, UV, skinning) |
triangles | array | Flat array of vertex indices (3 per triangle) |
Vertex
| Field | Type | Description |
|---|---|---|
x | float | X position in pixels (world space, centered on origin) |
y | float | Y position in pixels (world space, centered on origin) |
u | float | U texture coordinate (0–1) |
v | float | V texture coordinate (0–1) |
bones | array | Bone influences: { "idx": int, "weight": float } |
Vertex positions are in the bind pose, in world-space pixels centered on the world origin.
Animation
| Field | Type | Description |
|---|---|---|
name | string | Animation name |
loop | bool | Whether the animation loops |
frames | array | Array of frames, each frame is an array of bone transforms |
Each frame contains one entry per bone, in the same order as the bones array.
Bone transform (per frame)
| Field | Type | Description |
|---|---|---|
x | float | X offset from parent (pixels, parent-local space) |
y | float | Y offset from parent (pixels, parent-local space) |
rotation | float | Rotation relative to parent (degrees) |
scaleX | float | Scale perpendicular to bone |
scaleY | float | Scale along bone (squash/stretch) |
visible | bool | Whether this bone’s meshes are visible |
zOrder | int | Draw order (higher = drawn on top) |
Root bone transforms are in world space (pixels, centered on origin). Child bone transforms are relative to their parent.
Packed binary format (.pchr)
The packed binary format bundles the JSON data and atlas image into a single .pchr file.
Binary layout
Offset Size Type Description
------ ------ ---------- ---------------------------
0 4 char[4] Magic bytes: "PCHR"
4 4 uint32 LE Format version (currently 1)
8 4 uint32 LE JSON data length in bytes (N)
12 4 uint32 LE Image data length in bytes (M)
16 N bytes JSON data (UTF-8, same schema as above)
16+N M bytes Image file bytes (raw PNG)
Header (16 bytes)
| Offset | Size | Type | Description |
|---|---|---|---|
| 0 | 4 | char[4] | "PCHR" magic |
| 4 | 4 | uint32 LE | Version (1) |
| 8 | 4 | uint32 LE | JSON length (N) |
| 12 | 4 | uint32 LE | Image length (M) |
Reading a .pchr file
import struct
import json
with open("character.pchr", "rb") as f:
magic = f.read(4)
assert magic == b"PCHR"
version, json_len, image_len = struct.unpack("<III", f.read(12))
json_data = json.loads(f.read(json_len).decode("utf-8"))
image_data = f.read(image_len) # Raw PNG bytes
use std::fs;
fn read_pchr(path: &str) -> (serde_json::Value, Vec<u8>) {
let data = fs::read(path).unwrap();
assert_eq!(&data[0..4], b"PCHR");
let version = u32::from_le_bytes(data[4..8].try_into().unwrap());
let json_len = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
let image_len = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize;
let json_data: serde_json::Value =
serde_json::from_slice(&data[16..16 + json_len]).unwrap();
let image_data = data[16 + json_len..16 + json_len + image_len].to_vec();
(json_data, image_data)
}
Notes
- The JSON payload inside
.pchruses the exact same schema as the standalone.jsonformat. - In
.pchrfiles, the imagepathfield contains just the image name (no file path) since the image is embedded. - Image data is raw file bytes (PNG), not decoded pixel data.
- If no atlas image is set,
image_lenwill be0and no image bytes follow the JSON.