MDL Format (Model Hierarchy)
The .mdl format serves as the overarching structural spine for 3D model geometry. Rather than storing literal vertex positions directly, it recursively structures a tree of generalized nodes (Bones, Trimeshes, Lights, Emitters) into a unified visual mesh. It delegates vertex geometry out, binds textures, links dynamic controllers (keyframe transformations), and maps bounding sphere matrices directly to the model’s rigid physical space.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .mdl |
| Magic Signature | Text (filedependancy) or Binary (\0 byte header) |
| Type | 3D Hierarchical Mesh |
| Rust Reference | View rakata_formats::Mdl in Rustdocs |
Data Model Structure
Rakata maps the .mdl binary tree exactly into rakata_formats::Mdl.
Because a model intrinsically utilizes 11 distinct struct sub-types, Rakata resolves the pointer-based tree structure into a secure Rust Vec<MdlNode>. Native file pointer offsets which are normally resolved inside KOTOR via an explicit raw memory relocation dump are converted into safe recursive structures at parse time.
Node Sub-Types
The engine determines exact node allocations using a rigid bitflag header.
| Sub-Type | Description |
|---|---|
| Base | A pure structure node (Dummy) acting strictly as an invisible visual group or spatial pivot. |
| Light | Projects localized dynamic lighting, lens flares, and shading priorities. |
| Emitter | Configures particle spawning systems (fountains, single-shots, lightning, explosions). |
| Camera | An empty node serving as a static viewport anchor for dialogue cinematics. |
| Reference | An anchor point explicitly linking an external 3D model asset to a point. |
| TriMesh | A rigid standard triangle geometry boundary carrying static vertex arrays. |
| SkinMesh | A procedural mesh utilizing skeleton bone-weights and vectors to calculate organic deformations. |
| AnimMesh | A mesh carrying hardcoded, explicitly sampled vertex coordinate animation loops. |
| DanglyMesh | A sub-mesh evaluated through swinging physics constraints (displacement, tightness, period). |
| AABB | A strict spatial collision tree structurally defining an internal walkmesh barrier. |
| Saber | Allocates dynamic 3D quad arrays utilized exclusively to generate stretching lightsaber swing trails. |
Engine Audits & Decompilation
Deep Dive: For an exhaustive archive of the Ghidra decompilation notes detailing the exact byte-level layout of the binary MDL format and engine loading pipeline, refer to the MDL & MDX Deep Dive.
The following information documents the engine’s exact load sequence for genuine Binary MDL models. All behavior was mapped from natively analyzing swkotor.exe execution pipelines via Ghidra.
Loading and Wrapper Validation
Read initially via Input::Read (0x004a14b0).
| Pipeline Event | Ghidra Provenance & Engine Behavior |
|---|---|
| Binary vs ASCII Detection | The engine checks the exact first byte of the file. If it hits a \0 (NULL), it dispatches the asset entirely to the InputBinary track. If it hits text ("filedependancy" or "newmodel"), it loops into the FuncInterp ASCII parser track. |
| Wrapper Mapping | The Binary format evaluates the initial 12 bytes as an abstract Wrapper block defining explicit sizes for the .MDL and the associated .MDX geometry. |
| In-Memory Heap Dump | The engine allocates the sizes noted in the wrapper, runs memcpy on both the .MDL and .MDX assets blindly into memory, and then runs the recursive Reset path to relocate spatial internal pointer offsets to absolute memory addresses. |
Node Dispatch Architecture
Read initially via InputBinary::ResetMdlNode (0x004a0900). The engine recursively navigates downwards matching against a constant 16-bit node-type flag lookup spanning from 0x0001 (Base Node) to 0x0821 (Lightsaber).
| Mapped Property | Engine Behavior |
|---|---|
| Sub-node Allocation Sizes | Nodes are dynamically allocated varying byte lengths strictly based on their type-mask. A root Base node only evaluates 80 contiguous bytes, but an Emitter allocates 304, and a Skin allocates 512. |
| Parent/Child Graph Resolution | Engine structures evaluate nodes continuously downward via embedded raw pointer arrays. These arrays branch a group of distinct sub-children implicitly off their master parent. At load time, the engine must safely rewrite all relative file offsets into absolute physical memory locations, otherwise the entire hierarchy will instantly detach. |
Mapped Behavior Quirks
| Mapped Property | Ghidra Provenance & Engine Behavior |
|---|---|
| LOD Suffix Generation | The engine natively evaluates if the cullWithLOD property is set. If true, it explicitly triggers string concatenations for FindModel(name + "_x") and FindModel(name + "_z") sequentially to dynamically attach lower-quality auxiliary geometry instances based on viewport distance. |
| Animation Bone Binding | When building the live hierarchy tree for a rendering sequence, the engine explicitly ignores the node’s textual string name. Instead, it rigidly evaluates physical pairings against a mapped node_id integer. If the bone isn’t properly sequenced to that numeric ID array, it detaches from the runtime arrays entirely. |
| Self-Describing Keyframes | Unlike older properties that rely on rigid dictionaries, KOTOR determines how an animation was saved dynamically by reading the keyframe’s controller type integer. It applies a bitwise AND check against the type’s lowest hex digit (& 0x0F) to instantly dictate whether the loaded keyframe is a single float (like scaling), 3 floats (like an XYZ positional vector), or 4 floats (for a Slerp quaternion rotation). |
Proposed Linter Rules (Rakata-Lint)
While rakata-lint currently only evaluates GFF formats and does not yet parse .mdl models dynamically, the engine behaviors above hint at some suggested lint diagnostics:
Planned Lint Diagnostics:
- Skeleton / Animation Tracing: Flags animation nodes where the internal skeletal
node_numberbinding parameter implicitly equals0, ensuring the mesh does not hard freeze via pointing to the rigid root spine. - Controller Mask Encoding: Validates that generic Controller properties properly bit-mask against the Bezier indicator (
0x10) rather than reading explicitly raw quaternion values (which causes cascading loop failures through the rest of the array block). - Emitter Detonation Allocation: Flags interactive
Emitternodes attempting to bind thedetonatekey (Controller502) while structurally mis-identifying as"Fountain". The engine native only maps controller 502 data to strict"Explosion"memory paths, resulting in an aggressive Access Violation engine crash otherwise. - Name Graph Sanitization: Notifies developers if the node graph contains artificially un-referenced graph pointers mapped under the unified Name Table. (BioWare notoriously shipped identical shared name tables compiling
.pwkand.wokmodels into.mdlnodes natively throughout the 2003 pipeline).