Assetfix: hook CheckFileExistence to serve loose Data\ files. The original flags|1 approach failed because game paths contain backslashes, causing CheckFileExistence to skip BuildFilePath and check the raw path (no Data\ prefix). Fix: write the correct Data\-prefixed disk path to the output buffer directly and return 1, bypassing the original function for hash map hits. This preserves hook chaining (filename argument is never transformed). Also adds transmogfix (transmog update coalescing), minimapicons (stub), new build options for all three modules, mutex-based multi-DLL safety, embed .skin data into .m2 models, and various module improvements.
18 KiB
WoW M2 Model System Analysis
This document describes the process World of Warcraft (1.12.x client) uses to load, manage, and render M2 model files. The analysis is based on reverse engineering of the game client binary.
Overview
M2 (Model 2) files are the primary 3D model format used in WoW for characters, creatures, doodads, items, and effects. The engine uses a sophisticated multi-layered system involving:
- CM2Shared - Shared model data (geometry, bones, textures) loaded once per model file
- CM2Model - Per-instance model data (animation state, transforms, attachments)
- CM2Scene - Scene management and batch rendering system
Key Data Structures
CM2Model (Instance Data)
Located at per-instance memory, key offsets:
+0x10: Initialization flag (0 = not loaded, 1 = loaded)+0x2c: Parent scene/world context pointer+0x30: Pointer to CM2Shared data+0x34: Parent CM2Model (for attachments)+0x44-0x48: Render list linked list pointers (prev/next)+0x64: Color/texture lookup table+0x90: Bone animation state array+0x94: Bone matrix array (4x4 matrices, 0x40 bytes each)+0xa0: Texture animation state+0xa4: Texture reference array+0x1cc: Next sibling model (for hierarchy traversal)+0x1dc: Child model list head+0x1e8: Children initialized flag+0x200: Particle emitter instance array+0x3c4: Ribbon emitter state array+0x3c8: Particle emitter parameters+0x3cc: Particle system object pointers+0x3d0: Ribbon emitter parameters+0x3d4: Ribbon system object pointers
CM2Shared (Shared Model Data)
Contains the actual model geometry and animation data loaded from .M2 files:
+0x14: Number of global sequences+0x18: Global sequence data offset+0x1c: Number of animations+0x20: Animation data offset+0x24: Animation lookup table offset+0x34: Number of bones+0x38: Bone data offset+0x48: Vertex data (positions, normals, UVs, bone weights)+0x50: LOD skin data pointer+0x54: Number of texture animations+0x5c: Number of textures+0x60: Texture type/flags array+0x64: Number of transparency animations+0x6c: Number of UV animations+0x74: Number of bones (duplicate for validation)+0x88: Render flags array+0x104: Number of lights+0x114: Number of cameras+0x11c: Number of particle emitters+0x120: Particle emitter data offset+0x124: Number of ribbon emitters+0x128: Ribbon emitter data offset+0x130: Model header pointer+0x134: Number of particle systems+0x138: Particle system data offset+0x13c: Number of ribbon systems+0x140: Ribbon system data offset
Loading Process
1. CM2Model_Initialize (0x007103d0)
Entry point for model initialization:
CM2Model_Initialize(this, forceReload, initializeChildren)
- Checks if model is already loaded (
+0x10) - Calls
CM2Model_LoadInstanceDataif needed - Recursively initializes child models via linked list at
+0x1dc
2. CM2Model_LoadInstanceData (0x00710xxx - renamed from LoadPlayerModelData)
Main loading function that allocates and initializes all per-instance data:
- Allocates bone animation state - One entry per bone in the model
- Allocates texture lookup tables - Maps texture slots to actual textures
- Initializes bone matrices - 4x4 transformation matrices for each bone
- Loads particle emitters - Creates particle system instances
- Loads ribbon emitters - Creates ribbon trail instances
- Sets up animation state - Initializes default animations
- Processes queued commands - Handles deferred texture/animation changes
3. Memory Allocation
Uses M2_AllocateModelBuffer (renamed from TextureResource_Load):
buffer = M2_AllocateModelBuffer(size, sourceFile, lineNumber, flags, alignment)
This is a general-purpose allocator that optionally zero-initializes memory.
Rendering Pipeline
Scene Render Entry Point
CM2SceneRenderDraw (0x0070b360) - Main scene rendering function:
CM2SceneRenderDraw(renderContext, viewMatrix, batchData, batchIndices, batchCount)
- Calls
BeginRender()to initialize D3D state - Sets identity transform matrix
- Clears all 9 texture stages
- Sets up vertex shader
- Iterates through batch list, dispatching to appropriate draw function:
- Type 0:
CM2Scene_DrawModelBatchProjected- Projected/UI models - Type 1:
CM2Scene_DrawModelBatch- Standard model batches - Type 2:
CM2Scene_DrawDoodadBatch- Instanced doodads - Type 3:
CM2Scene_DrawRibbonEmitter- Ribbon trails - Type 4:
CM2Scene_DrawParticleEmitter- Particle systems - Type 5:
DrawCallback- Custom render callbacks
- Type 0:
- Clears texture transforms
- Calls
EndRender()
Batch Rendering Context
The render context (passed as this) contains extensive state at offsets +0x3240 through +0x3358:
- Current/previous batch pointers for state change optimization
- Bone matrix upload state
- Pixel shader constant buffers
- Material state tracking
Vertex Skinning (CPU-side)
WoW 1.12 performs vertex skinning on the CPU, not GPU. Key functions:
CM2Model_TransformVerticesSSE (0x0071a9e0)
Optimized SSE path for 16-byte aligned output buffers:
- Processes vertices in a tight loop
- Reads bone weights (4 bytes: weight0, weight1, weight2, weight3)
- Reads bone indices (4 bytes following weights)
- Blends up to 4 bone matrices per vertex
- Transforms position and normal
- Outputs 32 bytes per vertex (pos, normal, UV, color)
CM2Model_ApplySkinning (0x0071a460)
Fallback path for unaligned buffers:
- Same algorithm as SSE version
- Uses standard floating-point operations
Vertex Data Layout (Input)
Each source vertex is 0x30 (48) bytes:
+0x00: Position (3 floats, 12 bytes)+0x0C: Bone weights/indices (8 bytes packed)+0x14: Normal (3 floats, 12 bytes)+0x20: UV coordinates (2 floats, 8 bytes)+0x28: Secondary UV (2 floats, 8 bytes)
Vertex Data Layout (Output)
Each transformed vertex is 0x20 (32) bytes:
+0x00: Transformed position (3 floats)+0x0C: Transformed normal (3 floats)+0x18: UV coordinates (2 floats)
Material System
CM2Scene_SetMaterialRenderState (0x0070c190) manages:
- Blend modes (opaque, alpha, additive, etc.)
- Depth write/test settings
- Backface culling
- Diffuse/ambient color from animation tracks
- Emissive color for glow effects
Blend mode values:
- 0: Opaque
- 1: Alpha key (alpha test)
- 2: Alpha blend
- 3: Additive
- 4: Additive alpha
- 5: Modulate
- 6: Modulate 2x
Texture Binding
Textures are bound using slot indices:
- Slot 0x3F (63): Environment map / reflection
- Slot 0x40 (64): Primary diffuse texture
The model stores texture references in arrays at +0xa4 which point to loaded CGxTexture objects.
Bone Animation System
PlayBoneAnimation (0x007121a0)
PlayBoneAnimation(model, boneIndex, animationId, sequenceIndex, animData, speed, blendMode, queue)
boneIndex = -1affects all bones- Supports blending between animations
- Queue mode allows chaining animations
- Speed can be negative for reverse playback
Animation State (per bone at +0x90)
Each bone's animation state is 0x118 bytes:
+0x98-0xC0: Current animation parameters+0xC4-0xEC: Previous animation (for blending)+0xF8: Queued animation ID+0x100: Blend end time+0x104: Blend rate+0x108: Blend target weight+0x110-0x114: Animation queue linked list
Particle and Ribbon Systems
Particle Emitters
Managed via arrays at +0x3cc (pointers) and +0x3c8 (state):
- Support spherical, planar, and spline emission
- Billboard or velocity-aligned rendering
- Color/alpha gradients over lifetime
- Gravity and drag physics
Ribbon Emitters
Trail effects stored at +0x3d4 (pointers) and +0x3d0 (state):
- Spawn segments along movement path
- UV scrolling along ribbon length
- Fade out over time/distance
Doodad Batching
CM2Scene_DrawDoodadBatch handles instanced rendering of world objects:
- Multiple instances share the same model data
- Per-instance transforms stored in batch buffer
- Bone matrices computed per-instance
- Supports hardware instancing where available
Key Global Variables
CGxDeviceD3d__device(0x00??????): D3D device singletong_skinningFunctionPtr: Function pointer to active skinning routinePTR_00c7b298(0x00C7B298): Global resource manager (CM2Shared cache)PTR_00c7cae0(0x00C7CAE0): Render object list head (iterated by RenderObjectList)PTR_00c7cad8(0x00C7CAD8): Render object list next pointer offset0x00CA7D6C: World object list ( AllocateAndInitializeWorldObject inserts here)0x00c7cb14: Per-render-pass list array (used by CullObjectsToRenderList)0x00c7cb18: Per-render-pass list tail array
Critical Discovery: Dual Object Lists
The game uses TWO separate object lists:
1. World Object List (0x00CA7D6C)
AllocateAndInitializeWorldObjectinserts here when initFlag=1- This is a scene graph list for world state
- NOT used for rendering directly
2. Render Object List (0x00C7CAE0)
RenderObjectList@ 0x006813D0 iterates THIS listCullObjectsToRenderList@ 0x00683AB0 processes this list- Objects must be HERE to be rendered
This is why markers don't render: Objects are added to the world object list but never registered with the render object list.
Render List Registration
Objects get into the render list through:
-
CullObjectsToRenderList - Called each frame to cull and sort objects
- Reads from game object manager (param_1 + 0x38 + renderListIndex * 0xC)
- Performs frustum culling via
IsSphereInFrustumandFrustumCullBoundingBox - Inserts visible objects into sorted linked list via
InsertSortedLinkedList - List head stored at
0x00c7cb18 + renderListIndex * 0xC
-
CM2Model_ManageRenderListNode @ 0x00710B90
- Adds/removes model from its scene's render list
- Uses
+0x44(prev) and+0x48(next) pointers on the model - Scene pointer at
+0x2Cmust be valid
-
addModelToLoadQueue @ 0x0071D5A0
- Called by
InitializeModelAttachmentNodeat the end of setup - Either queues model for async load or calls
CM2Model_LoadInstanceDatadirectly - Model load queue is at scene
+0x10
- Called by
Object Manager Integration
Units get rendered through SetupUnitDisplayHandler:
createModelAttachment(resource_mgr, model_path, 0)→ render contextCGUnit_C__SetDisplayHandler(unit, render_ctx)→ attaches to unit- Unit is already registered with object manager
CreateGameObject_WithPropertiescalled with unit's model → creates game object- Game object automatically picked up by
CullObjectsToRenderListvia object manager
Key insight: Units work because they're already in the object manager. Standalone world objects need separate registration.
Critical Function: CM2Model_ManageRenderListNode
Address: 0x00710B90
Convention: __thiscall (ECX = render context, stack: enable)
This function adds/removes a model from its scene's render list:
void CM2Model_ManageRenderListNode(void *renderContext, int enable) {
if (enable == 0) {
// Remove from list: unlink +0x44 and +0x48
} else {
// Add to list: insert at scene+0x20
// Scene pointer read from renderContext+0x2C
}
}
CRITICAL: After CM2Model_CreateForModelObject, must call:
CM2Model_ManageRenderListNode(render_ctx, 1)
Without this call, the model exists in the world object list but is never rendered because it's not in the scene's render list.
Game Object List (PTR_00C7CAEC)
The primary render path for dynamic objects:
- List head:
0x00C7CAEC - Linked list offsets:
+0x16C(prev),+0x170(next) - Iterator:
AllocateGameObject@ 0x00683F80 (called each frame)
How it works:
AllocateGameObjectiterates the list- For each object with model at
+0x88:- Calculates distance from camera
- Calls
CM2Model_ManageRenderListNode(obj+0x88, visible) - Calls
SetRenderCallbacksfor lighting - Sets fade/transparency based on distance
To add an object:
// Insert at head (sentinel pattern)
obj[0x16C] = 0x00C7CAEC; // prev = list head address
obj[0x170] = old_head; // next = old list head
[0x00C7CAEC] = obj; // new head = this object
Object requirements:
+0x88: Model/render context pointer+0x5C-0x68: Position (XYZ) for distance calculation+0x8: Visibility flags (0x8000 = visible)+0x174/+0x178: Optional manager callback slots used byAllocateGameObject
Critical safety note (2026-02-27)
AllocateGameObject (0x00683F80) contains an indirect callback call:
- if
obj+0x174 != 0, engine executescall [obj+0x174]with context from+0x178.
If custom-inserted objects (manual list insertion path) carry garbage at +0x174, this causes immediate crash (jump to invalid address).
Mitigation for manual insertion path:
- Set
obj+0x174 = 0 - Set
obj+0x178 = 0before linking object intoPTR_00C7CAEC.
List pointer tagging note
Manager list pointers may be low-bit tagged (ptr | 1, e.g. ...E9).
Always clear tag bits before dereferencing list links during unlink or traversal (ptr & ~1).
Function Renames Applied
| Old Name | New Name | Address |
|---|---|---|
| TextureResource_Load | M2_AllocateModelBuffer | - |
| LoadPlayerModelData | CM2Model_LoadInstanceData | - |
| optimizedBoneTransform | CM2Model_TransformVerticesSSE | 0x0071a9e0 |
| applyBoneTransforms | CM2Model_ApplySkinning | 0x0071a460 |
| calculateBoneMatrices | CM2Model_ApplySkinningUnaligned | 0x0071a720 |
| DrawBatch | CM2Scene_DrawModelBatch | 0x0070cf70 |
| DrawBatchProj | CM2Scene_DrawModelBatchProjected | 0x0070cb30 |
| DrawBatchDoodad | CM2Scene_DrawDoodadBatch | 0x0070d330 |
| DrawParticle | CM2Scene_DrawParticleEmitter | - |
| DrawRibbon | CM2Scene_DrawRibbonEmitter | - |
| SetupRendering | CM2Scene_SetupRenderState | - |
| RenderMesh | CM2Model_RenderSkinnedMesh | 0x00719ac0 |
| BuildVertexBuffer | CM2Shared_BuildSkinnedVertexBuffer | 0x0071dc80 |
| executeSceneRenderPass | CM2Scene_ExecuteRenderPass | 0x00708900 |
| InitializeModelHierarchy | CM2Model_InitializeHierarchy | 0x00710560 |
| validateAndFixupBoneArray | CM2Model_ValidateBoneArray | 0x0071e9d0 |
| SetMaterialRenderState | CM2Scene_SetMaterialRenderState | 0x0070c190 |
Rendering Flow Summary
Game Loop
|
v
CM2Scene_ExecuteRenderPass
|
v
CM2SceneRenderDraw
|
+---> Check hardware shader support (flag bit 8)
| |
| +---> If supported: initPixelShaderDispatcher4/2()
| Load Model2.bls vertex shader
|
+---> For each batch:
| |
| +---> Type 0/1: CM2Scene_DrawModelBatch[Projected]
| | |
| | +---> CM2Scene_SetupRenderState
| | +---> CM2Scene_SetMaterialRenderState
| | +---> [GPU Path] Upload bone matrices to shader constants
| | | RenderVertexBuffer with shader
| | +---> [CPU Path] CM2Model_TransformVerticesSSE
| | DrawPrimitive (pre-transformed)
| |
| +---> Type 2: CM2Scene_DrawDoodadBatch
| | |
| | +---> [GPU Path] Upload bone matrices per instance
| | | CM2Shared_BuildSkinnedVertexBuffer
| | +---> [CPU Path] g_skinningFunctionPtr per instance
| | DrawPrimitive (pre-transformed)
| |
| +---> Type 3: CM2Scene_DrawRibbonEmitter
| +---> Type 4: CM2Scene_DrawParticleEmitter
|
v
EndRender
Vertex Skinning Paths
The WoW 1.12 client supports TWO vertex skinning paths:
1. GPU Vertex Shader Skinning (Hardware Path)
Enabled when:
- Hardware supports vertex shaders (checked via
UpdateLightingOffset() + 0x94) - The
M2UseShadersconsole variable is enabled - Flag bit 8 is set in the renderer context at
*(renderer + 0x44) + 4
When enabled:
- Loads vertex shader from
"shaders\vertex\Model2.bls" - Bone matrices are uploaded to shader constants (offsets +0x70 through +0xac in render context)
initPixelShaderDispatcher4()andinitPixelShaderDispatcher2()configure the shader pipeline- Vertex transformation happens on the GPU
Evidence: String "skin models using vertex shaders" at 0x0082e68c describes this feature.
2. CPU Vertex Skinning (Software Fallback)
Used when hardware shaders are unavailable or disabled:
Function pointer: g_skinningFunctionPtr (0x00cf04c8) is set during initialization:
- Default:
CM2Model_ApplySkinningUnaligned(basic FPU path) - SSE2 capable:
CM2Model_TransformVerticesSSE(SIMD optimized)
The CPU path:
- Reads bone weights (4 bytes) and indices (4 bytes) per vertex
- Blends up to 4 bone matrices per vertex
- Outputs transformed position, normal, and UVs
- 32 bytes output per vertex
Path Selection in Drawing Functions
CM2Scene_DrawDoodadBatch:
if (renderContext[0x32f0] == 0) {
// CPU path: call g_skinningFunctionPtr for each instance
} else {
// GPU path: upload bone matrices to shader constants
}
CM2Scene_DrawModelBatchProjected:
if (renderContext[0xcbc] != 0) { // flag bit 8
// GPU path: bone matrices to constants, use vertex shader
} else {
// CPU path: software transform
}
Console Variables
M2UseShaders- Enable/disable GPU vertex shader skinningM2UsePixelShaders- Related pixel shader featuresM2UseThreads- Multi-threaded model processing