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_t is a target: a named shape the mesh can be deformed into, like AU1 or rightBrowRaiser. It is one string and nothing else.

  • mstudioflexcontroller_t is an input: a slider the game sets, with a range. Controllers and targets are not one to one.

  • mstudioflexrule_t and mstudioflexop_t are the wiring between them: a small stack machine, one rule per target, computing that target’s weight from the controllers.

  • mstudioflexcontrollerui_t is 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.flexindex reaches mstudioflex_t, which names its target and carries the per-vertex deltas.

The rules are reverse polish. Each mstudioflexop_t is an opcode and a four byte operand that is either an int or a float depending on the opcode – STUDIO_CONST pushes d.value as a float, STUDIO_FETCH1 reads controller d.index, and STUDIO_ADD and 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 with stereo and remaptype, and despite the sz prefix they are not string offsets – they point at mstudioflexcontroller_t.

mstudiovertanim_t has two discriminators, and they are unrelated to each other.

  • Which union member is live – short delta[3] or float16 flDelta[3] – is decided by STUDIOHDR_FLAGS_FLEXES_CONVERTED on 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 selects mstudiovertanim_wrinkle_t. The SDK spells this out in VertAnimSizeBytes().

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.flVertAnimFixedPointScale if STUDIOHDR_FLAGS_VERT_ANIM_FIXED_POINT_SCALE is set and 1/4096 otherwise. Exactly one model in Team Fortress 2 sets that flag.

struct valvemdl.structs.flex.mstudioeyeball_t
sznameindex(int)<parsed from long>

offset to a string

bone(int)<parsed from long>
org(Vector)
zoffset(float)<parsed from float>
radius(float)<parsed from float>
up(Vector)
forward(Vector)
texture(int)<parsed from long>
unused1(int)<parsed from long>
iris_scale(float)<parsed from float>
unused2(int)<parsed from long>
upperflexdesc(int[3])<parsed from long>

raiser, neutral, lowerer

lowerflexdesc(int[3])<parsed from long>
uppertarget(float[3])<parsed from float>

the lid angle each flexdesc corresponds to

lowertarget(float[3])<parsed from float>
upperlidflexdesc(int)<parsed from long>

the flexdesc holding the lid geometry

lowerlidflexdesc(int)<parsed from long>
unused(int[4])<parsed from long>

the SDK warns these were used before, so they are not guaranteed to be zero

m_bNonFACS(int)<parsed from unsigned char>

never used before version 44

unused3(int[3])<parsed from unsigned char>
unused4(int[7])<parsed from long>
struct valvemdl.structs.flex.mstudiovertanim_wrinkle_t
index(int)<parsed from unsigned short>
speed(int)<parsed from unsigned char>
side(int)<parsed from unsigned char>
delta(int[3])<parsed from short>
ndelta(int[3])<parsed from short>
wrinkledelta(int)<parsed from short>
struct valvemdl.structs.flex.mstudiovertanim_t
index(int)<parsed from unsigned short>

which vertex of the mesh this moves

speed(int)<parsed from unsigned char>

255/max_length_in_flex

side(int)<parsed from unsigned char>

255/left_right

delta(int[3])<parsed from short>

a union of short[3] and float16[3]; on disk it is the float16 member, since STUDIOHDR_FLAGS_FLEXES_CONVERTED is never set

ndelta(int[3])<parsed from short>

the normal delta; the same union

struct valvemdl.structs.flex.mstudioflex_t
flexdesc(int)<parsed from long>

the mstudioflexdesc_t this deformation targets

target0(float)<parsed from float>

zero

target1(float)<parsed from float>

one

target2(float)<parsed from float>

one

target3(float)<parsed from float>

zero

numverts(int)<parsed from long>
vertindex(int)<parsed from long>

offset to an array of mstudiovertanim_t, or of mstudiovertanim_wrinkle_t; vertanimtype decides which

flexpair(int)<parsed from long>

the second flexdesc, for a stereo pair

vertanimtype(int)<parsed from unsigned char>

StudioVertAnimType_t; 0 is normal (16 bytes each), 1 is wrinkle (18)

unusedchar(int[3])<parsed from unsigned char>
unused(int[6])<parsed from long>
struct valvemdl.structs.flex.mstudioflexrule_t
flex(int)<parsed from long>

the mstudioflexdesc_t this rule computes

numops(int)<parsed from long>
opindex(int)<parsed from long>

offset to an array of mstudioflexop_t

struct valvemdl.structs.flex.mstudioflexop_t
op(int)<parsed from long>

one of the STUDIO_CONST/FETCH1/ADD/… opcodes

d(int)<parsed from long>

a union: an int index for most opcodes, a float value for STUDIO_CONST

struct valvemdl.structs.flex.mstudioflexcontrollerui_t
sznameindex(int)<parsed from long>

offset to a string

szindex0(int)<parsed from long>

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(int)<parsed from long>

offset to an mstudioflexcontroller_t

szindex2(int)<parsed from long>

offset to an mstudioflexcontroller_t

remaptype(int)<parsed from unsigned char>

one of the FlexControllerRemapType_t values

stereo(int)<parsed from unsigned char>

whether the controller is split left/right

unused(int[2])<parsed from unsigned char>
struct valvemdl.structs.flex.mstudioflexcontroller_t
sztypeindex(int)<parsed from long>

offset to a string

sznameindex(int)<parsed from long>

offset to a string

localToGlobal(int)<parsed from long>

runtime scratch; resolved at load

min(float)<parsed from float>
max(float)<parsed from float>
struct valvemdl.structs.flex.mstudioflexdesc_t
szFACSindex(int)<parsed from long>

offset to a string; the FACS name of the target