Textures

What the qc calls $texturegroup, and what everyone else calls materials.

An mdl does not contain textures, or even material definitions. Each mstudiotexture_t is just a name, and the engine finds the real .vmt by trying that name under each of the model’s material search paths in turn. Those paths are the cdtextures, and they are stored in the one genuinely awkward way the format has to offer.

cdtextures are a double indirection. studiohdr_t.cdtextureindex points at an array of numcdtextures ints, and each of those is itself an offset to a string. The SDK accessor is worth reading twice:

inline char *pCdtexture( int i ) const
    { return (((char *)this) + *((int *)(((byte *)this) + cdtextureindex) + i)); };

this is the studiohdr_t in both halves, so unlike almost every other index in the format, both hops are relative to the file base rather than to the containing structure – there is no intermediate struct to be relative to. localnodenameindex works the same way.

The skin table is a matrix. studiohdr_t.skinindex points at short[numskinfamilies][numskinref]. A mesh’s material field is not an index into the texture array directly: it is a column in this table, and the row is the skin the entity is using. numskinref is the row width and is usually, but not necessarily, the texture count. This is how a model shows red and blu variants of one mesh without duplicating geometry.

material and clientmaterial are runtime pointers, written into the mapped file image by the engine, and zero on disk. They are 4 bytes here because the on-disk layout is the 32 bit one; the unused array absorbs the difference on 64 bit so that the structure stays 64 bytes on both.

struct valvemdl.structs.texture.mstudiotexture_t
sznameindex(int)<parsed from long>

offset to a string; the material name, without a path or the .vmt suffix

flags(int)<parsed from long>
used(int)<parsed from long>
unused1(int)<parsed from long>
material(int)<parsed from long>

runtime pointer; zero on disk

clientmaterial(int)<parsed from long>

runtime pointer; zero on disk

unused(int[10])<parsed from long>