-
Notifications
You must be signed in to change notification settings - Fork 0
MDL: Half Life 1 file format
The Half-Life 1 MDL file format.
- Each position/rotation is in local space, except for the model's eye position, which is in model space.
- The Half-Life engine uses a left handed coordinate system, where X is forward, Y is left and Z is up.
- Rotations are represented as euler angles. The angles are such that pitch is the rotation around the Y axis, yaw is the rotation around the Z axis and roll is the rotation around the X axis.
- The type vec3_t is an alias for a float[3] array. It is used to specify 3D positions or vectors.
This is the header used when loading any MDL file. It provides global information about the model, as well as the offset to every other chunk.
The header has a size of 244 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
4 | int | id | The magic word. See below. |
4 | int | version | The file format version. See below. |
64 | char[64] | name | Model name. |
4 | int | length | The file size in bytes. |
12 | vec3_t | eyeposition | The model's eye position. |
12 | vec3_t | min | Hull min extent. |
12 | vec3_t | max | Hull max extent. |
12 | vec3_t | bbmin | Clipping box min extent. |
12 | vec3_t | bbmax | Clipping box max extent. |
4 | int | flags | ? |
4 | int | numbones | The number of bones. |
4 | int | boneindex | The offset of the first bone chunk. |
4 | int | numbonecontrollers | The number of bone controllers. |
4 | int | bonecontrollerindex | The offset of the first bone controller chunk. |
4 | int | numhitboxes | The number of hitboxes. |
4 | int | hitboxindex | The offset of the first hitbox chunk. |
4 | int | numseq | The number of sequences. |
4 | int | seqindex | The offset of the first sequence chunk. |
4 | int | numseqgroups | The number of sequence groups. |
4 | int | seqgroupindex | The offset of the first sequence group chunk. |
4 | int | numtextures | The number of textures. See below. |
4 | int | textureindex | The offset of the first texture chunk. |
4 | int | texturedataindex | The offset of the pixel data of the first texture chunk. |
4 | int | numskinref | The number of replacable textures. |
4 | int | numskinfamilies | The number of skin families. |
4 | int | skinindex | The offset of the first skin value. |
4 | int | numbodyparts | The number of bodyparts. |
4 | int | bodypartindex | The offset of the first bodypart chunk. |
4 | int | numattachments | The number of attachments. |
4 | int | attachmentindex | The offset of the first attachment chunk. |
4 | int | soundtable | ? |
4 | int | soundindex | ? |
4 | int | soundgroups | ? |
4 | int | soundgroupindex | ? |
4 | int | numtransitions | The number of nodes in the sequence transition graph. |
4 | int | transitionindex | The offset of the first transition value of the sequence transition graph. |
id
should always be IDST for the main MDL file and external texture files.
version
should always be 10. This is the version found in the official Half-Life SDK.
numtextures
is the number of textures stored in the actual MDL file. If a model uses an external texture file, then this number will be 0 when reading from the main MDL file.
Note:
- Sequence group files use a separate header for loading, but they can also be loaded with the following header.
Sequence group files use a separate header, but a regular header may also be used.
The Sequence header a size of 76 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
4 | int | id | The magic word. See below. |
4 | int | version | The file format version. See below. |
64 | char[64] | name | Model name. |
4 | int | length | The file size in bytes. |
id
magic word should always be IDSQ for sequence group files.
version
should always be 10. This is the version found in the official Half-Life SDK.
Textures are stored as 8-bit color bitmaps. They are either stored in the main MDL file, or in an external MDL.
Each texture chunk has a size of 80 bytes. This does not include the size of the pixel data.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
64 | char[64] | name | The texture name. |
4 | int | flags | One or more texture flags. |
4 | int | width | Width in pixel. |
4 | int | height | Height in pixel. |
4 | int | index | The offset of the pixel data. |
A skin is a replacable texture. Skin information is stored as a matrix M x N where M is the number of skin families and N is the number of replacable textures. The number of replacable textures is the number of textures that can only be applied to a model at once.
Example
Given a model with 2 skin families and 6 replacable textures. We can see that the default skin uses textures 0 to 5. However, in the second skin family, texture 6 replaces the second texture, textures 7 replaces "before last" texture and texture 8 replaces last texture.
// First skin family.
[0][0] => 0
[0][1] => 1
[0][2] => 2
[0][3] => 3
[0][4] => 4
[0][5] => 5
// Second skin family.
[1][0] => 0
[1][1] => 6
[1][2] => 2
[1][3] => 3
[1][4] => 7
[1][5] => 8
The bones are stored starting from the root node.
Each bone chunk has a size of 112 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
32 | char[32] | name | The name of the bone. |
4 | int | parent | The bone's parent index. |
4 | int | flags | Unused. |
24 | int[6] | bonecontroller | See below. |
24 | float[6] | value | See below. |
24 | float[6] | scale | The scale value. |
bonecontroller
, value
, scale
all have a fixed size of 6 elements, or 6 slots. The 6 elements slots are related to the first 6 motion types.
Slot[0] <-> STUDIO_X // pos x
Slot[1] <-> STUDIO_Y // pos y
Slot[2] <-> STUDIO_Z // pos z
Slot[3] <-> STUDIO_XR // rot x
Slot[4] <-> STUDIO_YR // rot y
Slot[5] <-> STUDIO_ZR // rot z
For each slot, bonecontroller
stores the index of the bone controller, with respect to the motion bone used by this bone controller. If no bone controller is associated to the slot, then the value is -1.
Example
bonecontroller[0] => -1
bonecontroller[1] => bone controller at [1] has motion type STUDIO_Y.
bonecontroller[2] => -1
bonecontroller[3] => -1
bonecontroller[4] => -1
bonecontroller[5] => bone controller at [5] has motion type STUDIO_ZR.
The first 3 slots of value
make up the bone position and the last 3 slots make up the bone rotations.
The values in scale
are used to scale the bone position and rotation, when reading a compressed animation value data.
Bone controllers are used in the game to change the position or the rotation of one or more bones at run time.
Each bone controller chunk has a size of 24 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
4 | int | bone | The bone index. |
4 | int | type | The motion type. Can be used in conjunction with STUDIO_RLOOP. |
4 | float | start | The minimum value. |
4 | float | end | The maximum value. |
4 | int | rest | The value at rest. |
4 | int | index | See below. |
index
is the channel of the bone controller. The following example shows two bone controllers declared in a SMD. The index
value for the first bone controller would be 0 and the second bone controller's index
value would be 4.
$controller 0 "Head bone" XR -60.000000 60.000000
$controller 4 "Mouth bone" ZR 0.000000 45.000000
Each hitbox chunk has a size of 32 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
4 | int | bone | The bone index. |
4 | int | group | The hit group. |
12 | vec3_t | bbmin | The hitbox min extent. |
12 | vec3_t | bbmax | The hitbox max extent. |
Goldsrc allows animation data to be stored in separate files and load them on demand at run time. These files are called sequence group files.
Each sequence group data has a size of 104 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
32 | char[32] | label | The sequence group name. |
64 | char[64] | name | The sequence group file name. |
4 | int | unused1 | Unused. |
4 | int | unused2 | Unused. |
Note:
- The sequence group file must be in the same directory as the main MDL file.
- The file name is always prepended with "models/".
Each sequence description chunk has a size of 176 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
32 | char[32] | label | The sequence name. |
4 | float | fps | Frames per second. |
4 | int | flags | One or more sequence flags. |
4 | int | activity | The activity. |
4 | int | actweight | The activity weight. |
4 | int | numevents | The number of animation events. |
4 | int | eventindex | The offset of the first animation event chunk. |
4 | int | numframes | The number of frames. |
4 | int | numpivots | Unused. |
4 | int | pivotindex | Unused. |
4 | int | motiontype | The motion type. |
4 | int | motionbone | The motion bone index. |
12 | vec3_t | linearmovement | The motion bone's displacement from the first frame to the last frame of this sequence. |
4 | int | automoveposindex | Unused. |
4 | int | automoveangleindex | Unused. |
12 | vec3_t | bbmin | The sequence bounds min extent. |
12 | vec3_t | bbmax | The sequence bounds min extent. |
4 | int | numblends | The number of blend controllers. |
4 | int | animindex | The offset of the first animation frame offsets chunk. |
8 | int[2] | blendtype | The motion type. One per blend controller. |
8 | float[2] | blendstart | The start value. One per blend controller. |
8 | float[2] | blendend | The end value. One per blend controller. |
4 | int | blendparent | Unused. |
4 | int | seqgroup | The sequence group index. |
4 | int | entrynode | The entry value in the sequence transition graph. |
4 | int | exitnode | The exit value in the sequence transition graph. |
4 | int | nodeflags | One or more node flags. |
4 | int | nextseq | Unused. |
actweight
is the activity weight. Many sequences may use the same activity, but the game will pick the sequence that has the highest actweight
.
The nodeflags
is either 0 or 1, where 0 means that the transition to this sequence can only be made from entrynode
to exitnode
and 1 means the sequence transition can also be made in the reverse order, that is exitnode
to entrynode
.
An animation is used to tell how bones move. A sequence is used to group one or more animations coherently. Using more than 2 animations per sequences allows animation blending.
Blending refers to interpolating the bone positions and rotations from multiple animations. Goldsrc supports up to 4 blend animations, but you must chose either 2 or 4 animations for your blending. The reason is because of how the game interpolates between the animations. Blend controllers are used to determine the influence of each animation.
The blend controllers specify the motion type using blendtype[2]
, as well as the range of each blend using blendstart[2]
and blendend[2]
. A sequence that has 2 animations will only need 1 blend controller, while a sequence that has 4 animations will use 2 blend controllers.
Here is an example that explains the influence of each blend controller.
Let A1, A2, A3, A4 be the 4 animations of the sequence, and s, t be the blend factor for the first and second blend controllers. Both s and t are between 0 and 1, inclusively. R is the animation resulting from the blending.
// Blend between the first and second animation using the first blend factor.
A = A1 * (1 - s) + A2 * s
// Blend between the third and fourth animation using the first blend factor.
B = A3 * (1 - s) + A4 * s
// Blend between A and B using the second blend factor.
R = A * (1 - t) + B * t
It is important to understand that the purpose of this example is to help understand the way blend controllers influence the resulting pose of the model. You cannot directly interpolate a bone's transformation matrix with another. Orientations need to use Quaternion slerp to interpolate correctly.
Each sequence group file contains multiple chunks of this structure. The sequence property seqgroup
tells which sequence group file contains the animation data of each animation in the sequence. For a given animation, there are as many chunks as there are bones. If a sequence contains more than one animation, then the next animation data can be accessed after the last chunk of the first animation .i.e they are stored contiguous. Each chunk stores 6 offsets (1 per motion type), to the first animation frame chunk of the current bone. The layout is as follows:
// Blend 1 (First animation)
Bone 0 chunk
Bone 1 chunk
Bone 2 chunk
...
Bone N chunk
// Blend 2 (Second animation)
Bone 0 chunk
Bone 1 chunk
Bone 2 chunk
...
Bone N chunk
// Blend 3 (Third animation)
...
// Blend 4 (Fourth animation)
...
Each anim chunk has a size of 12 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
12 | unsigned short[6] | offset | The offset to the first animation frame chunk. One per element of the array. See below. |
The offset
has a fixed size of 6 elements, or 6 slots. The 6 elements slots are related to the first 6 motion types. Each value represents an offset to an animation frame chunk. If no animation frame is available for an offset, then the value is 0.
StudioMDL reduces animations by removing values of positions and rotations that are consecutive and identical.
For more info, please refer to the following links.
- Compression algorithm
- Reading a compressed animation rotation value
- Reading a compressed animation position value
Each animation frame chunk has a maximum size of 2 bytes.
typedef union
{
struct {
byte valid;
byte total;
} num;
short value;
} mstudioanimvalue_t;
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
1 | uint8 | valid | ? |
1 | uint8 | total | ? |
2 | short | value | See below. |
The value
is one of the component X Y Z of either position or rotation at this frame.
The sequence transition graph stores transitions between sequences. It is implemented similarly to an adjacency matrix. Each sequence contains an entrynode
and an exitnode
. The graph maps each entry node to the exit node, and each exit node to each entry node. The graph contains numtransitions * numtransitions
entries where numtransitions
is the parameter from header and represents the number of nodes.
Important: Entry node and exit nodes numbering starts at 1. You must subtract 1 from them to use them as matrix indices.
Given a sequence A, let A(a), A(b) be the entry and exit nodes from sequence A.
If no transition exists for a given relation, then the value at [A(a) - 1][A(b) - 1]
is 0.
There are 2 types of transitions. Direct transitions and multistage transitions.
-
Direct transition means that there is a path to go from the entry node to the exit node directly .e.g
[A(a) - 1][A(b) - 1]
isA(b)
. -
Multistage transition means that it is not possible to go from
A(a)
toA(b)
directly, but there is a transitive relation such thatA(a) => A(x) => A(b)
whereA(x)
is an intermediate node and is equal to[A(a) - 1][A(b) - 1]
. Therefore, the position of the intermediate node in the graph can be obtained via:[A(x) - 1][A(b) - 1]
.
Additionally, if the sequence has the nodeflags
set to 1, then there is a path from A(b)
to A(a)
. This means that [A(b) - 1][A(a) - 1]
is A(a)
.
Sequences may store events, which will be fired at a specific frame in the game at run time .e.g Playing a step sound each time the model's foot touches the ground.
Each animation event chunk has a size of 76 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
4 | int | frame | The frame at which this event occurs. |
4 | int | event | The script event. |
4 | int | type | Unused. |
64 | char[64] | options | Additional data. |
Attachments are a position of interest on the model that can be retrieved at runtime in the game .e.g The gun tip from where to draw the muzzle flash. Each attachment will move accordingly to the bone it is parented to.
Each attachment chunk has a size of 88 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
32 | char[32] | name | Unused. |
4 | int | type | Unused. |
4 | int | bone | The bone index. |
12 | vec3_t | org | The attachment position. |
36 | vec3_t[3] | vectors | Unused. |
Bodyparts allow to group models coherently. For example, you could have multiple head models for a single bodypart named "heads" and change the head model in code at runtime.
Each bodypart chunk has a size of 76 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
64 | char[64] | name | The name of the bodypart. |
4 | int | nummodels | The number of Models. |
4 | int | base | See below. |
4 | int | modelindex | The offset of the first model chunk. |
base
is the divisor used to calculate the index of a model relative to it's bodypart. In short, it is the process of converting a global model index to a local model index. Please refer to the code for an example.
A model contains vertices, normals, and an index to the first mesh of this model.
Each model chunk has a size of 112 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
64 | char[64] | name | The name of the model. This is also the name of the SMD file. |
4 | int | type | Unused. |
4 | float | boundingradius | Unused. |
4 | int | nummesh | The number of meshes. |
4 | int | meshindex | The offset of the first mesh chunk. |
4 | int | numverts | The number of vertex positions. |
4 | int | vertinfoindex | The offset to the first vertexinfoindex value. |
4 | int | vertindex | The offset to the first vertex position. |
4 | int | numnorms | The number of vertex normals. |
4 | int | norminfoindex | The offset to the first norminfoindex value. |
4 | int | normindex | The offset to the first vertex normal. |
4 | int | numgroups | Unused. |
4 | int | groupindex | Unused. |
-
vertexinfoindex
is the offset to an array of int. This array is the same size asnumverts
and maps each vertex position to a bone index. This bone index is used to tell which bone affects this vertex position. -
norminfoindex
is the offset to an array of int. This array is the same size asnumnorms
and maps each vertex normal to a bone index. This bone index is used to tell which bone affects this vertex normal.
Note:
- The
name
of the model is actually the name of the SMD file that was used for this model. - Goldsrc does not support multiple bone weights per vertex, so each vertex is assigned to exactly one bone.
Half-Life models store mesh triangles as strips or fans. Each mesh chunk specify a list of triverts. This list is divided in sequences and each new sequence means a different triangle type. Each sequence starts with the number of triverts N used in the sequence. If N is positive, this means that the sequence is a triangle strip, otherwise it is a triangle fan.
// Triangle strip
N <Trivert> <Trivert> <Trivert> ...
// Triangle fan
-N <Trivert> <Trivert> <Trivert> ...
...
Also, each mesh chunk stores the texture index to use for the mesh.
Each mesh chunk has a size of 20 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
4 | int | numtris | The number of triverts. |
4 | int | triindex | The offset of the first trivert. |
4 | int | skinref | The texture index. |
4 | int | numnorms | Unused. |
4 | int | normindex | Unused. |
Triverts are used to specify which position, normal, texture coordinates to use for a single vertex.
Each trivert chunk has a size of 8 bytes.
Size (Bytes) | Data Type | Name | Description |
---|---|---|---|
2 | short | vertindex | The index of a vec3_t position. See below. |
2 | short | normindex | The index of a vec3_t normal. See below. |
2 | short | s | The horizontal texture coordinate. See below. |
2 | short | t | The vertical texture coordinate. See below. |
vertindex
is the index of a vec3_t position from the model vertex position array. See the model parameter named vertindex
.
normindex
is the index of a vec3_t normal from the model vertex normal array. See the model parameter named normindex
.
The s
and t
coordinates are absolute, which means they are not normalized/unit coordinates. To get the normalized coordinate, you must divide both s
and t
by width and height of the texture of the mesh this trivert is associated with.
Note:
- Two or more triverts can have the same
vertindex
, but not the samenormindex
,s
,t
.