Animations¶
studiohdr_t.localanimindexreaches an array ofmstudioanimdesc_t, one per animation. Every model in Team Fortress 2 has at least one, static props included – there are 16021 of them across the game’s 9858 models.animdescs are not sequences. An animdesc is raw animation data; a
mstudioseqdesc_tis what the game plays, and it may blend several animdescs together. The local innumlocalanimmatters: a model using$includemodelborrows animations from other files, and the engine merges the lists at runtime. ValveMDL reads the local set, which is what is actually in the file.Where the data is.
animblockselects it, and the SDK’spAnimBlockspells out all three cases:if (block == -1) return NULL; // broken if (block == 0) return (mstudioanim_t *)(((byte *)this) + index); // local ... // .ani
animblock == 0means the nodes sit in the mdl atanimindex, relative to the animdesc. Non-zero means they live in the external.aninamed bystudiohdr_t.szanimblocknameindex, atanimindexfrom that block’sdatastart.In practice every animdesc in Team Fortress 2 is local: all 16021 of them have
animblock == 0. Five models even ship a populated animblock table and a real multi-megabyte.anibeside them –boss_tank_part1_destruction.mdlhas 150 blocks and a 4.3 MB sidecar – and still keep every animdesc local. Why those files exist is an open question; nothing appears to read them.Zeroframe data is dead.
zeroframeindexis a low resolution cache for stalled async loads. Not one animdesc in the game uses it. The v47 rule that it must be zero, and the v48 permission to use it, are both moot here.Sections. When
sectionframesis non-zero the frames are chunked, andsectionindexreaches an array ofmstudioanimsections_t, each naming its own block and index. 2.34% of animdescs use this,headless_hatmanamong them. The section count is(numframes / sectionframes) + 2, not + 1: long animations get an extra section holding just the last frame. Sectioned animations written by v45 are declared incompatible byStudio_ConvertStudioHdrToNewVersion.The compressed node stream.
mstudioanim_tis the hardest thing in the format, and the only structure here whose real size is not the size of its declaration.It is a linked list, not an array. Each node is 4 bytes of header followed by a variable length payload, and
nextoffset– relative to the node – is the only way to find the next one. Zero ends the list. You cannot index it, and you cannot skip a node without decoding enough of it to know its length.The payload starts after the header, and its layout is decided by ``flags``. Note that
pData()returnsthis + sizeof(mstudioanim_t), i.e. +4, so the payload sits afternextoffsetrather than between it and the flags:
Flag
Value
Bytes
Payload
STUDIO_ANIM_RAWROT0x02
6
a Quaternion48
STUDIO_ANIM_RAWROT20x20
8
a Quaternion64
STUDIO_ANIM_RAWPOS0x01
6
a Vector48
STUDIO_ANIM_ANIMROT0x08
6
STUDIO_ANIM_ANIMPOS0x04
6
STUDIO_ANIM_DELTA0x10
0
none; the values are additive instead
Rotation always precedes position, and the SDK computes the position’s offset with boolean arithmetic rather than a branch:
inline Vector48 *pPos( void ) const { return (Vector48 *)(pData() + ((flags & STUDIO_ANIM_RAWROT) != 0) * sizeof( *pQuat48() ) + ((flags & STUDIO_ANIM_RAWROT2) != 0) * sizeof( *pQuat64() ) ); };Likewise
pPosV()ispRotV() + ((flags & STUDIO_ANIM_ANIMROT) != 0). The raw and animated forms can be mixed freely – a bone whose rotation is animated but whose position never moves is the common case.Only 12 flag combinations occur across the whole of Team Fortress 2, and no node uses a bit outside the six defined ones.
RAWPOS|RAWROT2is the most common by far, then bare nodes with no payload at all, thenRAWROT2, thenANIMPOS|ANIMROT.The animated forms point at run length encoded streams. An
mstudioanim_valueptr_tholds one offset per axis, each reaching a chain ofmstudioanimvalue_t– a two byte union that is either a{valid, total}run header or ashortvalue, depending on where you are in the stream. Each run is one header followed byvalidvalues; the run then holds its last value fortotal - validfurther frames. Seeking to framekmeans walking runs and subtractingtotaluntil you land inside one.A node ends where the next one begins. Nodes are packed end to end, streams included, so a node’s true extent is exactly its
nextoffset. That is a strong check and ValveMDL uses it as one: every one of the 295057 animation nodes in Team Fortress 2’s 9858 models is measured and lands exactly on the byte the next node starts at. SeeMdl.validate().bone 255 ends the list.
bone_setup.cppwalks nodes withwhile (panim && panim->bone < 255), so a node naming bone 255 is a terminator rather than a bone, and carries no payload whatever its flags say. An animation that moves nothing at all is written as exactly one of these:ff 00 00 00. There are 3293 of them in the game.The scale is not here. Values are multiplied by the bone’s
posscaleorrotscale– seemstudiobone_t. An animation read without its skeleton is meaningless numbers.
- struct valvemdl.structs.anim.mstudioanim_t¶
- struct valvemdl.structs.anim.mstudioanim_valueptr_t¶
- struct valvemdl.structs.anim.mstudiomovement_t¶
- struct valvemdl.structs.anim.mstudioanimsections_t¶
- struct valvemdl.structs.anim.mstudioanimdesc_t¶
-
baseptr
(<parsed from long>¶int) NEGATIVE offset back to the
studiohdr_t
-
movementindex
(<parsed from long>¶int) offset to an array of
mstudiomovement_t
-
animblock
(<parsed from long>¶int) 0 means the nodes are here in the mdl; >0 names a block in the external .ani; -1 means the model needs recompiling
-
animindex
(<parsed from long>¶int) offset to the first
mstudioanim_t; non-zero when the data is not in sections
-
ikruleindex
(<parsed from long>¶int) offset to an array of
mstudioikrule_t; non-zero when the ik data is in the mdl
-
animblockikruleindex
(<parsed from long>¶int) offset to the ik data when it lives in the .ani instead
-
localhierarchyindex
(<parsed from long>¶int) offset to an array of
mstudiolocalhierarchy_t
-
sectionindex
(<parsed from long>¶int) offset to an array of
mstudioanimsections_t
-
baseptr