# INR Format Specification (v1)
INR is a runtime/deployment format for Inochi2D puppets, in the spirit of
glTF/GLB: a small JSON index plus one binary blob with all heavy data laid
out for direct GPU upload. It is produced from INX/INP (authoring formats)
by the `inochi2d-parser` exporter (feature `inr-export`) and is intended
for engines and renderers
that want to consume puppets without parsing nested JSON trees, decoding
images, or resolving UUID cross-references at load time.
INR is **not** an authoring format. Round-tripping back to INX is not a
goal; editor-only data (node groups, automation, vendor extensions, PSD
layer paths) is dropped on export.
## 1. Container
All integers are little-endian.
| 0 | 4 | Magic `b"INR1"` |
| 4 | 4 | `u32` container version (currently `1`) |
| 8 | 4 | `u32` JSON chunk length (4-aligned) |
| 12 | 4 | `u32` BIN chunk length |
| 16 | — | JSON chunk (UTF-8, space-padded to 4) |
| 16+J | — | BIN chunk |
Readers must reject unknown magic and unknown major versions.
## 2. Conventions
- **Coordinate space**: puppet space is **Y-down** (screen-like), matching
Inochi2D. Engines with Y-up worlds flip with `scale.y = -1` (or negate Y
positions) at integration time.
- **Draw order**: **higher `zsort` = further back** = drawn first
(painter's back-to-front). Children inherit ordering within their parent.
- **Cross-references**: everything references by **array index**
(`nodes`, `params`, `textures`, `buffer_views`). UUIDs are provenance
metadata only; consumers never need to build UUID maps.
- **Node order**: `nodes` is the tree flattened in **pre-order**; a parent
always precedes its children, so a single forward pass can cascade
world transforms.
- **Enums**: lowercase `snake_case` strings (`"clip_to_lower"`,
`"spring_pendulum"`, `"angle_length"`). Unknown values should fall back
to defaults (`"normal"`, `"linear"`, `"additive"`), not error.
## 3. JSON chunk
Top-level object (`InrDoc`):
```jsonc
{
"asset": { "generator": "inochi2d-parser 0.2.0", "version": 1 },
"meta": { /* name/artist/rights… optional strings */ },
"physics": { "pixels_per_meter": 1000.0, "gravity": 9.8 },
"buffer_views": [ { "offset": 0, "length": 1024 }, … ],
"nodes": [ … ],
"params": [ … ],
"animations": [ … ],
"mask_contours": { /* optional, see 3.5 */ },
"textures": [ … ]
}
```
### 3.1 buffer_views
`{ "offset": u32, "length": u32 }` — byte ranges into the BIN chunk.
Offsets are always 4-aligned, so `f32`/`u32` payloads can be reinterpreted
zero-copy (or memory-mapped).
### 3.2 nodes
```jsonc
{
"name": "Head", "uuid": 123456,
"parent": 0, // index into nodes; absent on the root
"translation": [x,y,z], "rotation": [rx,ry,rz], "scale": [sx,sy],
"mesh": { … }, // part / mask / meshgroup
"part": { … }, // kind == "part"
"composite": { … }, // kind == "composite"
"physics": { … } // kind == "simplephysics"
}
```
`mesh`:
```jsonc
{
"vertex_count": 312,
"positions": 7, // buffer view: f32 [x,y] * vertex_count
"uvs": 8, // buffer view: f32 [u,v] * vertex_count
"indices": 9, // buffer view: u32 triangle indices
"origin": [ox, oy]
}
```
Final vertex position = `(position - origin + deform) * node world transform`.
`part`:
```jsonc
{
"textures": [0, -1, -1], // texture indices [albedo, emissive, bump]; -1 = none
"blend_mode": "normal",
"tint": [1,1,1], "screen_tint": [0,0,0],
"opacity": 1.0, "emission_strength": 0.0,
"mask_threshold": 0.5,
```
`composite` has the same shading fields minus textures/emission. Composite
nodes render their subtree to an offscreen target and composite it back
with their blend/opacity/tint.
```jsonc
{
"blend_mode": "normal",
"tint": [1,1,1], "screen_tint": [0,0,0],
"opacity": 1.0, "mask_threshold": 0.5,
"masks": [ … ],
```
`compose_hint` is baked by the exporter from a conservative overlap
analysis of the composite's child geometry across authored param samples:
- `"children_disjoint"` — children proven pairwise disjoint at every
sampled pose; the group blend/opacity may be applied per child with
identical results, skipping offscreen compositing.
- `"children_overlap"` — children overlap at some pose, or the analysis
was inconclusive; correct rendering needs real offscreen compositing.
Absent on identity composites and files from older exporters; readers
must treat a missing hint as `"children_overlap"` (the safe fallback).
`physics` (SimplePhysics):
```jsonc
{
"param": 4, // index into params; -1 = unresolved
"gravity": 1.0, "length": 100.0, "frequency": 1.0,
"angle_damping": 0.5, "length_damping": 0.5,
"output_scale": [1,1], "local_only": false
}
```
### 3.3 params
```jsonc
{
"name": "Head Yaw", "uuid": 42, "is_vec2": true,
"min": [-1,-1], "max": [1,1], "defaults": [0,0],
"axis_points": [[0,0.5,1],[0,1]],
"merge_mode": "additive", // | multiplicative | override | forced
"bindings": [ … ]
}
```
Binding:
```jsonc
{
"node": 17,
"target": "transform.t.x", // Inochi2D property names; "deform", "opacity"
"is_set": [true, …], // row-major [x][y], x_count*y_count entries
"kind": "scalar", // | "deform"
"view": 14
}
```
- `kind: "scalar"` → view holds `f32 * x_count * y_count` (row-major [x][y]).
- `kind: "deform"` → view holds `[f32;2] * x_count * y_count * vertex_count`
per-vertex offsets, grouped per X-axis point.
### 3.4 animations
Keyframes are small and stay inline:
```jsonc
{
"name": "idle", "timestep": 0.0166667, "additive": false,
"length": 120, "lead_in": 0, "lead_out": 0, "weight": 1.0,
"lanes": [
{
"param": 4, "target": 0, // param index; 0 = X, 1 = Y
"interpolation": "linear", "merge_mode": "forced",
"keyframes": [ [frame, value, tension], … ]
}
]
}
```
### 3.5 mask_contours
Optional top-level map, omitted when empty. Keyed by **node uuid**
(stringified in JSON); value is a list of outer **CCW polygons in UV
space** (`0..1`), each polygon a list of `[u, v]` points:
```jsonc
"mask_contours": {
"123456": [ [ [0.12, 0.30], [0.85, 0.31], … ] ]
}
```
These are alpha silhouettes baked by the exporter for parts referenced as
mask sources. Renderers doing CPU mask clipping should use them as the
source contour instead of the source mesh triangles — the mesh is usually
a loose quad around the visible texture and would over-clip.
Readers that render masks on the GPU (stencil/alpha test) can ignore this
map entirely.
### 3.6 textures
```jsonc
{
"width": 1024, "height": 1024,
"format": "rgba8", // pixel layout, row-major, no padding
"color_space": "srgb", // "srgb" | "linear" (RGB channel encoding)
"premultiplied": false, // RGB premultiplied by alpha, in color_space
"view": 3
}
```
Inochi2D sources ship premultiplied sRGB PNG/TGA. The exporter converts
them to **straight (un-premultiplied) alpha sRGB** and dilates edge colors
into fully transparent texels, so files are always `"srgb"` +
`premultiplied: false`. This is the texture convention engines like Bevy,
Unity, Unreal and Godot expect: upload as an sRGB format (hardware decode),
premultiply in-shader, blend in linear space. Edge dilation prevents dark
fringes from bilinear filtering across transparent texels.
Readers must still honor the declared `color_space`/`premultiplied` flags
rather than assuming them, so other generators can store different
conventions.
## 4. Versioning
- Container version bumps only on layout-breaking changes.
- JSON additions are backwards-compatible: readers must ignore unknown
fields.