Mod3 Structure Notes - Ezekial711/MonsterHunterWorldModding GitHub Wiki

This section is the product of research by CrazyT, AsteriskAmpersand, PredatorCZ, Ubergrainy and Jodo.

As the template itself is pretty descriptive of structures involved this section only lists the bizarre parts of the specification that require clarification.

Vertex Structure

Within the mod3 each meshpart has a different blocktype which determines the binary structure of the vertices. In Monster Hunter World in particular there are 14 vertex buffer types. For the most part the Vertex Buffer Name is descriptive enough of the properties the type has on top of of the usual geometry header.

Vertex Types

Blocktype Hash Vertex Buffer Name Weight Type
a756f2f9 IANonSkin1UV None
a5104ca0 IANonSkin2UV None
c3ac03a1 IANonSkin3UVColor None
c9690ab8 IANonSkin4UVColor None
818904dc IANonSkin1UVColor None
f06033f IANonSkin2UVColor None
f637401c IASkin4wt1UV 3x10bit Weights + ComplementWeight
f471fe45 IASkin4wt2UV 3x10bit Weights + ComplementWeight
3c730760 IASkin4wt1UVColor 3x10bit Weights + ComplementWeight
b2fc0083 IASkin4wt2UVColor 3x10bit Weights + ComplementWeight
81f58067 IASkin8wt1UV 3x10bit Weights + 4x8bit Weights + ComplementWeight
83b33e3e IASkin8wt2UV 3x10bit Weights + 4x8bit Weights + ComplementWeight
366995a7 IASkin8wt1UVColor 3x10bit Weights + 4x8bit Weights + ComplementWeight
b8e69244 IASkin8wt2UVColor 3x10bit Weights + 4x8bit Weights + ComplementWeight

Component Types When present components occur in the order listed here.

Property C Type
Geometry
Position float[3]
Normal byte[4]
Tangent byte[4]
UV halffloat[2]
Weights
3x10bitWeights UInt32
4x8bitWeights UInt32
BoneIDs
BoneIDs(4 Weight) UByte[4]
BoneIDs(8 Weight) UByte[8]
Extra
Colour ubyte[4]

Weight Structure

While the weight is stored as a UInt32 according to the shader, the underlying structure is more complex. 3x10bit Weights are 3 10bit unsigned values that have to be normalized by dividing by 2^9-1. 4x8bit Weights are 4 8bit unsigned values that have to be normalized by dividing by 2^7-1. The "remaining" weight for n=4 and n=8 is 1-sum(weight[i] for i in [0,n-1)). This "weight" can be negative.

For each weight there's a corresponding bone id. A vertex can have repeated bone ids. In this cases it appears that the way animations operate is by adding the weights assigned to one bone but this is not confirmed through reverse engineering of code but simply model experimentation. Additionally negative weights animate as such and are used by some of the game models.

The astute reader might notice that there's 2 "unused" bits on the 3x10bit Weights. Of the game's 5002 models, 2 models have non-null bits on this 2 bits: a Xeno'Jiiva model that's not the monster's model but a stage asset and Zorah Magadaros Carapace which corresponds to map geometry.

This bizarre 2-bits and the repeated structure of weights means that importing it to a 3d modelling program (and exporting) results in loss of data unless "non-traditional" structures are used for the import/export process.

As a final coup de grace to the bizarre hellscape that weights represent in the mod3 format, the order in which the weights are listed within the vertex structure is relevant to animations for specific models (at the time only Geralt's model is known to suffer from this). This goes beyond the negative additional weight but also on the "normal" weights.

Vertex Base, Sub and Offset

For GPU rendering optimization purposes meshes are ordered by Vertex Buffer block size. As a result meshes with equal block size are lumped together and their vertex and face arrays can be processed with fixed size buffers applied to the biggest amount of entries. To account for this structure each meshpart header stores 3 different variables: Vertex Base, Sub and Offset. Vertex Base and Sub are vertex counts while offset is a binary offset. Vertex Sub is the sum of vertices of the same blocksize up to the current meshpart. If Vertex Sub plus the current meshpart vertex count would exceed 2^16 VertexSub is instead reset to 0 and VertexBase is added the previous VertexSub.

When there's a change of blocksize VertexBase and VertexSub are added up, multiplied by the previous blocksize and added to VertexOffset and Sub and Base are reset to 0.

Additionally Faces are relative to the start of the current VertexSub block. As a result to get a mapping of face indices to current meshpart indices it's necessary to substract the meshpart VertexSub from each face's vertex indices.

Bone Remap Table

Each bone on the armature has a fixed struct.

Property C Type
boneTableId short
parentId ubyte
symmetricPairID ubyte
unkn2 float
length float
position float[3]

boneTableId corresponds to a 1 to 1 mapping with the remap table entries. So if remap entry 22 is bone 15, bone 15 will have boneTableId 22. The remap table is the one that determines animations and the boneTableId appears to help with reference optimization but not be actually necessary for changes on the bone table to take effect.

The symmetric pair links one bone as the opposite to another through a symmetry plane (Right and Left hands for example).

Bones, LMatrices and AMatrices

After the section composed of bones follows the LMatrices and AMatrices. For every bone there's an LMatrix and an AMatrix (LMatrix from Local Matrix and AMatrix from Absolute Matrix). LMatrix are sequentially stored and then followed by AMatrices (after both of them comes the BoneMap).

In MTFramework bones are really "Nodes" which is roughly equivalent to tail oriented bones (instead of head oriented bones like those used by engineering software and blender). The LMatrix it the Matrix corresponding to the transformation relative to the parent. The AMatrix is the inverse of the Absolute Matrix of the bone.

In modelling software the correct structure results in a natural importing operation. Dummies in 3dsmax and Empties in blender have a transform and matrix_local property respectively which can be directly assigned the respective LMatrix.

To calculate the AMatrices Imagine a chain of bones that starts with p0 and ends in pn (p{j-1} is the parent of p{j})

amatrix[p{j}] =  lmatrix[p{j}].inverted() * amatrix[p{j-1}]

Yields the correct value (within floating point error).

To show this is the inverse of the absolute coordinates it suffices to know unparented bones have an AMatrix of Identity and derive as follows.

amatrix[p{j}] =  lmatrix[p{j}].inverted() * lmatrix[p{j-1}].inverted() * amatrix[p{j-2}]
amatrix[p{j}] =  lmatrix[p{j}].inverted() *...* lmatrix[p{j{0}].inverted() * amatrix[p{0}]
amatrix[p{0}] is always the identity
amatrix[p{j}] =  lmatrix[p{j}].inverted() *...* lmatrix[p{j{0}].inverted() 
amatrix[p{j}].inverted() = lmatrix[p{j{0}] *...* lmatrix[p{j}]

which means that the inverse of the absolute matrix is the product of all local matrices. In other words the inverse of the world matrix gives you the absolute position of the node.

UnknGroupStuff

Used by the game for controlling miscellaneous details. Nekotaga Yuhatora found out Nergigante projectile spikes are based on his model's UnknGroupStuff values, similar parameters might be contained on this section for different monsters.