Sources: src/public/studio.h (for the .vvd) and src/public/optimize.h (for
the .vtx), both in ValveSoftware/source-sdk-2013 and read on 13 November 2026. The
.mdl, .vvd and .vtx of one model carry the same checksum; the header
says the checksum has to match in the .phy too. Sizes are the sums of the declared field types.
The file names and extensions the tools give these files are not stated in the two headers.
The vertex file (.vvd)
vertexFileHeader_t, 64 bytes, little-endian. Offsets in it are from the start of the file.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 4 | char[4] | id | IDSV (MODEL_VERTEX_FILE_ID). IDCV (MODEL_VERTEX_FILE_THIN_ID) is used once the vertex data has been compressed in memory |
| 4 | 4 | int32 | version | 4 (MODEL_VERTEX_FILE_VERSION) |
| 8 | 4 | int32 | checksum | Same as in the .mdl, “ensures sync” |
| 12 | 4 | int32 | numLODs | Number of valid levels of detail |
| 16 | 32 | int32[8] | numLODVertexes | Number of vertices for each desired root LOD |
| 48 | 4 | int32 | numFixups | Number of vertexFileFixup_t records |
| 52 | 4 | int32 | fixupTableStart | Offset of the fixup table |
| 56 | 4 | int32 | vertexDataStart | Offset of the vertex block |
| 60 | 4 | int32 | tangentDataStart | Offset of the tangent block |
The header’s accessors return the vertices only when id is IDSV and
vertexDataStart is not 0, the tangents only when id is IDSV and
tangentDataStart is not 0. Tangents are not stored in the compressed form.
Vertex
mstudiovertex_t, exactly 48 bytes.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 12 | float[3] | m_BoneWeights.weight | Bone weights (MAX_NUM_BONES_PER_VERT is 3) |
| 12 | 3 | char[3] | m_BoneWeights.bone | Bone indexes (signed bytes) |
| 15 | 1 | byte | m_BoneWeights.numbones | Number of bones |
| 16 | 12 | Vector | m_vecPosition | Position |
| 28 | 12 | Vector | m_vecNormal | Normal |
| 40 | 8 | float[2] | m_vecTexCoord | Texture coordinate |
A tangent is a Vector4D, four floats (16 bytes). A model’s vertexindex and
tangentsindex are byte offsets into these arrays, and the header asserts that they are
multiples of 48 and 16.
Fixup record
vertexFileFixup_t, 12 bytes. The header comment says to apply the records sequentially to the
vertex and tangent pools, which are sorted by level of detail, to re-establish mesh order.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 4 | int32 | lod | Used to skip a culled root LOD |
| 4 | 4 | int32 | sourceVertexID | Absolute index from the start of the vertex and tangent blocks |
| 8 | 4 | int32 | numVertexes | Number of vertices |
The strip file (.vtx)
Defined in optimize.h, in the OptimizedModel namespace with
#pragma pack(1): there is no padding between fields. The version is
OPTIMIZED_MODEL_FILE_VERSION 7. Every offset in the records is a byte offset from the start
of the record that holds it. The file is a tree: file header, body parts, models, LODs, meshes, strip
groups, strips.
The header notes that MAX_NUM_BONES_PER_VERT (3, from studio.h) changes this
format if it is changed, while MAX_NUM_BONES_PER_TRI (that value times 3) and
MAX_NUM_BONES_PER_STRIP (512) do not.
File header
FileHeader_t, 36 bytes.
| Offset | Size | Type | Name | Meaning |
|---|---|---|---|---|
| 0 | 4 | int32 | version | 7 |
| 4 | 4 | int32 | vertCacheSize | Hardware parameter that affects how the model is optimised |
| 8 | 2 | uint16 | maxBonesPerStrip | Hardware parameter |
| 10 | 2 | uint16 | maxBonesPerTri | Hardware parameter |
| 12 | 4 | int32 | maxBonesPerVert | Hardware parameter |
| 16 | 4 | int32 | checkSum | Must match the checksum in the .mdl |
| 20 | 4 | int32 | numLODs | Number of LODs (also in each model header; they should match) |
| 24 | 4 | int32 | materialReplacementListOffset | Offset of one MaterialReplacementListHeader_t for each LOD |
| 28 | 4 | int32 | numBodyParts | Body parts |
| 32 | 4 | int32 | bodyPartOffset | Offset of the BodyPartHeader_t array |
Records
| Record | Size | Fields, in order |
|---|---|---|
BodyPartHeader_t | 8 | numModels, modelOffset (int each). One per body part; this maps one to one with the body parts of the .mdl |
ModelHeader_t | 8 | numLODs, lodOffset (int each). One per model of the .mdl; the LODs are those of the QC $lod command |
ModelLODHeader_t | 12 | numMeshes (int), meshOffset (int), switchPoint (float) |
MeshHeader_t | 9 | numStripGroups (int), stripGroupHeaderOffset (int), flags (byte). A mesh has one material |
StripGroupHeader_t | 25 | numVerts, vertOffset, numIndices, indexOffset, numStrips, stripOffset (int each), flags (byte). A strip group is a locking group with one vertex buffer and one index buffer; a mesh has up to four (non-flexed or flexed, hardware or software skinned) |
StripHeader_t | 27 | numIndices, indexOffset (into the mesh’s index array), numVerts, vertOffset (into the mesh’s vertex array) (int each), numBones (short; enables or disables skinning), flags (byte), numBoneStateChanges, boneStateChangeOffset (int each). A strip is a piece of a strip group divided by bones |
Vertex_t | 9 | boneWeightIndex[3] (bytes, index into the bone list of the mesh’s vertex), numBones (byte), origMeshVertID (uint16), boneID[3] (chars; global bone indexes for software skinning, hardware bone indexes for hardware skinning) |
| Index | 2 | An unsigned short, in the array at the strip group’s indexOffset |
BoneStateChangeHeader_t | 8 | hardwareID, newBoneID (int each) |
MaterialReplacementListHeader_t | 8 | numReplacements, replacementOffset (int each) |
MaterialReplacementHeader_t | 6 | materialID (short), replacementMaterialNameOffset (int, to a string, from the start of this record) |
Flags
| Where | Values |
|---|---|
Strip flags | 0x01 STRIP_IS_TRILIST; 0x02 STRIP_IS_TRISTRIP |
Strip group flags | 0x01 STRIPGROUP_IS_FLEXED; 0x02 STRIPGROUP_IS_HWSKINNED; 0x04 STRIPGROUP_IS_DELTA_FLEXED; 0x08 STRIPGROUP_SUPPRESS_HW_MORPH, a temporary flag used at run time |
Mesh flags | 0x01 MESH_IS_TEETH; 0x02 MESH_IS_EYES; both are material properties |