Skip to main content

ANIMBIN

Skeletal animation clips. Produced by tools/blender_animbin.py, parsed by AnimationManager::LoadAnimation, played by SkeletonController::PlayAnimation.

Every multi-byte field is little-endian.

FieldTypeBytesNotes
magicchar[7]7ANIMBIN, no terminator. Mismatch aborts the load.
versionuint8_t1Exporter writes 1.
numAnimationsuint8_t1

Animation record​

Repeated numAnimations times.

FieldTypeBytesNotes
namechar[32]32Fixed width, null-padded.
flagsuint32_t4Only bit 0 is read anywhere: set means looping.
lengthuint16_t2Clip length in frames. See the time base section.
numTracksuint16_t2
numMarkersuint16_t2

Then numTracks track records, then numMarkers marker records. Markers come after all tracks, not interleaved.

Track record​

FieldTypeBytesNotes
typeuint8_t1Parsed, stored, never read. Playback keys off each Key.keyType instead.
jointIduint8_t1Index into the skeleton's bone array.
numKeysuint16_t2

Key record​

FieldTypeBytesNotes
frameuint16_t2Frame this key applies at.
keyTypeuint8_t10 = ROTATION, 1 = TRANSLATION. Selects the payload below.
value (ROTATION)int16_t x48Quaternion w, x, y, z in FP12 (x4096).
value (TRANSLATION)int32_t x312Vector x, y, z, copied raw into a psyqo::Vec3.

Marker record​

FieldTypeBytes
namechar[32]32
frameuint16_t2

Time base​

Frame numbers are GPU vsync ticks, not a fixed authoring rate and not milliseconds. Renderer::Process computes deltaTime as the difference between successive GPU::getFrameCount() values and SkeletonController::PlayAnimation adds that straight to the current frame with no conversion.

The practical consequence: a clip advances one frame per refresh, so the same file plays about 20% faster on NTSC than on PAL. Your Blender scene frame rate has no effect on playback speed; it only decides how many keys get baked. If you need consistent timing across regions you have to scale deltaTime by the refresh rate yourself.

Interpolation​

Rotation keys are interpolated with Slerp, which despite the name is a normalised linear interpolation. Its own comment notes it is only correct for small rotations, which is fine for adjacent keyframes and not fine for large steps. There is no blending between clips: SetAnimation is an instant cut.

Non-looping clips clamp and hold on the last frame rather than stopping.

Capacity limits​

These are the fixed array sizes in src/animation/animation.hh.

ConstantValueBounds
MAX_ANIMATIONS5animations per file
MAX_TRACKS50tracks per animation
MAX_KEYS30keys per track
MAX_MARKERS5markers per animation

The exporter only clamps against MAX_KEYS in ALL export mode. ACTIVE and SELECTED take whatever frame range you give them, and nothing clamps the track count against the number of deform bones in your rig. Check your counts before exporting.

Caveats​

  • Translation keys are parsed and never applied. The format defines them, the reader has a full branch for them, and PlayAnimation handles only ROTATION before its // TODO: translation. The exporter never emits one either: it bakes channel_types={'ROTATION'} and writes keyType = 0 for every key. Animation is rotation-only in practice.
  • Markers are parsed and never emitted. The exporter hardcodes numMarkers to 0 and has no marker-writing code.
  • Track.type and version are both dead. Written, parsed, never consulted.

Changelog​

  • Version 1 (2025-10-11): initial format.