Flexes¶
Facial animation, and the most layered corner of the format. Only 204 of Team Fortress 2’s 9858 models have any of it – character heads, essentially.
There are four distinct things here and it is easy to conflate them:
mstudioflexdesc_tis a target: a named shape the mesh can be deformed into, likeAU1orrightBrowRaiser. It is one string and nothing else.
mstudioflexcontroller_tis an input: a slider the game sets, with a range. Controllers and targets are not one to one.
mstudioflexrule_tandmstudioflexop_tare the wiring between them: a small stack machine, one rule per target, computing that target’s weight from the controllers.
mstudioflexcontrollerui_tis presentation only – how the sliders group together in Valve’s face poser. It affects nothing about the model.The deformation itself lives on the meshes:
mstudiomesh_t.flexindexreachesmstudioflex_t, which names its target and carries the per-vertex deltas.The rules are reverse polish. Each
mstudioflexop_tis an opcode and a four byte operand that is either an int or a float depending on the opcode –STUDIO_CONSTpushesd.valueas a float,STUDIO_FETCH1reads controllerd.index, andSTUDIO_ADDand friends pop two and push one. A rule is evaluated by running its ops over a stack and taking what is left.mstudioflexcontrollerui_t does pointer arithmetic on its own fields. The SDK reads its three indices with
*( &szindex0 + index ), which only works because the three are declared adjacent. Their meaning changes withstereoandremaptype, and despite theszprefix they are not string offsets – they point atmstudioflexcontroller_t.mstudiovertanim_t has two discriminators, and they are unrelated to each other.
Which union member is live –
short delta[3]orfloat16 flDelta[3]– is decided bySTUDIOHDR_FLAGS_FLEXES_CONVERTEDon the header. The flag means the runtime has already converted the fp16 disk form into fixed point in the mapped image. It is set on no file in Team Fortress 2, so on disk the fp16 member is always the live one.How big the structure is – 16 bytes or 18 – is decided by
mstudioflex_t.vertanimtype, which selectsmstudiovertanim_wrinkle_t. The SDK spells this out inVertAnimSizeBytes().So one array is described by two fields that live in different structures and know nothing about each other. ValveMDL reads the fp16 form and sizes the array from
vertanimtype.The fixed point scale, when it is needed, is
studiohdr_t.flVertAnimFixedPointScaleifSTUDIOHDR_FLAGS_VERT_ANIM_FIXED_POINT_SCALEis set and1/4096otherwise. Exactly one model in Team Fortress 2 sets that flag.
- struct valvemdl.structs.flex.mstudioeyeball_t¶
- struct valvemdl.structs.flex.mstudiovertanim_wrinkle_t¶
- struct valvemdl.structs.flex.mstudiovertanim_t¶
- struct valvemdl.structs.flex.mstudioflex_t¶
-
flexdesc
(<parsed from long>¶int) the
mstudioflexdesc_tthis deformation targets
-
vertindex
(<parsed from long>¶int) offset to an array of
mstudiovertanim_t, or ofmstudiovertanim_wrinkle_t; vertanimtype decides which
-
flexdesc
- struct valvemdl.structs.flex.mstudioflexrule_t¶
-
flex
(<parsed from long>¶int) the
mstudioflexdesc_tthis rule computes
-
opindex
(<parsed from long>¶int) offset to an array of
mstudioflexop_t
-
flex
- struct valvemdl.structs.flex.mstudioflexop_t¶
- struct valvemdl.structs.flex.mstudioflexcontrollerui_t¶
-
-
szindex0
(<parsed from long>¶int) offset to an
mstudioflexcontroller_t, NOT a string despite the name; the SDK reads all three with pointer arithmetic over this field, so they must stay adjacent
-
szindex1
(<parsed from long>¶int) offset to an
mstudioflexcontroller_t
-
szindex2
(<parsed from long>¶int) offset to an
mstudioflexcontroller_t
-
szindex0
- struct valvemdl.structs.flex.mstudioflexcontroller_t¶