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

FieldTypeDescription
versionintFormat version (currently 1)
fpsintPlayback frame rate
imagesarrayAtlas images used by meshes
bonesarrayBone hierarchy (topological order)
meshesarraySkinned meshes
animationsarraySampled animations

Image

FieldTypeDescription
namestringImage name
pathstringRelative path to image file (.json) or image name (.pchr)
widthintImage width in pixels
heightintImage height in pixels

Bone

FieldTypeDescription
namestringBone name
parentIdxintIndex 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

FieldTypeDescription
namestringMesh object name
imageIdxintIndex into the images array
verticesarrayVertex data (position, UV, skinning)
trianglesarrayFlat array of vertex indices (3 per triangle)

Vertex

FieldTypeDescription
xfloatX position in pixels (world space, centered on origin)
yfloatY position in pixels (world space, centered on origin)
ufloatU texture coordinate (0–1)
vfloatV texture coordinate (0–1)
bonesarrayBone influences: { "idx": int, "weight": float }

Vertex positions are in the bind pose, in world-space pixels centered on the world origin.

Animation

FieldTypeDescription
namestringAnimation name
loopboolWhether the animation loops
framesarrayArray 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)

FieldTypeDescription
xfloatX offset from parent (pixels, parent-local space)
yfloatY offset from parent (pixels, parent-local space)
rotationfloatRotation relative to parent (degrees)
scaleXfloatScale perpendicular to bone
scaleYfloatScale along bone (squash/stretch)
visibleboolWhether this bone’s meshes are visible
zOrderintDraw 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)

OffsetSizeTypeDescription
04char[4]"PCHR" magic
44uint32 LEVersion (1)
84uint32 LEJSON length (N)
124uint32 LEImage 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 .pchr uses the exact same schema as the standalone .json format.
  • In .pchr files, the image path field 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_len will be 0 and no image bytes follow the JSON.