Bodyparts, models and meshes

The geometry spine: studiohdr_t.bodypartindex reaches an array of mstudiobodyparts_t, each of which owns mstudiomodel_ts, each of which owns mstudiomesh_tes. It is a three level tree, and none of it contains a single vertex position or triangle index – those live in the Vvd and Vtx respectively. What this tree describes is the shape of the model: which pieces exist, which are swappable, and which material each piece wants.

A bodypart is a set of alternatives, not a part. Despite the name, a bodypart holds nummodels mutually exclusive options – a head bodypart might hold “head”, “head with hat” and “nothing”. The engine picks one per bodypart using a single packed integer, the bodygroup, decoded with base:

(bodynum / base) % nummodels

base is the stride studiomdl assigned this bodypart when it packed them all into that one int. A model with no bodyparts at all is not malformed – it is an animation-only model, and there are plenty in Team Fortress 2.

A model is not the model. mstudiomodel_t is one alternative inside one bodypart. Its name is stored inline as a fixed 64 byte buffer, like studiohdr_t.name and unlike almost every other string in the format.

vertexindex and tangentsindex are byte offsets, not counts. They point into the Vvd’s vertex and tangent pools. The SDK gives the game away in GetGlobalVertexIndex, which divides before using it:

i + ( modelptr->vertexindex / sizeof( mstudiovertex_t ) )

So the vertex a model starts at is vertexindex / 48. Treating the field as a vertex count is the classic way to read a model that looks almost right.

A mesh’s vertexoffset, on the other hand, is a count – vertices from the start of its own model, not bytes. Two adjacent fields, two different units.

modelindex points backwards. mstudiomesh_t carries a negative offset back to the model that owns it, so it is signed and it resolves to something earlier in the file.

material is not a texture index. It is a column into the skin table; see mstudiotexture_t.

vertexdata is half runtime junk and half real data. modelvertexdata is a pointer the engine fills in, zero on disk. numLODVertexes immediately after it is genuine: it is how many vertices this mesh keeps at each level of detail, and Studio_SetRootLOD uses it to cull. So the structure has to be read, but only half of it believed.

struct valvemdl.structs.mesh.mstudiomesh_t
material(int)<parsed from long>

a column into the skin table, not an index into the texture array

modelindex(int)<parsed from long>

NEGATIVE offset back to the mstudiomodel_t that owns this mesh

numvertices(int)<parsed from long>

unique vertices in this mesh

vertexoffset(int)<parsed from long>

where this mesh starts within its model, in VERTICES; unlike mstudiomodel_t.vertexindex, which is bytes

numflexes(int)<parsed from long>
flexindex(int)<parsed from long>

offset to an array of mstudioflex_t

materialtype(int)<parsed from long>
materialparam(int)<parsed from long>
meshid(int)<parsed from long>

a unique ID for this mesh within the model

center(Vector)
vertexdata(mstudio_meshvertexdata_t)
unused(int[8])<parsed from long>
struct valvemdl.structs.mesh.mstudiomodel_t
name(string)<parsed from a 64 bytes long padded string>

stored inline as a fixed 64 byte buffer, not as an offset

type(int)<parsed from long>
boundingradius(float)<parsed from float>
nummeshes(int)<parsed from long>
meshindex(int)<parsed from long>

offset to an array of mstudiomesh_t

numvertices(int)<parsed from long>

number of unique vertices, in vertices

vertexindex(int)<parsed from long>

offset into the vvd vertex pool, in BYTES; divide by sizeof(mstudiovertex_t) for a vertex index

tangentsindex(int)<parsed from long>

offset into the vvd tangent pool, in BYTES

numattachments(int)<parsed from long>
attachmentindex(int)<parsed from long>
numeyeballs(int)<parsed from long>
eyeballindex(int)<parsed from long>

offset to an array of mstudioeyeball_t

vertexdata(mstudio_modelvertexdata_t)
unused(int[8])<parsed from long>
struct valvemdl.structs.mesh.mstudio_meshvertexdata_t
modelvertexdata(int)<parsed from long>

runtime pointer; zero on disk

numLODVertexes(int[8])<parsed from long>

vertices kept at each lod; real data, unlike the pointer above

struct valvemdl.structs.mesh.mstudio_modelvertexdata_t
pVertexData(int)<parsed from long>

runtime pointer; zero on disk

pTangentData(int)<parsed from long>

runtime pointer; zero on disk

struct valvemdl.structs.mesh.mstudiobodyparts_t
sznameindex(int)<parsed from long>

offset to a string

nummodels(int)<parsed from long>

how many alternatives this bodypart offers

base(int)<parsed from long>

the stride used to decode this bodypart out of a packed bodygroup int

modelindex(int)<parsed from long>

offset to an array of mstudiomodel_t