# MDL Files These seem to be simple model files containing the required information to render 3D objects. They're also used in some 2D backgrounds (like 2879436369) for bones. **NOTE**: This documentation page is not completed yet as there's still work left to do on the reverse engineering of this file format. For now just a structure, describing the file format is here ``` // // 010 editor template // // typedef struct { float x ; float y ; float z ; } VECTOR3; typedef struct { float x ; float y ; float z ; float w ; } VECTOR4; typedef struct { float x ; float y ; } VECTOR2; typedef struct { DWORD x ; DWORD y ; DWORD z ; DWORD w ; } BLENDINDICES; typedef struct { VECTOR3 vertex ; BLENDINDICES blendindices; VECTOR4 blendweight; VECTOR2 uv ; } VERTEX; typedef struct { WORD x; WORD y; WORD z; } VERTEXINDICE; typedef struct { CHAR header[]; DWORD first; DWORD second; DWORD third; CHAR json[]; DWORD fourth; DWORD fifth; DWORD vertexByteLength; VERTEX vertices[vertexByteLength / sizeof(VERTEX)]; DWORD indicesByteLength; VERTEXINDICE indices[indicesByteLength / sizeof (VERTEXINDICE)] ; } MDLVHEADER; typedef struct { BYTE tmp; DWORD type; DWORD unk1; } BONEENTRYHEADER; typedef struct { BONEENTRYHEADER header; DWORD entryByteLength; float v[entryByteLength / sizeof (float)]; CHAR info[]; } BONEENTRY; typedef struct { BONEENTRYHEADER header; } BONE2ENTRY; typedef struct { CHAR header[]; DWORD mightBeByteLength; DWORD numberOfBones; BONEENTRY bones[numberOfBones]; BONE2ENTRY unk[numberOfBones]; } MDLSHEADER; typedef struct { CHAR header[]; } MDLAHEADER; typedef struct { MDLVHEADER mdlv; MDLSHEADER mdls; } MDLFILE; LittleEndian (); MDLFILE file; // mdlv => vertices information // mdls => skinning information // mdla => animation information ``` ## Vertex blend indices/weights `VERTEX.blendindices`/`blendweight` are always the 32 bytes immediately before the trailing UV pair, regardless of the overall vertex stride: ``` blendIndicesOffset = vertexStride - 40 // 4x DWORD, values are bone indices into the MDLS bone array blendWeightsOffset = vertexStride - 24 // 4x float, sums to 1.0 uvOffset = vertexStride - 8 ``` The "narrow" (52-byte) stride is exactly `position(12) + blendindices(16) + blendweight(16) + uv(8)`. The "wide" (80-byte, and similar) stride inserts a normal + tangent4 between position and the blend data: `position(12) + normal(12) + tangent4(16) + blendindices(16) + blendweight(16) + uv(8)`. This is not a second bone slot as previously assumed - puppet meshes only ever carry one set of up to 4 blend influences. ## MDLS bone array (first array only) ``` CHAR header[9] // "MDLSxxxx\0" DWORD mdlaOffset // absolute file offset where MDLA begins (matches the MDLA search marker) DWORD boneCount BONE bones[boneCount] ``` ``` typedef struct { BYTE tmp; DWORD type; INT32 parent; // index into bones[], -1 for a root bone DWORD matrixByteLength; // always 64 (16 floats) in every sample seen FLOAT bindLocalMatrix[16]; // row-major 4x4, row-vector convention (matches the engine's `mul(v,M)=v*M` // HLSL convention elsewhere); rotation/scale block has always been identity in // every sample seen, translation sits in row 3 (elements 12/13 = tx/ty, 14 = tz=0) CHAR name[]; // null-terminated, always empty in every sample seen } BONE; ``` A second array (`numberOfBones` more entries, previously named `BONE2ENTRY` in this doc) follows and has *not* been decoded - it isn't needed for skinning since the per-bone inverse-bind matrix can be derived directly from the local bind matrices above by walking the parent chain. `mdlaOffset` gives an exact byte length for the whole MDLS section, so the second array can safely be skipped wholesale rather than parsed. ## MDLA (baked animation clips) ``` CHAR header[9] // "MDLAxxxx\0" DWORD A // close to file size; exact meaning/use unclear, not needed for playback DWORD clipCount DWORD C // for single-clip files this equals the scene.json object's // animationlayers[].animation id; ambiguous with >1 clip - match clips by // name instead (scene.json's animationlayers[].name), not by this field DWORD D // always 0 in every sample seen CLIP clips[clipCount] ``` ``` typedef struct { CHAR name[]; // null-terminated, matches scene.json's animationlayers[].name CHAR mode[]; // null-terminated, "loop" observed; other values unconfirmed FLOAT fps; DWORD frameCount; DWORD flag; // always 0 in every sample seen DWORD boneCount; // should match the MDLS bone count BONETRACK tracks[boneCount]; // same order as the MDLS bone array } CLIP; ``` ``` typedef struct { DWORD zero; // always 0 in every sample seen DWORD trackByteLength; // == (frameCount + 1) * 36 SAMPLE samples[frameCount + 1]; // fixed-rate, one every 1/fps seconds; sample[frameCount] == // sample[0] for a "loop" clip (closes the cycle exactly) } BONETRACK; ``` ``` typedef struct { FLOAT posX, posY, posZ; FLOAT rotX, rotY, rotZ; // Euler angles in radians; rotX/rotY are 0 in every 2D puppet sample seen FLOAT scaleX, scaleY, scaleZ; } SAMPLE; ``` There is a small (a few dozen to ~900 bytes, seen to vary per clip), still-undecoded trailer between one clip's last bone track and the next clip's name string (or EOF for the last clip). It doesn't matter for playback of a single matched clip; a parser reading multiple clips sequentially needs to resynchronize past it (e.g. by scanning forward for the next plausible clip header) rather than assuming a fixed size. ## MDAT (attachment points) An optional section between MDLS and MDLA, present on puppets that have named points other objects can follow via scene.json's `"attachment"` field (e.g. an orb or a weapon rigidly stuck to a hand bone). When absent, the MDLS jump field goes straight to MDLA instead. ``` CHAR header[9] // "MDATxxxx\0" DWORD mdlaOffset // absolute file offset where MDLA begins, same role as MDLS's jump field WORD pointCount WORD boneIndex0 // point[0]'s bone index - see note below, this is NOT padding POINT points[pointCount] ``` ``` typedef struct { CHAR name[]; // null-terminated, matches scene.json's "attachment" value on a child object FLOAT localMatrix[16]; // row-major 4x4, same convention as BONE.bindLocalMatrix - the point's transform // relative to this point's bone index WORD nextBoneIndex; // bone index for points[i+1], NOT for this point - see note below. Absent // entirely on the last point (nothing follows it to index) } POINT; ``` The bone index for `points[i]` is written one slot early: `points[0]`'s index is the WORD immediately after `pointCount` (previously assumed to be an unused/padding field), and `points[i]`'s own trailing WORD is actually `points[i+1]`'s index. The last point has no trailing WORD at all. Reading a WORD after every point's matrix (including the last) overruns two bytes past the end of the MDAT section, landing on the next section's magic bytes (`MDLA`/`MDAT` read back as a byte-swapped "implausible" bone index); the shifted reading above consumes the section's declared byte length exactly, confirmed against `mikasaback_puppet.mdl` (2 points, "hair" and "eye" - MDAT section byte length matches exactly under the shifted reading, both resolved bone indices fall comfortably inside that puppet's 73-bone skeleton, and the "hair" attachment already renders correctly using its half of this scheme). The point's live offset from its bind pose is `(animatedBoneWorld[boneIndex] * localMatrix).translation - (bindBoneWorld[boneIndex] * localMatrix).translation`; a child object with a matching `"attachment"` adds that offset to its own local origin instead of (or on top of) following a plain `"parent"` relationship. Only cross-checked against `mikasaback_puppet.mdl` and (for the general shape) `spiritblossomahribase_puppet.mdl`, which declares 3 points where the 3rd previously failed every plausibility check under the old (unshifted) reading - worth re-checking against the shifted reading above, since that file hasn't been re-verified since this was found. Parsing still stops at the first implausible entry and keeps whatever points were read successfully rather than risking garbage data. Reverse-engineered by cross-referencing multiple real puppet `.mdl` files pulled from Workshop content (`ahriarm_puppet.mdl`, `ahritailbottom_puppet.mdl`, `spiritblossomahribase_puppet.mdl`) - matching decoded bind translations/keyframe values against scene.json, and validating that decoded byte offsets exactly consume every byte up to the known MDLA/EOF boundary. No official documentation or decompilation was available for this part of the format.