Source: src/public/studio.h in ValveSoftware/source-sdk-2013, version 48, read on
13 November 2026. In a Source model the .mdl does not hold vertices or triangles. It holds the
structure (body parts, models, meshes, materials and flexes) and points into the
.vvd and .vtx files. Sizes are the sums of the
declared field types for the 32-bit layout. Every …index is a byte offset from the start of the record
that holds it unless stated.
Textures and skins
Textures are material names, not pixels. mstudiotexture_t, 64 bytes, at
textureindex.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 4 | int32 | sznameindex | Offset of the material name |
| 4 | 4 | int32 | flags | Flags |
| 8 | 4 | int32 | used | Used |
| 12 | 4 | int32 | unused1 | Unused |
| 16, 20 | 4, 4 | pointer | material, clientmaterial | Runtime pointers |
| 24 | 40 | int32[10] | unused | Unused (eight ints in the 64-bit build, which has larger pointers) |
The search paths, at cdtextureindex, are numcdtextures offsets (from the file start)
to strings. The replaceable-texture table at skinindex holds numskinref ×
numskinfamilies shorts (pSkinref).
Body parts, models and meshes
A body part, mstudiobodyparts_t at bodypartindex, is 16 bytes:
sznameindex, nummodels, base, modelindex (from the start of
the body part).
Models
mstudiomodel_t, 148 bytes.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 64 | char[64] | name | Name |
| 64 | 4 | int32 | type | Type |
| 68 | 4 | float | boundingradius | Bounding radius |
| 72, 76 | 4, 4 | int32 | nummeshes, meshindex | mstudiomesh_t, 116 bytes |
| 80 | 4 | int32 | numvertices | Number of unique vertices, normals and texture coordinates |
| 84 | 4 | int32 | vertexindex | Byte offset into the .vvd vertex array (a multiple of 48) |
| 88 | 4 | int32 | tangentsindex | Byte offset into the tangent array (a multiple of 16) |
| 92, 96 | 4, 4 | int32 | numattachments, attachmentindex | Attachments |
| 100, 104 | 4, 4 | int32 | numeyeballs, eyeballindex | mstudioeyeball_t, 172 bytes |
| 108 | 8 | pointer × 2 | vertexdata | Runtime pointers to the vertex and tangent data |
| 116 | 32 | int32[8] | unused | Unused (six ints in the 64-bit build) |
Meshes
mstudiomesh_t, 116 bytes.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 4 | int32 | material | Material of the mesh (the header gives no further comment) |
| 4 | 4 | int32 | modelindex | Offset back to the model that holds the mesh |
| 8 | 4 | int32 | numvertices | Number of unique vertices, normals and texture coordinates |
| 12 | 4 | int32 | vertexoffset | Commented “vertex mstudiovertex_t”: the mesh’s offset into the vertex array |
| 16, 20 | 4, 4 | int32 | numflexes, flexindex | Vertex animation: mstudioflex_t, 60 bytes |
| 24 | 4 | int32 | materialtype | Special code for material operations |
| 28 | 4 | int32 | materialparam | Parameter of that code |
| 32 | 4 | int32 | meshid | Unique ordinal of the mesh |
| 36 | 12 | Vector | center | Centre |
| 48 | 4 | pointer | vertexdata.modelvertexdata | Runtime pointer |
| 52 | 32 | int32[8] | vertexdata.numLODVertexes | Expected number of mesh vertices at the desired LOD, used when culling top-level LODs |
| 84 | 32 | int32[8] | unused | Unused (six ints in the 64-bit build) |
Eyeballs and mouths
mstudioeyeball_t, 172 bytes.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 4 | int32 | sznameindex | Offset of the name |
| 4 | 4 | int32 | bone | Bone |
| 8 | 12 | Vector | org | Origin |
| 20, 24 | 4, 4 | float | zoffset, radius | |
| 28, 40 | 12, 12 | Vector | up, forward | |
| 52 | 4 | int32 | texture | Texture |
| 56 | 4 | int32 | unused1 | Unused |
| 60 | 4 | float | iris_scale | Iris scale |
| 64 | 4 | int32 | unused2 | Unused |
| 68, 80 | 12, 12 | int32[3] | upperflexdesc, lowerflexdesc | Flex descriptions of the raiser, neutral and lowerer that flex controllers set |
| 92, 104 | 12, 12 | float[3] | uppertarget, lowertarget | Angle in radians of the raised, neutral and lowered lid positions |
| 116, 120 | 4, 4 | int32 | upperlidflexdesc, lowerlidflexdesc | Flex descriptions that the real lid flexes follow |
| 124 | 16 | int32[4] | unused | Used before, so not guaranteed to be zero |
| 140 | 1 | bool | m_bNonFACS | Never used before version 44 |
| 141 | 3 | char[3] | unused3 | Unused |
| 144 | 28 | int32[7] | unused4 | Unused |
A mouth, mstudiomouth_t at mouthindex, is 20 bytes: bone (int),
forward (Vector) and flexdesc (int).
Flex controllers, rules and descriptions
A flex description, mstudioflexdesc_t at flexdescindex, is 4 bytes:
szFACSindex, the offset of its FACS name. A flex controller,
mstudioflexcontroller_t at flexcontrollerindex, is 20 bytes:
sztypeindex, sznameindex, localToGlobal (remapped at load time to a
master list), min and max (floats).
Flex controller UI
mstudioflexcontrollerui_t, 20 bytes, at flexcontrolleruiindex:
sznameindex, then szindex0, szindex1, szindex2, offsets
of mstudioflexcontroller_t records used like a union (simple: control, unused, unused;
stereo: left, right, unused; N-way: control, unused, value; N-way stereo: left, right, value), then
remaptype (byte), stereo (bool) and two unused bytes. Remap types:
0 pass-through, 1 two-way (control 0 ramps 1 to 0 over 0 to 0.5, control 1 ramps 0 to 1 over 0.5 to 1),
2 N-way (each control ramps 0, 1, 0 across its step, with a second control giving the amount),
3 eyelid.
Flex rules
mstudioflexrule_t at flexruleindex, 12 bytes: flex,
numops, opindex (from the start of the rule). Each operation,
mstudioflexop_t, is 8 bytes: op (int) and a 4-byte union, an int
index or a float value.
| op | Name | Meaning |
|---|---|---|
| 1 | STUDIO_CONST | Push a float |
| 2 | STUDIO_FETCH1 | Push a flex controller value |
| 3 | STUDIO_FETCH2 | Push a flex weight |
| 4, 5, 6, 7 | STUDIO_ADD, SUB, MUL, DIV | Arithmetic |
| 8, 9 | STUDIO_NEG, EXP | Not implemented |
| 10, 11, 12 | STUDIO_OPEN, CLOSE, COMMA | Used only in token parsing |
| 13, 14 | STUDIO_MAX, MIN | Maximum and minimum |
| 15 | STUDIO_2WAY_0 | First value of a two-way slider, RemapVal(0, 0.5, 0, 1) |
| 16 | STUDIO_2WAY_1 | Second value of a two-way slider, RemapVal(0.5, 1, 0, 1) |
| 17 | STUDIO_NWAY | Value from an N-way slider |
| 18 | STUDIO_COMBO | Multiply the last N values on the stack |
| 19 | STUDIO_DOMINATE | A combination domination operation |
| 20, 21 | STUDIO_DME_LOWER_EYELID, STUDIO_DME_UPPER_EYELID | Eyelid operations |
Vertex animation
A mesh’s flexes, mstudioflex_t, 60 bytes, give the vertex movement for one flex.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 4 | int32 | flexdesc | Input value (flex description) |
| 4, 8, 12, 16 | 4 each | float | target0, target1, target2, target3 | The four target points; the header comments them zero, one, one, zero |
| 20 | 4 | int32 | numverts | Number of vertex records |
| 24 | 4 | int32 | vertindex | Offset of the vertex records (from the flex) |
| 28 | 4 | int32 | flexpair | Second flex description |
| 32 | 1 | byte | vertanimtype | 0 normal (STUDIO_VERT_ANIM_NORMAL), 1 wrinkle (STUDIO_VERT_ANIM_WRINKLE) |
| 33 | 3 | byte[3] | unusedchar | Unused |
| 36 | 24 | int32[6] | unused | Unused |
Each vertex record, mstudiovertanim_t, is 16 bytes. A wrinkle record,
mstudiovertanim_wrinkle_t, adds a short, so it is 18 bytes.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 2 | uint16 | index | Vertex index |
| 2 | 1 | byte | speed | 255 / the maximum length in the flex |
| 3 | 1 | byte | side | 255 / left–right |
| 4 | 6 | int16[3] or float16[3] | delta | Position delta. 16-bit fixed point in memory, multiplied by the fixed-point scale (flVertAnimFixedPointScale in the header, or 1/4096 if the flag is not set); the header notes a legacy format written as 16-bit floats |
| 10 | 6 | int16[3] or float16[3] | ndelta | Normal delta, in the same form |
| 16 | 2 | int16 | wrinkledelta | Wrinkle records only; scaled the same way |
How these deltas are blended with the flex weights is not in the header; see Not described.