MSH Mesh Format
A .msh file holds the geometry of one model part: a gear piece, a weapon, a map object. It has no textures inside. Textures, shaders and the number of sub-meshes come from a material script (token MTL\0, version 3000) that the client loads first. The file extension of the material script is not confirmed yet.
This page follows the client loader (ioMeshManager::LoadFileInternal in io3DEngine) and the web tools’ reader. Anything marked not confirmed yet was not verified.
Conventions
Section titled “Conventions”All 3D formats (.msh, .skl, .ani) share these rules:
- Little-endian, no compression.
- A file starts with a 4-byte token: three letters and a zero byte (
MSH\0). - A string is an
int32length and then that many raw bytes. There is no terminator. The bytes are CP949 (Korean) text, so keep names plain ASCII. float3is three 32-bit floats (x, y, z).
Header
Section titled “Header”| Offset | Type | Field |
|---|---|---|
| 0 | char[4] | MSH\0 |
| 4 | int32 | version: 2000, 2001 or 2002 |
| 8 | int32 | mesh type (list below) |
| 12 | uint32 | vertex component mask (list below) |
| 16 | float3 | bounding box minimum |
| 28 | float3 | bounding box maximum |
| 40 | float | bounding sphere radius |
| 44 | int32 | sub-mesh count |
Then, for each sub-mesh, 16 bytes: minIndex, vertexCount, indexStart, faceCount (four int32). After that comes int32 vertex count.
If the count of sub-meshes differs from the number of materials in the material script, the client writes a “Count Not Match” line to its log and reads only the sub-meshes it knows.
Versions
Section titled “Versions”| Version | Meaning |
|---|---|
| 2000 | Normal mesh |
| 2001 | Static mesh with vertex colours (token defined in the sources; the loader treats it like 2000) |
| 2002 | Like 2000, plus a list of control points after the faces |
Mesh types
Section titled “Mesh types”The values are the MeshType enum order in io3DCommon.h:
| Value | Name |
|---|---|
| 0 | Static |
| 1 | Animated (skinned to a skeleton) |
| 2 | Lightmap |
| 3 | Billboard |
| 4 | Normal billboard |
| 5 | Animate effect |
| 6 | Static with vertex colour |
Vertex blocks
Section titled “Vertex blocks”Each block below is present only when its bit is set in the mask. They follow each other in exactly this order, each one holding one entry per vertex:
| Order | Mask bit | Block | Size per vertex |
|---|---|---|---|
| 1 | 0x1 position |
float3 | 12 bytes |
| 2 | 0x20 normal |
float3 | 12 |
| 3 | 0x1000 tangent |
float3 | 12 |
| 4 | 0x2000 binormal |
float3 | 12 |
| 5 | 0x40 colour 0 |
uint32 (D3D colour) | 4 |
| 6 | 0x100 UV 0 |
float2 | 8 |
| 7 | 0x200 UV 1 (lightmap) |
float2 | 8 |
| 8 | 0x8 weights |
see below | 32 |
| 9 | 0x2 position 2 (billboard centre) |
float3 | 12 |
Other bits exist in the enum (0x4 position W, 0x10 indices, 0x80 colour 1, UV 2 and 3) but the loader reads nothing for them. 0x10 always goes together with 0x8 and has no block of its own.
Weights block
Section titled “Weights block”int32bone count, then that many strings: the bone names the vertices refer to.- Per vertex: four weights (float) and four bone indices (float, holding whole numbers). The indices point into the bone name list above, not into the skeleton.
The client stops at the first weight that is zero or less, so put the biggest weight first and zeros last. A vertex can use up to four bones.
The names must exist in the skeleton (.skl) that the model config names. See SKL Skeleton Format.
After the vertex blocks: int32 face count, then faceCount × 3 indices as unsigned 16-bit numbers (WORD). So a mesh cannot have more than 65 536 vertices. Each sub-mesh uses faceCount faces starting at indexStart.
The web tools’ reader also accepts 32-bit indices when the file size only fits that width. The 2014, 2019 and 2021 client loaders read 16 bits only (m_pFace is an array of WORD).
Control points (version 2002)
Section titled “Control points (version 2002)”If the version is 2002, the file continues after the faces with:
| Type | Field |
|---|---|
| int32 | control point count |
and for each control point:
| Type | Field |
|---|---|
| string | type and index, for example Type_Index |
| string | linked bone name |
| string | extra info |
| float3 | position |
The client uses control points as attach points (weapon grips, effect spots). A type and index pair must be unique inside the mesh. A repeat is dropped and logged.
Differences between the sources
Section titled “Differences between the sources”The mesh loader (ioMeshManager.cpp), ioFileTokenDefine.h and the format constants are byte-identical in the 2014, 2019 and 2021 sources. A mesh from one client loads in the others as far as the format goes. Material scripts and bone names may still differ per client.
| Tool | Use |
|---|---|
| Models | Reads .msh: textures, wireframe, bones, control points, bounding box |
| Heroes, Costumes | Read meshes for the hero and gear preview. Costumes builds a new .msh (version 2000, 16-bit faces, box and radius computed) from an import |
| Models (edit control points) | Writes the geometry bytes back unchanged and rewrites only the control point list. A version 2000 or 2001 file becomes 2002 |
Not confirmed yet
Section titled “Not confirmed yet”- The exact use of mesh types 2 (lightmap) and 5 (animate effect) in the shipped files.
- The material script (token
MTL\0, version 3000) is only mentioned here; its layout has no page yet. - Whether any shipped file uses version 2001.
