.panim File Format
.panim is Pluto’s binary animation format. It extends the SM64 DMA animation layout (values + indices) with a metadata header and optional per-bone translation and scale tracks stored in tagged sections appended after the core data.
All multi-byte integers are big-endian unless otherwise noted.
High-Level Layout
[Header] - fixed 103 bytes
[values section] - tagged: "values" + raw s16 stream
[indices section] - tagged: "indices" + raw s16 stream
[translations] - optional tagged section
[scales] - optional tagged section
Header (bytes 0x00 – 0x66)
| Offset | Size | Type | Field | Notes |
|---|---|---|---|---|
0x00 | 64 | char[64] | Name | UTF-8, null-padded to 64 bytes |
0x40 | 32 | char[32] | Author | UTF-8, null-padded to 32 bytes |
0x60 | 1 | u8 | Looping | 0xA6 = looping, 0x00 = one-shot |
0x61 | 2 | u16 BE | Length | Frame count (exclusive end), i.e. loopEnd |
0x63 | 1 | u8 | Node count | Number of animatable bones, capped at 255 |
Total header size: 100 bytes (0x64). The values section tag begins immediately after at offset 0x64.
BoneCountis derived at load time from the indices section:indices.size() / 6 - 1.
Values Section
"values" <-- 6 ASCII bytes, no length prefix
<s16 stream, big-endian>
Begins at offset 0x64. The literal ASCII string values (6 bytes) acts as the section tag. It is followed immediately by the raw rotation value table - a flat array of s16 big-endian shorts, identical in layout to the SM64 DMA animation values array.
The section ends when the loader finds the literal string indices scanning forward two bytes at a time.
Indices Section
"indices" <-- 7 ASCII bytes, no length prefix
<s16 stream, big-endian>
Following the values section is the 7-byte tag indices. The raw index table follows - also a flat array of s16 big-endian shorts matching the SM64 DMA animation index array.
Each animatable bone occupies 6 shorts (3 axes * 2 shorts each: numFrames + startOffset). The root bone’s 6 shorts come first, followed by one entry per non-root bone. Therefore:
BoneCount = (indices.size() / sizeof(s16)) / 6 - 1
The section ends when either:
- The 12-byte tag
translationsis found, or - EOF is reached.
Translations Section (optional)
Stores per-bone, per-frame local translation offsets for all non-root animatable bones. Only present when at least one bone has a non-zero translation track.
"translations" <-- 12 ASCII bytes
[2] frame_count u16 BE - number of frames in all tracks
[2] bone_count u16 BE - number of non-root animatable bones tracked
[ceil(bone_count/8)] bitmask - one bit per bone; 1 = data present for that bone
[data] s16 x 3 x frame_count per present bone, big-endian
Bitmask
ceil(bone_count / 8) bytes immediately follow the header. Bit bi % 8 of byte bi / 8 is 1 if bone bi has translation data; bones with all-zero translations are omitted and their bit is 0.
Data layout
Only bones whose bitmask bit is 1 have data. They are written in ascending bone index order. For each present bone, frame_count frames of (tx, ty, tz) are written:
for each present bone (in ascending index order):
for each frame in [0, frame_count):
s16 tx (big-endian)
s16 ty
s16 tz
Total data size for one present bone: frame_count x 6 bytes.
Addressing
In memory (PlutoAnim::Translations), the flat array is indexed bone-major:
index = (bone_idx * frame_count + frame) * 3 + axis
where bone_idx is 0-based from the first non-root bone (bone 1 in the skeleton), frame is the current animation frame, and axis is 0=X, 1=Y, 2=Z.
Units
Raw s16 game units, matching the existing SM64 translation scale. Values are read directly into Vec3s and added to the bone’s animated translation each frame. Clamped to [-32768, 32767] at export.
Scales Section (optional)
Stores per-bone, per-frame scale factors for all non-root animatable bones. Only present when at least one bone has a non-identity scale track.
"scales" <-- 6 ASCII bytes
[2] frame_count u16 BE
[2] bone_count u16 BE
[ceil(bone_count/8)] bitmask
[data] s16 x 3 x frame_count per present bone, big-endian
The layout is basically identical to the translations section. Bones with unchanged scale (1.0, 1.0, 1.0) on all frames are omitted.
Encoding
Scale is stored as a fixed-point s16:
stored = round(scale_float * 1024)
scale_float = stored / 1024.0
Identity 1.0 -> 1024. The default value for entries not present in the file is 1024 (identity).
Clamped to s16 range at export: max(-32768, min(32767, round(scale * 1024))).
Addressing
index = (bone_idx * frame_count + frame) * 3 + axis
Same bone-major layout as translations. CustomBoneCountScale tracks the bone count separately from the translations section.