File Format¶
Both OTS files are plain UTF-8 JSON, indented and readable. You can open one in a text editor, diff two of them in version control, or write a script that reads one. That's deliberate — an interchange format you can't inspect is an interchange format you can't debug.
The current format is version 4.
Shared header¶
Both formats start the same way:
{
"format": "octo_anm",
"version": 4,
"source": "harmony",
"fps": 24.0,
"coordinate_system": {
"up": "y",
"hand": "right",
"unit": "meters",
"rot_order": "xyz",
"rot_repr_anim": "euler_deg",
"rot_repr_rest": "quat_wxyz"
}
}
| Key | Meaning |
|---|---|
format |
octo_pvt or octo_anm |
version |
The format version. Must match exactly — see below |
source |
Which application wrote it: harmony, blender, or maya |
fps |
The source scene's frame rate |
coordinate_system |
The space the values are in — see Coordinate Systems |
No backward migration
A reader accepts only its own version. A v3 file loaded into a v4 build reports:
unsupported version 3 (this build writes/reads v4; no backward migration — re-export from the source DCC)
This is a choice, not an oversight. Silently migrating an old file means guessing what its numbers meant, and a wrong guess produces a rig that's subtly off rather than obviously broken. Re-exporting from the source takes seconds and is always correct.
.pvt — pivot file¶
{
"format": "octo_pvt",
"version": 4,
"source": "harmony",
"name": "spaceship",
"scale_factor": 1.0,
"pivot_space": "local",
"fps": 24.0,
"coordinate_system": { "…": "…" },
"bones": [
{
"name": "hip",
"parent": null,
"pivot": [0.0, 0.0, 0.0],
"rotation": [1.0, 0.0, 0.0, 0.0],
"path": "Armature/hip",
"rig": "spaceship",
"is_camera": false
}
],
"cameras": []
}
| Key | Meaning |
|---|---|
name |
The rig's name |
scale_factor |
The user-facing Scale option. Pivots are multiplied by it on write, divided on read |
pivot_space |
local (relative to parent) or armature |
bones[].pivot |
Rest position |
bones[].rotation |
Rest rotation, as a quaternion [w, x, y, z] |
bones[].path |
The item's full path in its source scene — used for matching, and to keep same-named items in different rigs distinct |
bones[].rig |
The rig tag |
path, rig, and is_camera are omitted when empty, so a simple file stays small.
.anm — animation file¶
{
"format": "octo_anm",
"version": 4,
"source": "maya",
"frame_range": [1, 60],
"fps": 24.0,
"coordinate_system": { "…": "…" },
"pegs": [
{
"name": "hip-P",
"path": "Top/spaceship/hip-P",
"parent": "root",
"rest_pivot": [0.0, 0.0, 0.0],
"rest_rotation": [1.0, 0.0, 0.0, 0.0],
"rig": "spaceship",
"is_camera": false,
"source_modes": {
"position": "separate",
"rotation": "euler",
"scale": "separate",
"enable_3d": true
},
"keyframes": [
{
"frame": 1,
"pos": [0.0, 0.0, 0.0],
"rot": [0.0, 0.0, 0.0],
"rot_quat": [1.0, 0.0, 0.0, 0.0],
"scl": [1.0, 1.0, 1.0],
"interp": "BEZIER",
"handles": { "pos": [], "rot": [], "scl": [] }
}
]
}
],
"cameras": []
}
The self-sufficiency fields¶
parent, rest_pivot, rest_rotation, and rig are the .pvt content, folded into each peg. They're the reason Create Rig can build a working skeleton from a .anm alone. See Animation Files (.anm).
Keyframes¶
| Key | Meaning |
|---|---|
frame |
Frame number |
pos |
Position, canonical metres |
rot |
Euler XYZ, degrees — the human-readable form |
rot_quat |
The same rotation as a quaternion [w, x, y, z], from the world-matrix bake |
scl |
Scale |
interp |
BEZIER, LINEAR, or CONSTANT |
handles |
Absolute bezier handles, per axis, grouped by channel |
rot_quat wins
When both are present, rot_quat is authoritative and every importer prefers it. It's order-independent, so it sidesteps Euler-order ambiguity entirely. rot is kept as a readable fallback.
Handles are not emitted alongside rot_quat by the Maya and Blender bakes, because local-frame handles don't map onto a baked world curve. The interpolation type still travels.
Handles are absolute [[left_x, left_y], [right_x, right_y]] pairs in (frame, value) space — one per axis, grouped into pos, rot, and scl. Absolute rather than relative, so they don't have to be re-derived against a frame rate on the way in.
source_modes¶
A record of how the peg was authored in its source application — separate or path-based position, Euler or quaternion-path rotation, and whether 3D was enabled.
It's a hint, and only a hint
source_modes never affects any value. It's diagnostic information for a human reading the file. An importer will not change how it writes a curve because of it.
Cameras¶
Both formats carry the same camera block, linked to its animated item by name:
| Key | Meaning |
|---|---|
name, peg, path |
Identity, and which item animates it |
is_default |
Whether this is the scene's default camera |
fov, fov_axis |
Field of view — vertical, matching Harmony's convention |
override_fov |
Harmony's per-camera FOV override flag |
near_plane, far_plane |
Clip planes, as canonical distances |
near_field, far_field |
Harmony's original clip values in fields, preserved so a Harmony round trip is lossless |
angle |
Roll, degrees about Z |
offset_x/y/z, pivot_x/y |
Placement relative to the peg |
rig |
The rig tag |
See Cameras.
Practical notes¶
Files are small. A full character rig with a 60-frame shot is a few hundred kilobytes. They're fine to email, put in version control, or keep alongside a shot as a record.
They're diffable. Two exports of the same rig produce comparable JSON, which makes "what changed between these two versions" a text diff rather than a guess.
Naming is up to you. OTS doesn't care what the file is called; the format key inside it is what identifies it. Keeping the .pvt / .anm extensions just makes the file pickers behave.