Untrack TODO.md, RELEASING.md, RELEASE_NOTES.md, ideas/ and docs/CLAUDE_PROPER_OBJECT_REGISTRATION_PLAN.md -- internal planning and agent notes with no reason to be published. Files stay on disk locally. Replace remaining absolute machine paths in docs with relative ones (one referenced a separate private project), and swap decorative emoji for ASCII markers. README: lead with the no-longer-actively-developed notice.
84 KiB
Outline Shading Research Log
Goal
Implement outline shading for corpses visible through walls in the WoW 1.12.1 overlay DLL.
Research Findings
WoW 1.12.1 Native Highlighting System (from Ghidra)
Key Functions Found:
HandleUnitHighlight@ 0x00492890 - Main unit highlight handlerSetTargetHighlight@ 0x00614550 - Sets highlight on target, calls SetModelAmbientColorEnableTargetHighlight@ 0x004945e0 - Enables highlight, calls SetTargetHighlightUnitHighlightWrapper@ 0x00492e70 - Wrapper for HandleUnitHighlightRenderUnitSelectionIndicator@ 0x00611ff0 - Renders selection circle under unitsSetupSelectionRenderStates@ 0x00614e00 - Configures render states for selection
Selection Render States (SetupSelectionRenderStates):
SetRenderState(7, 3); // Blend mode
SetRenderState(0x14, 0); // D3DRS_ALPHATESTENABLE = false
SetRenderState(0x0e, 0); // D3DRS_ZWRITEENABLE = false
SetRenderState(0x12, 0); // D3DRS_ALPHAFUNC disabled
Model Rendering Pipeline:
CM2SceneRenderDraw@ 0x0070b360 - Main M2 model scene renderingDrawBatch@ 0x0070cf70 - Draws model batchesDrawBatchDoodad@ 0x0070d330 - Draws doodad batchesSetModelAlpha@ 0x00710da0 - Sets model alpha (writes to offset 0x1c4)SetModelAmbientColor@ sets RGB at offsets 0x190, 0x194, 0x198
Render State Functions:
SetRenderState@ wraps D3D_SetRenderStateD3D_SetRenderState@ uses CGxDeviceD3d__device
Depth/Stencil Functions:
setDepthTest@ 0x0071f9d0depthFunc@ 0x005a4af0depthMask@ 0x005a4ac0CreateDepthStencilSurface@ 0x005999c0stencilFuncSeparate@ 0x005a4810stencilOpSeparate@ 0x005a49b0
Nameplate Rendering (renderUnitNameplate @ 0x006c6e90):
Shows how to render world-space elements with proper transform:
- SetTransformMatrix for identity
- BeginRender()
- SetRenderState calls for depth/alpha
- CreateVertexBuffer, LockVertexBuffer
- DrawPrimitive
- EndRender()
UnitXP_SP3 Source Analysis
Repository: https://codeberg.org/konaka/UnitXP_SP3
Key Files:
sceneBegin_sceneEnd.cpp- Hooks WoW's scene rendering pipelineVanilla1121_functions.cpp- Function addresses and hooksworldText.cpp- Combat text rendering
Scene Rendering Hook Pattern:
void __fastcall detoured_sceneBegin(uint32_t CGxDevice, void* ignored, uint32_t unknown) {
HRESULT test = dxDevice->TestCooperativeLevel();
if (D3DERR_DEVICELOST == test || D3DERR_DEVICENOTRESET == test) {
sceneEnd_fontsOnLostDevice();
}
}
Scene end hook iterates text collections and calls update()/draw().
Key Function Addresses (from UnitXP):
getCamera@ 0x4818F0worldToScreen@ 0x483ee0 (same as our WOW_FUNC_WORLD_TO_SCREEN)getObject_byGUID@ 0x464870 (same as our WOW_FUNC_GET_OBJECT_BY_GUID)- Unit position via vftable offset 0x14 -> 0x5f1f10
Outline Rendering Techniques
Option 1: Stencil Buffer Approach (Classic)
- Render model to stencil buffer only (no color write)
- Disable depth test
- Render slightly scaled-up model with outline color where stencil != written
- Re-enable depth test
Option 2: Two-Pass with Depth Disable
- First pass: Render model normally
- Second pass: Disable Z-test, render wireframe or scaled silhouette
Option 3: Post-Process Edge Detection
- Hook EndScene
- Use pixel shader to detect edges based on depth discontinuities
- Overlay edges as outline
Option 4: Hook Model Rendering
- Hook DrawBatch or CM2SceneRenderDraw
- Identify corpse models being rendered
- Add extra render pass with modified states
D3D9 Render States Reference
For outline/through-wall effects:
// Disable depth testing (see through walls)
pDevice->SetRenderState(D3DRS_ZENABLE, FALSE);
pDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
// Enable alpha blending
pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
// For wireframe outline
pDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
Implementation Status [COMPLETED]
All objectives achieved - see "Working Implementation" at end of document:
- [OK] Hook DrawIndexedPrimitive to detect corpse/target/raid-marked models
- [OK] Stencil-based outline rendering in EndScene
- [OK] Custom vertex shader for bone-animated outline expansion
- [OK] Per-category visual effects (dark halo for dead, colored outline for marks/target)
- [OK] Through-wall visibility with proper body/outline layering
D3D9 "Chams" / Wallhack Technique
The classic approach used in game mods for through-wall visibility:
How It Works:
- Hook
DrawIndexedPrimitive(D3D9 vtable index 82) - Identify target models by stride/vertex count/primitive count
- Render model twice:
- First pass: Normal render (visible when not occluded)
- Second pass: Disable Z-buffer, render with colored material (visible through walls)
D3D9 VTable Indices:
- EndScene = 42 (currently hooked)
- DrawIndexedPrimitive = 82
- DrawPrimitive = 81
- SetRenderState = 57
- SetTexture = 65
Chams Implementation Pattern:
HRESULT WINAPI hkDrawIndexedPrimitive(
IDirect3DDevice9* pDevice,
D3DPRIMITIVETYPE Type,
INT BaseVertexIndex,
UINT MinVertexIndex,
UINT NumVertices,
UINT StartIndex,
UINT PrimitiveCount)
{
// Check if this is our target model (by stride, vertex count, etc.)
UINT stride;
pDevice->GetStreamSource(0, &pVB, &offset, &stride);
if (IsTargetModel(stride, NumVertices, PrimitiveCount)) {
// First pass: render with Z-buffer disabled (through walls)
pDevice->SetRenderState(D3DRS_ZENABLE, FALSE);
pDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
// Set color (e.g., red for behind walls)
oDrawIndexedPrimitive(pDevice, Type, BaseVertexIndex, MinVertexIndex,
NumVertices, StartIndex, PrimitiveCount);
// Second pass: normal render (visible normally)
pDevice->SetRenderState(D3DRS_ZENABLE, TRUE);
pDevice->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);
// Set different color (e.g., green for visible)
}
return oDrawIndexedPrimitive(pDevice, Type, BaseVertexIndex, MinVertexIndex,
NumVertices, StartIndex, PrimitiveCount);
}
Challenge: Model Identification
The hard part is identifying which draw calls correspond to corpse models. Options:
- Log stride/vertex/primitive values and correlate with visual inspection
- Hook WoW's internal functions to track which object is being rendered
- Use world position correlation (complex)
Current d3d9_hook.cpp Structure
Our existing hook infrastructure:
- Uses dummy device technique to get vtable
- Patches vtable entry for EndScene (index 42)
- Has PatchVTableEntry/RestoreVTableEntry helpers
- Calls RenderConsole() (which renders dead overlay) in EndScene hook
To add DrawIndexedPrimitive hook:
- Add DRAWINDEXEDPRIMITIVE_VTABLE_INDEX = 82
- Create hkDrawIndexedPrimitive function
- Patch vtable at index 82
Practical Implementation Strategies
Strategy A: DrawIndexedPrimitive Hook with Model Logging
- Hook DrawIndexedPrimitive
- Add logging mode to record stride/vertices/primitives
- Visually identify corpse-related values in game
- Filter and apply chams effect to matching draw calls
Pros: Standard technique, well-documented Cons: Requires empirical model identification, may affect many unrelated draws
Strategy B: Hook WoW's CM2SceneRenderDraw
- Hook CM2SceneRenderDraw @ 0x0070b360 using MinHook/detours
- In hook, check if rendering model belongs to dead player
- Modify render states before calling original
Pros: Direct access to model context, knows what's being rendered Cons: Requires understanding WoW's internal structures
Strategy C: Simple Circle/Glow Indicator (Current Enhancement)
- Keep current skull/name rendering
- Add a colored circle/glow rendered at corpse position
- Render with Z-buffer disabled so visible through walls
Pros: Simple, works with current architecture Cons: Not true model outline, just a marker
Strategy D: Hybrid - World-Space Outline Sprite
- Create an outline/halo texture
- Render it in world-space at corpse position (like selection circles)
- Disable Z-buffer so it shows through walls
- Use WoW's BeginRender/EndRender pattern
Pros: Looks like selection circle but visible through walls Cons: Not model-conforming outline
Recommended Approach
Start with Strategy D (world-space outline sprite) as it:
- Works with our current EndScene hook
- Doesn't require DrawIndexedPrimitive model identification
- Can be enhanced later with model-specific rendering
Implementation steps:
- Create a circular outline texture (or use D3DX to draw circle)
- In RenderDeadOverlay, for each corpse:
- Transform corpse world position to screen
- Disable Z-buffer
- Render circle at ground level
- Re-enable Z-buffer
- Render skull and name on top (already working)
Strategy D Implementation (COMPLETED)
Added DrawCircleAtScreen() function in dead_overlay.cpp:
static void DrawCircleAtScreen(IDirect3DDevice9* pDevice, float screenX, float screenY,
float radius, float thickness, D3DCOLOR color) {
// Save render states
// ...
// DISABLE DEPTH TESTING - this makes it visible through walls!
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
pDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
// Enable alpha blending for semi-transparent effect
pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
// Draw thick ring using triangle strip (inner and outer circles)
float innerRadius = radius - thickness / 2.0f;
float outerRadius = radius + thickness / 2.0f;
COLORED_VERTEX ring[(CIRCLE_SEGMENTS + 1) * 2];
for (int i = 0; i <= CIRCLE_SEGMENTS; i++) {
float angle = (float)i / (float)CIRCLE_SEGMENTS * 2.0f * PI;
// Create inner and outer vertices for each segment
// ...
}
pDevice->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, CIRCLE_SEGMENTS * 2, ring, sizeof(COLORED_VERTEX));
// Restore states
// ...
}
Circle Parameters Used:
- Radius: 35.0f pixels
- Thickness: 6.0f pixels
- Color:
D3DCOLOR_ARGB(180, 64, 200, 255)- semi-transparent cyan/blue
Key Insight:
The circle is visible through walls because D3DRS_ZENABLE is set to D3DZB_FALSE, which disables depth testing. This means the circle is drawn regardless of what's in front of it in 3D space.
Next Step: Strategy B (Model Outline)
To implement true model outlines, we need to hook WoW's internal CM2SceneRenderDraw function at 0x0070b360.
Model-to-GUID Mapping Discovery
From CGUnit_LoadModelWithEquipment:
SetCallbackFunctions(this, DrawObjectModel,
(undefined *)**(undefined4 **)((int)param_1 + 8), // GUID low
(undefined *)(*(undefined4 **)((int)param_1 + 8))[1]); // GUID high
Model structure stores owner GUID:
model + 0x1f8= GUID low (32-bit)model + 0x1fc= GUID high (32-bit)
During CM2SceneRenderDraw:
renderContext + 0x3310= model data pointer- From model data:
*(uint32_t*)(modelData + 0x1f8)= GUID low - From model data:
*(uint32_t*)(modelData + 0x1fc)= GUID high
Hook Implementation Plan
- Create trampoline for
CM2SceneRenderDrawat0x0070b360 - In detour function:
- Call original to render normally
- For each batch, extract model GUID from
renderContext + 0x3310 -> +0x1f8/0x1fc - Check if GUID belongs to dead friendly player (using our existing tracking)
- If dead player: render again with Z-buffer disabled and colored shader
Function Signature
// __thiscall means 'this' is in ECX register
typedef void (__thiscall *CM2SceneRenderDraw_t)(
void* thisPtr, // ECX
void* viewMatrix, // arg1
int batchData, // arg2
int batchIndices, // arg3
uint32_t batchCount // arg4
);
Sources
- UnitXP_SP3 Codeberg
- UnitXP_SP3 GitHub Fork
- wowdev.wiki Rendering
- wowdev.wiki M2
- wowdev.wiki M2/Rendering
- D3D9 Chams Tutorial
- Stack Overflow D3D9 Hooking
Detailed Structure Analysis (Ghidra Decompilation)
CM2SceneRenderDraw @ 0x0070b360 (FULL ANALYSIS)
void __thiscall CM2SceneRenderDraw(void* this, undefined* viewMatrix,
int batchData, int batchIndices, uint batchCount)
Prologue (9 bytes total - MUST copy all for inline hook):
0x0070b360: PUSH EBP ; 1 byte (55)
0x0070b361: MOV EBP, ESP ; 2 bytes (8B EC)
0x0070b363: SUB ESP, 0x80 ; 6 bytes (81 EC 80 00 00 00)
Batch Processing Loop:
for (uVar4 = 0; uVar4 < batchCount; uVar4++) {
// Get batch pointer: batchData + (batchIndices[i] * 0x40)
puVar2 = (uint32_t*)(*(int*)(batchIndices + uVar4 * 4) * 0x40 + batchData);
// Store batch ptr at renderContext+0x3300
*(uint32_t**)(this + 0x3300) = puVar2;
// batch[0] = batch type, stored at renderContext+0x3308
*(uint32_t*)(this + 0x3308) = puVar2[0];
// batch[1] = CM2Model pointer
iVar1 = puVar2[1];
*(int*)(this + 0x3310) = iVar1;
// Read CM2Model+0x30 -> stored at renderContext+0x3318
*(uint32_t*)(this + 0x3318) = *(uint32_t*)(iVar1 + 0x30);
// Read CM2Model+0x3b8 -> stored at renderContext+0x3320
*(uint32_t*)(this + 0x3320) = *(uint32_t*)(iVar1 + 0x3b8);
// Read (CM2Model+0x30)->0x130 -> stored at renderContext+0x48
*(uint32_t*)(this + 0x48) = *(uint32_t*)(*(int*)(this + 0x3318) + 0x130);
switch(batch[0]) {
case 0: DrawBatchProj(this); break; // Projected/2D
case 1: DrawBatch(this); break; // Standard 3D
case 2: DrawBatchDoodad(...); break; // Doodads
case 3: DrawRibbon(this); break; // Ribbons
case 4: DrawParticle(this); break; // Particles
case 5: DrawCallback(this); break; // Callbacks
}
}
Batch Structure (0x40 bytes per batch)
| Offset | Size | Description |
|---|---|---|
| 0x00 | 4 | Batch type (0-5) |
| 0x04 | 4 | CM2Model pointer |
| 0x20 | 4 | Count (used in doodad batching) |
| 0x2c | 4 | Some index |
| 0x30 | 4 | Another index |
CreateUnitModel @ 0x00695100
undefined* __fastcall CreateUnitModel(undefined** modelData, int unitObject, int forceInitialize)
{
// Create model attachment
this = createModelAttachment(PTR_00c7b298, modelData, 0);
// IMPORTANT: Store model pointer at unit+0x88
*(undefined***)(unitObject + 0x88) = this;
if (this == NULL) return NULL;
CopyArrayToObject(this, (undefined**)(unitObject + 0xcc));
SetModelScale(*(void**)(unitObject + 0x88), ...);
// Store unit pointer in model at multiple offsets
SetCallbackFunctions(*(void**)(unitObject + 0x88), HandleSoundEvents,
(undefined*)unitObject, (undefined*)0x0);
SetRenderCallbacks(*(void**)(unitObject + 0x88), EntityRenderCallback_ProcessLighting,
(undefined*)unitObject);
PlayBoneAnimation(...);
if (forceInitialize) InitializePlayerModel(...);
return (undefined*)0x1;
}
SetCallbackFunctions
void __thiscall SetCallbackFunctions(void* this, undefined* callback1,
undefined* callback2, undefined* callback3)
{
*(undefined**)(this + 0x1F4) = callback1; // HandleSoundEvents
*(undefined**)(this + 0x1F8) = callback2; // unitObject pointer!
*(undefined**)(this + 0x1FC) = callback3; // NULL
}
SetRenderCallbacks
void __thiscall SetRenderCallbacks(void* this, undefined* renderCallback1,
undefined* renderCallback2)
{
*(undefined**)(this + 0x3BC) = renderCallback1; // EntityRenderCallback_ProcessLighting
*(undefined**)(this + 0x3C0) = renderCallback2; // unitObject pointer!
}
Key Offset Summary
Unit/Corpse Object:
| Offset | Description |
|---|---|
| 0x30 | GUID low 32 bits |
| 0x34 | GUID high 32 bits |
| 0x88 | Model pointer (CM2Model instance) |
CM2Model Instance (from unit+0x88):
| Offset | Description |
|---|---|
| 0x1F4 | Sound callback function |
| 0x1F8 | Owner unit pointer (via SetCallbackFunctions) |
| 0x1FC | NULL |
| 0x3BC | Render callback function |
| 0x3C0 | Owner unit pointer (via SetRenderCallbacks) |
IMPORTANT DISCOVERY: The model pointer stored at unit+0x88 has back-pointers to the unit at model+0x1F8 AND model+0x3C0.
The Batch-to-Unit Mismatch Problem
Debug Log Evidence
[DeadOverlay] Corpse obj=0x3AE98008, +0x88=0x3AE98090, +0x120=0x3AE98125
[ModelOutline] Added dead player model: 0x3AE98090 (count=1)
[ModelOutline] batch modelData=0x33C4A008, tracking model=0x3AE98090
The Problem:
- Corpse object:
0x3AE98008 - Corpse's model (unit+0x88):
0x3AE98090 - Batch's modelData (batch[1]):
0x33C4A008
batch[1] (0x33C4A008) ≠ corpse model (0x3AE98090)
Hypothesis
The batch[1] pointer is NOT the same structure as unit+0x88. They're different objects:
- unit+0x88 = CM2Model instance (created by createModelAttachment, ~0x428 bytes)
- batch[1] = Something else, possibly:
- M2 file data pointer
- Render element wrapper
- Intermediate structure that CONTAINS a reference to the CM2Model
Chain to Explore
From CM2SceneRenderDraw:
batch[1] -> +0x30 -> stored at renderContext+0x3318
renderContext+0x3318 -> +0x130 -> stored at renderContext+0x48
Maybe: batch[1]+0x30 points to the CM2Model instance (0x3AE98090)?
Or: Need to find where in batch[1] structure the link to unit+0x88 model exists.
Next Investigation Steps
- Log batch[1] structure contents - read batch[1]+0x00 through +0x40 to find 0x3AE98090
- Follow the chain - check if batch[1]+0x30 -> ... -> leads to unit's model
- Alternative approach - if batch[1] is the "real" M2 model, then batch[1]+0x3C0 might directly give us the unit pointer
Alternative Matching Strategy
Instead of matching model pointers, try:
- From batch[1], read batch[1]+0x3C0 (owner unit pointer, if it exists)
- If valid, read (batch[1]+0x3C0)+0x30 to get GUID
- Compare GUID with tracked dead player GUIDs
New Research: CGUnit_ShouldRender Hook Strategy (from perf_boost)
Source: https://gitea.com/avitasia/perf_boost
The perf_boost addon hooks CGUnit_ShouldRender to intercept unit rendering decisions.
Key Offsets from perf_boost
CGUnitShouldRender = 0x00607da0
CGUnitPreAnimate = 0x00607ed0
CGUnitAnimate = 0x00608560
OnWorldRender = 0x00483460
CGUnit_ShouldRender Analysis (0x00607da0)
Function Signature:
// __thiscall: ECX = unit pointer
// Stack: stateFlags (uint32_t)
// Returns: non-zero if should render, 0 otherwise
// Callee cleans stack (RET 0x4)
undefined* __thiscall CGUnit_ShouldRender(void* this, uint stateFlags)
Prologue (6 bytes):
00607da0: PUSH EBX ; 1 byte
00607da1: MOV EBX,ESP ; 2 bytes
00607da3: SUB ESP,0x8 ; 3 bytes
Key Logic:
// Check if model needs initialization
if (*(int*)(unit + 0xCCC) != 0) {
// Initialize model at unit+0xD8
if (CM2Model_Initialize(*(void**)(unit + 0xD8), 0, 0) == NULL) {
return NULL; // Don't render
}
*(uint*)(unit + 0xCCC) = 0;
}
// Add model to render list
void* model = *(void**)(unit + 0xDC); // Try alternate model first
if (model == NULL) {
model = *(void**)(unit + 0xD8); // Fallback to primary model
}
CM2Model_ManageRenderListNode(model, 1); // 1 = add to list
Unit Model Pointer Relationships
CRITICAL FINDING: There are MULTIPLE model-related offsets on units:
| Offset | Description | Set By |
|---|---|---|
| 0x88 | Model pointer from CreateUnitModel | CreateUnitModel() |
| 0xD8 | Primary render model pointer | CGUnit_ShouldRender uses this |
| 0xDC | Alternate model (mounted/transformed?) | Unknown |
Model+0x3C0 stores owner unit pointer (set by SetRenderCallbacks in CreateUnitModel)
Corpses vs Dead Units - IMPORTANT DISTINCTION
Player Death Behavior in WoW 1.12.1:
-
Dead Players = CGUnit objects with health ≤ 0
- Still rendered via CGUnit_ShouldRender
- Model at unit+0xD8 is the corpse model (lying down)
- GUID at unit+0x30/0x34 unchanged
-
Corpse Objects (type 0x40 or 0x80) = Different object type
- Used for YOUR OWN corpse marker (for resurrection)
- NOT enemy player corpses
- Created at death location, separate from player unit
For enemy player corpses: We need to track CGUnit objects where:
- Health ≤ 0:
*(int*)(*(int*)(unit + 0x110) + 0x40) < 1 - Or dead flag:
(*(uint*)(*(int*)(unit + 0x110) + 0x224) >> 5 & 1) != 0
Health/Death Check (from Lua_UnitIsDead @ 0x00517ac0)
// Get unit descriptors
int* descriptors = *(int**)(unit + 0x110);
// Health at descriptors+0x40
int health = *(int*)(descriptors + 0x40);
// Dead flag at descriptors+0x224 bit 5
uint deadFlag = (*(uint*)(descriptors + 0x224) >> 5) & 1;
bool isDead = (health < 1) || (deadFlag != 0);
CM2Model_ManageRenderListNode @ 0x00710b90
This function adds/removes models from the render list. ALL renderable models pass through here.
Signature:
void __thiscall CM2Model_ManageRenderListNode(void* model, int addToList)
// addToList = 1: add to render list
// addToList = 0: remove from render list
Called From:
- CGUnit_ShouldRender (units)
- ProcessActiveGameObjects (game objects)
- ProcessInactiveGameObjects
- RenderModelsWithAnimation
- And many more...
Proposed New Hook Strategy
Option 1: Hook CGUnit_ShouldRender
// In hook:
// 1. ECX = unit pointer
// 2. Check if unit's GUID matches tracked dead player
// 3. If match, store unit+0xD8 (model) in tracking set
// 4. Call original
// 5. In CM2SceneRenderDraw, match batch[1] to stored models
Option 2: Hook CM2Model_ManageRenderListNode
// In hook:
// 1. ECX = model pointer being added to render list
// 2. Check model+0x3C0 for owner object pointer
// 3. If owner is tracked dead player, store model pointer
// 4. Call original
// 5. In CM2SceneRenderDraw, match batch[1] to stored models
Option 2 is more comprehensive as it catches ALL models, including:
- Units (players, NPCs)
- Game objects
- Corpse objects (if they exist)
- Effects, particles, etc.
GUID Location on Objects
From FindObjectByGUID @ 0x00464530:
object + 0x30= GUID low (32-bit)object + 0x34= GUID high (32-bit)
Object Type Identification
Object types are identified by:
- GUID high bits contain type info
- Or object structure differences
For our purposes, we track dead players by GUID from Lua, so type identification happens at the Lua level.
Resolution
Model detection was solved by hooking DrawBatchProj (called before each model's draw batches) which provides the model pointer directly. Model-to-unit mapping is done by iterating game objects each frame and reading their model pointers at offset 0xD8.
Current Investigation: Finding Abuzee's Corpse Model (2024-12)
Test Subject
- Player Name: Abuzee
- GUID:
0x00000000003DAEE9(consistent across attempts) - Goal: Find where this GUID appears in model ownership so we can render an outline
Key Discovery: CGCorpse Objects Are Minimal
From GetObjectTypeSize @ 0x00465690:
switch(objectType) {
case 1: case 3: case 5: case 6: case 7: // Item, Unit(?), GO, DynObj, Corpse
return 0x18; // Only 24 bytes!
case 2: // Container
return 0xC0;
case 4: // Player
return 0x2F0;
}
Corpse objects (type 7) are only 0x18 bytes - just GUID and a few fields. They don't have model pointers at 0x88, 0xD8, etc.
Debug Log Analysis
=== Debug Log Initialized ===
[DeadOverlay] Player obj=0x32DD0008, isDead=0
[DeadOverlay] Corpse obj=0x3B060008 (no model - corpses are 0x18 bytes)
[DeadOverlay] Corpse obj=0x3B060370 (no model - corpses are 0x18 bytes)
[Track] Added GUID 0x00000000003DAEE9 name='Abuzee' (count=1)
[DeadOverlay] Player obj=0x3D0E0008, isDead=0
... (more players, all isDead=0)
[Hook] model=0x20A5B808 +0x3C0->0x3A090848('Unknown') +0x28->0x32DD0008('0x0000000000186B75') found='0x0000000000186B75' tracking=1
Analysis
- Abuzee's GUID (
0x003DAEE9) comes from corpse owner field (CORPSE_FIELD_OWNER at object+0x118) - Two corpse objects exist:
0x3B060008and0x3B060370- both are minimal 0x18-byte objects - All visible players have
isDead=0- none are dead CGUnit objects - The hook finds one model with owner GUID
0x00186B75(different player) - Abuzee's GUID never appears in the hook output
The Problem
When a player dies and releases spirit:
- Their CGUnit is despawned (removed from object list)
- A minimal CGCorpse object remains (just a marker for resurrection)
- The corpse visual (dead body lying on ground) must come from somewhere else
Abuzee has released spirit, so:
- No CGUnit exists with GUID
0x003DAEE9 - Only the minimal corpse marker exists
- But we can still see Abuzee's dead body in the game world
Hypotheses for Corpse Visual Rendering
Hypothesis A: Server-side model rendering The dead body might be rendered purely based on server data (model ID in corpse fields) without needing a client-side CGUnit.
Hypothesis B: Corpse has hidden model data The corpse object might store model info at different offsets than we're checking (not 0x88/0xD8).
Hypothesis C: Shared model system Dead body visuals might use a different rendering path that doesn't go through CM2Model_ManageRenderListNode.
Hypothesis D: Object data fields CORPSE_FIELD_DISPLAY_ID or similar might directly specify what to render.
UpdateFields for Corpses (from UpdateFields.h)
CORPSE_FIELD_OWNER = OBJECT_END + 0x00 // GUID of owner
CORPSE_FIELD_FACING = OBJECT_END + 0x02 // float
CORPSE_FIELD_POS_X = OBJECT_END + 0x03
CORPSE_FIELD_POS_Y = OBJECT_END + 0x04
CORPSE_FIELD_POS_Z = OBJECT_END + 0x05
CORPSE_FIELD_DISPLAY_ID= OBJECT_END + 0x06 // Model to display?
CORPSE_FIELD_ITEM = OBJECT_END + 0x07 // 19 slots
CORPSE_FIELD_BYTES_1 = OBJECT_END + 0x1A
CORPSE_FIELD_BYTES_2 = OBJECT_END + 0x1B
CORPSE_FIELD_GUILD = OBJECT_END + 0x1C
CORPSE_FIELD_FLAGS = OBJECT_END + 0x1D
CORPSE_FIELD_DYNAMIC_FLAGS = OBJECT_END + 0x1E
Key Ghidra Discovery: Corpses DO Have Models!
From InitializeObjectByType @ 0x00466010:
case 7: // Corpse type
ComplexObjectConstructor(param_2, ...);
SetupUnitDisplayHandler(param_2); // <-- Creates model at +0xD8!
Corpse objects call SetupUnitDisplayHandler which:
- Creates a model via
createModelAttachment() - Stores it at
corpse+0xD8viaSetDisplayHandler() - Calls
InitializeModelWithParameters(model, callback, corpse)which setsmodel+0x28 = corpse
This means corpses SHOULD have models at +0xD8 just like units!
Updated Code (dead_overlay.cpp)
Changed corpse handling to:
// Get model pointer from corpse+0xD8 (same as units)
void* corpseModelD8 = *(void**)((uint8_t*)obj + 0xD8);
void* corpseModelDC = *(void**)((uint8_t*)obj + 0xDC);
DebugLogF("[DeadOverlay] Corpse obj=0x%08X +0xD8=0x%08X +0xDC=0x%08X\n", ...);
// Track corpse model directly if available
if (corpseModelD8 != nullptr) {
ModelOutline_AddDeadPlayerModel(corpseModelD8);
}
Next Test
If corpse+0xD8 shows a valid model pointer:
- The model should appear in the CM2Model_ManageRenderListNode hook
- We can match by model pointer directly
model+0x28should point back to the corpse object
If corpse+0xD8 is NULL:
- Corpse visual might be rendered differently (e.g., server-side model ID in UpdateFields)
- Need to investigate CORPSE_FIELD_DISPLAY_ID usage
SUCCESS! Model Detection Working (2024-12)
Test Results:
[DeadOverlay] Corpse obj=0x3AFA8370 +0xD8=0x3AFBF008 +0xDC=0x00000000
[DeadOverlay] Adding corpse model 0x3AFBF008 for owner Abuzee (GUID=0x00000000003DAEE9)
[Hook] DIRECT MODEL MATCH! model=0x3AFBF008
What's Working:
- Corpse objects DO have model pointers at
+0xD8(not NULL) - Direct model pointer tracking works - we store
corpse+0xD8and match in hook - CM2Model_ManageRenderListNode hook detects when Abuzee's corpse model is being rendered
Next Step: Implement Outline Rendering Now that we can identify the corpse model during rendering, we need to:
- Modify render states when matched model is detected
- Re-render with depth test disabled and colored shader
- Create the visible-through-walls outline effect
Batch Type Discovery (RenderBatches @ 0x0070b630)
Critical Finding: Different Draw Functions per Batch Type
RenderBatches uses a switch statement to call different rendering functions based on batch type:
switch(*(uint32_t*)batch) { // batch[0] = type
case 0: DrawBatchProj(renderContext); // Projected/2D elements
case 1: DrawBatch(renderContext); // Standard 3D models
case 2: DrawBatchDoodad(...); // Doodads/world objects
case 3: DrawRibbon(renderContext); // Ribbon effects (trails)
case 4: DrawParticle(renderContext); // Particle systems
case 5: DrawCallback(renderContext); // Custom callback rendering
}
We only hooked DrawBatch (case 1) - corpse models might use a different batch type!
Draw Function Addresses
| Function | Address | Batch Type |
|---|---|---|
| DrawBatchProj | 0x0070cb30 | 0 |
| DrawBatch | 0x0070cf70 | 1 |
| DrawBatchDoodad | 0x0070d330 | 2 |
| DrawRibbon | 0x0070d820 | 3 |
| DrawParticle | 0x0070d8b0 | 4 |
| DrawCallback | 0x0070d960 | 5 |
Investigation Needed
- Determine corpse batch type - Add logging to RenderBatches to see what batch type corpse models use
- Hook the correct function - May need to hook a different draw function
- Consider hooking RenderBatches - Hook at a higher level to intercept all batch types
DXVK Considerations
Using DXVK (D3D9→Vulkan translation layer):
- DXVK intercepts at the D3D9 API level (IDirect3DDevice9 calls)
- WoW's internal functions (DrawBatch, RenderBatches) execute before D3D9 calls
- Internal function hooks should work normally - they're within WoW.exe address space
- DXVK only sees the D3D9 calls that result from these functions
Conclusion: DXVK shouldn't affect our internal WoW hooks. The issue is likely that corpse models use a batch type other than 1, so they go through a different draw function.
Render State Effects Discovered (2024-12)
See-Through-Models Effect (NOT through walls)
The following render states make a model visible through other models (players, NPCs, objects) but NOT through world geometry (walls, terrain):
// In DrawIndexedPrimitive hook, when rendering target model:
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE); // Disable Z-test
pDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); // Don't write to Z-buffer
pDevice->SetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_ARGB(255, 0, 255, 255)); // Cyan
pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
// IMPORTANT: Must save/restore states to avoid affecting other models
Key Insight: Disabling Z-test at DrawIndexedPrimitive level makes the model render on top of other models rendered in the same pass, but world geometry (walls/terrain) renders in a different pass with its own depth buffer management, so models behind walls are still occluded.
Use Case: This is useful for making specific models visible through crowds of NPCs/players without showing them through walls. Could be used for:
- Target highlighting in crowds
- Party member visibility
- Pet/companion tracking
True Through-Wall Visibility [SOLVED]
Solution: Render in EndScene with D3DRS_ZENABLE = FALSE. At EndScene, depth buffer is cleared, so disabling depth test allows through-wall visibility. Stencil buffer (on our custom D24S8 surface) handles body/outline separation.
Outline Rendering Approaches [SOLVED]
Goal
Add a colored outline around corpse models that is visible through walls.
Solution implemented: Custom vertex shader expands along normals after bone transform. Stencil buffer marks body, outline renders where stencil ≠ body.
Approach 1: Scaled Model Silhouette
Classic outline technique:
- Pass 1: Render model normally
- Pass 2: Scale model slightly larger (1.05x), cull front faces, render solid color
- The scaled back-faces create an outline effect around the normal model
// Pass 2: Outline
pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // Cull front faces (show back)
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE); // Through walls
// Scale transform... (complex - need access to world matrix)
Problem: Need access to the world/transform matrix to scale the model.
Approach 2: Wireframe Overlay
// Pass 2: Wireframe outline
pDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
pDevice->SetRenderState(D3DRS_TEXTUREFACTOR, g_outlineColor);
pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
Pros: Simple, no matrix manipulation needed Cons: Shows internal wireframe, not just outline edge
Approach 3: Edge Detection Post-Process
- Hook EndScene
- Read depth buffer and detect edges where depth changes sharply
- Draw outline at detected edges
Pros: True edge outline Cons: Complex, requires shaders, may have performance impact
Approach 4: Stencil Buffer Outline
- Render model to stencil buffer (increment)
- Render slightly enlarged model where stencil == 0 (edges only)
// Step 1: Write to stencil
pDevice->SetRenderState(D3DRS_STENCILENABLE, TRUE);
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS);
pDevice->SetRenderState(D3DRS_STENCILREF, 1);
pDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE);
// Render model normally...
// Step 2: Draw outline where stencil != 1
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL);
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
// Render enlarged model with outline color...
Problem: Still need transform matrix to enlarge model.
Current Recommendation
Start with Approach 2 (Wireframe) as it's simplest to implement:
- No transform matrix manipulation needed
- Already have the hook infrastructure
- Can be enhanced later with better techniques
Through-Wall Rendering Experiments (2024-12)
Findings
What Works:
- Disabling texture + setting emissive material → Model renders flat black (color control works)
- Face culling changes → Visible effect (can see body parts through other parts)
- Clearing depth buffer → Affects ground rendering (depth buffer IS accessible)
What Doesn't Work:
D3DRS_ZENABLE = FALSE→ Model still hidden by wallsD3DRS_ZFUNC = D3DCMP_ALWAYS→ Model still hidden by wallsD3DRS_ZWRITEENABLE = FALSE→ Model still hidden by walls- Clearing depth buffer before draw → Ground affected, but model unchanged
- World matrix scaling → No effect (WoW uses bone matrices, ignores D3DTS_WORLD)
- TEXTUREFACTOR color → No effect (WoW uses shaders, ignores fixed-function pipeline)
Key Insight: Render Order
WoW's rendering order appears to be:
- Models (characters, corpses, objects) - rendered first
- World geometry (terrain, walls) - rendered AFTER models
This means walls render on top of models regardless of our depth settings. Our depth modifications only affect how this model interacts with things rendered BEFORE it.
Why M2 Model Depth Settings Don't Work
WoW's M2 model rendering appears to:
- Use its own bone/transform matrices (ignoring D3DTS_WORLD)
- Use pixel shaders (ignoring fixed-function TEXTUREFACTOR)
- Have its depth handling managed at a higher level than individual DIP calls
The depth buffer clear test proved this: clearing the Z-buffer affected the ground (which uses standard depth) but NOT the M2 model (which has its own depth handling).
Solutions for True Through-Wall Visibility
Option A: EndScene Redraw (Complex)
- In DIP hook: Cache vertex buffer, index buffer, transforms when corpse is detected
- In EndScene: After all world rendering is complete, replay the cached draw calls with Z-disabled
- Challenge: Need to capture and replay all necessary state
Option B: Hook World Geometry Rendering
- Find where WoW renders walls/terrain
- Insert our corpse redraw AFTER wall rendering
- Challenge: Need to identify the right hook point
Option C: Enhanced 2D Overlay (Simple, Working)
- Keep current skull/name markers in EndScene
- Add screen-space indicators (arrows, distance, direction)
- Works reliably since 2D overlay renders after everything
Option D: Hybrid - World-Space Outline Sprite
- Create an outline/halo texture
- Render it in world-space at corpse position (like selection circles)
- Disable Z-buffer so it shows through walls
- Use WoW's BeginRender/EndRender pattern
Current Recommendation
For reliable through-wall corpse indication, use Option C (2D Overlay) as it:
- Already works (skull markers render on top of everything)
- Can be enhanced with arrows, distance text, directional indicators
- Doesn't require complex model caching/replay
For true 3D model silhouette through walls, Option A (EndScene Redraw) would be needed but is significantly more complex.
EndScene Replay Experiments (2024-12)
Attempt 1: Cache renderContext pointers
Approach:
- In DrawBatchProj hook, cache
renderContextpointers when corpse detected - In EndScene, call
DrawBatchProjagain with cached pointers
Result: Model pointers were NULL/invalid by EndScene time
Problem: renderContext is stack memory (always same address 0x00E2C7E0), gets reused between DrawBatchProj calls and is invalid by EndScene.
Attempt 2: Copy entire renderContext structure
Approach:
- Copy 0x3320 bytes of renderContext to static buffers
- In EndScene, pass copied buffer to DrawBatchProj
Result: CRASH at instruction 0x0070BB2F - memory reference to 0x3F72E8DF
Problem: The renderContext contains internal pointers to other stack-local structures (matrices, buffers, etc.). Copying the raw bytes preserves the pointers but they now point to invalid/reused stack memory. When DrawBatchProj dereferences them → crash.
The address 0x3F72E8DF looks like a float value (≈0.949) being interpreted as a pointer, confirming the copied structure has garbage where pointers should be.
Conclusion on EndScene Replay
EndScene replay of WoW's render functions is NOT viable because:
- Render contexts contain nested pointers to stack-local data
- Those structures are not relocatable via simple memory copy
- By EndScene time, original stack memory is reused
Alternative approaches needed:
- Find a simpler render function that takes just model pointer + transforms
- Use stencil buffer marking during normal render, draw overlay in EndScene
- Hook at a different point in the render pipeline (after walls but before present)
- Implement true outline shader technique at the D3D level
WoW 1.12.1 Rendering Pipeline Understanding
Render Order (from CGWorldFrame_OnWorldRender)
1. BeginRender()
2. RenderWorldMainLoop() - Main world terrain/geometry
3. RenderParticleSystemBatched()
4. executeSceneRenderPass(0) - Scene pass 0
5. RenderTargetingReticle()
6. RenderObjectList() - Objects (units, corpses, etc.)
7. executeSceneRenderPass(1/2) - More scene passes
8. CallWorldFunction_Wrapper()
9. renderBlizzard() - Blizzard logo?
10. EndRender()
Key Functions for M2 Model Rendering
| Function | Address | Purpose |
|---|---|---|
| CM2SceneRenderDraw | 0x0070b360 | Main M2 scene render, iterates batches |
| DrawBatchProj | 0x0070cb30 | Batch type 0 (projected/2D) - CORPSES USE THIS |
| DrawBatch | 0x0070cf70 | Batch type 1 (standard 3D) |
| DrawBatchDoodad | 0x0070d330 | Batch type 2 (doodads) |
| FinishRendering | 0x0070cb10 | Cleanup after rendering |
| CM2Model_ManageRenderListNode | 0x00710b90 | Add/remove models from render list |
M2 Model Internal Structures
WoW's M2 models use:
- Bone matrices for skeletal animation (not D3DTS_WORLD)
- Pixel shaders for rendering (not fixed-function TEXTUREFACTOR)
- Custom depth handling managed above DIP level
This is why D3D9 state changes at DrawIndexedPrimitive level don't affect M2 model depth:
- The D3D9 vtable hook works (we can see terrain effects)
- But M2 models use a different code path with their own state management
M2 Outline Research Findings (Agent)
1. Repository Analysis
UnitXP_SP3 (https://codeberg.org/konaka/UnitXP_SP3)
Purpose: World of Warcraft Vanilla 1.12 client modification toolkit focused on UI enhancements and rendering optimization.
Key Technical Components:
- Scene Rendering Hooks:
sceneBegin_sceneEnd.cpp/himplements scene lifecycle hooks for device lost logic and FPS management - Visual Effects:
worldText.cpp/hhandles floating combat text rendering with custom serif fonts - Distance Calculations:
distanceBetween.cppandmodernNameplateDistance.cpp/hfor unit/nameplate distance-based rendering - No Direct Outline Implementation: This repository focuses on UI overlays and text rendering, not 3D model outlining or silhouetting
Language Composition: C++ (78.5%), C (21.5%)
Relevance: Useful for understanding WoW 1.12.1 scene hook patterns but does not implement model outline techniques.
perf_boost (https://gitea.com/avitasia/perf_boost)
Purpose: Performance DLL using selective unit rendering with distance controls.
Key Features:
- Selective Unit Rendering: Distance-based culling for different unit types
- Fast distance approximation with frame-based caching
- Context-aware settings (combat vs non-combat, city vs outdoor)
- Smart exceptions for raid-marked units
Language Composition: C++ (96.4%), CMake (3.6%)
Architecture Notes:
- Uses boost and hadesmem libraries
- Implements render hooks but source code details not accessible via web fetch
Relevance: Repository README confirms it hooks rendering but implementation details require direct source inspection. Likely uses model culling rather than outline/silhouette techniques.
VanillaHelpers (https://github.com/isfir/VanillaHelpers)
Purpose: Helper library for Vanilla WoW 1.12 with display manipulation features.
Key Features:
- Minimap Blips: Customize unit markers on the minimap
- High-Resolution Textures: Support for 1024x1024 textures (vs standard 512x512)
- Character Morph: Change character appearances, mounts, and visible items via Lua API
- Display ID Manipulation:
SetUnitDisplayID(),RemapDisplayID(), etc.
Lua API Functions:
SetUnitMountDisplayID()/RemapMountDisplayID()SetUnitVisibleItemID()/RemapVisibleItemID()UnitDisplayInfo()GetItemDisplayID()
Relevance: This library manipulates what models are displayed but does not implement outline/glow effects. Focused on model swapping rather than rendering effects.
2. Technique Summary: D3D9 Model Outline Methods
Based on comprehensive web research, here are the viable techniques for rendering model outlines in DirectX 9:
Technique A: Stencil Buffer Two-Pass Outline
How It Works:
- First Pass: Render the model normally while writing a value (e.g., 1) to the stencil buffer
- Second Pass: Render the model slightly scaled up with stencil test set to only draw where stencil ≠ 1 (the edges)
D3D9 Implementation:
// Pass 1: Write to stencil
pDevice->SetRenderState(D3DRS_STENCILENABLE, TRUE);
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS);
pDevice->SetRenderState(D3DRS_STENCILREF, 1);
pDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE);
// Render model normally...
// Pass 2: Draw outline where stencil != 1
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL);
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE); // For through-walls visibility
// Render enlarged/scaled model with solid outline color...
Depth Buffer Format: Must use D3DFMT_D24S8 (24-bit depth + 8-bit stencil) to support stencil operations.
Pros:
- Clean outline edge detection
- Relatively efficient (two render passes)
- Standard technique used in many games
Cons:
- Requires model scaling/dilation (need transform matrix access)
- Simple uniform scaling doesn't work well for all mesh geometries
- For generic solution, mesh must be dilated (extruded along vertex normals), not just scaled
Sources:
- Microsoft Learn: Stencil Buffer Techniques
- Stack Overflow: Using stencil buffer in Direct3D
- Game Developer: Inside Direct3D Stencil Buffers
Technique B: Scaled Backface Silhouette (Inverted Hull)
How It Works:
- First Pass: Render model normally with standard culling
- Second Pass:
- Scale model slightly larger (e.g., 1.02-1.05x)
- Flip culling mode (cull front faces instead of back faces)
- Render with solid outline color
- The scaled back-faces create an outline around the normal model
D3D9 Implementation:
// Pass 1: Normal render
pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);
// Render model normally...
// Pass 2: Outline via scaled backfaces
pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // Cull front faces
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE); // Through walls
// Scale transform by 1.02-1.05x
// Render model with solid color shader
Vertex Shader Approach: Scale each vertex along its normal direction:
// In vertex shader
float outlineWidth = 0.02; // 2% larger
output.position = input.position + (input.normal * outlineWidth);
Pros:
- Very efficient (used in Guilty Gear Xrd for performance)
- Easy to control outline thickness via vertex color
- Both external AND internal outlines (around lips, eyes, etc.)
- Generally cheaper than post-processing
Cons:
- Requires vertex shader for proper normal extrusion
- WoW 1.12.1 M2 models use their own vertex shaders (bone animation)
- May need to inject/replace vertex shader
Sources:
- Game Dev Stack Exchange: How can I draw outlines around 3D models?
- RoveCoder: DirectX 11 Stencil Outline
- Imaginary Blend: Backface culling based outlines
Technique C: Edge Detection Post-Process
How It Works:
- First Pass: Render scene with normals/depth to separate render target
- Second Pass: Full-screen post-process applies edge detection filter (Sobel, etc.) to detect discontinuities
- Draw detected edges as outlines
Edge Detection Methods:
- Depth-based: Detect sharp changes in depth buffer
- Normal-based: Detect changes in surface normal direction
- Sobel filtering: Classic image processing edge detection
D3D9 Considerations:
- Requires Multiple Render Targets (MRT) or multiple passes
- D3D9 has limited MRT support (max 4 targets)
- Depth/stencil cannot be directly bound as texture in D3D9 (use separate R32F render target)
Pros:
- True edge-only outline (no wireframe artifacts)
- Can detect both external silhouette and internal feature lines
- No model manipulation needed
Cons:
- More complex implementation (requires shaders, render targets)
- Higher performance cost than two-pass methods
- D3D9 limitations make it more difficult than modern APIs
Sources:
- Medium: Three.js Post-processing outline Effect
- Ameye.dev: Edge Detection Outlines
- Stack Overflow: Multiple Render Targets in DirectX9
Technique D: Wireframe Overlay (Simple)
How It Works: Render the model twice:
- First Pass: Normal textured render
- Second Pass: Wireframe mode with thick lines in outline color
D3D9 Implementation:
// Pass 1: Normal render
// ... render model ...
// Pass 2: Wireframe outline
pDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE); // Through walls
pDevice->SetRenderState(D3DRS_TEXTUREFACTOR, outlineColor);
pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
// Render model again in wireframe...
Pros:
- Extremely simple to implement
- No matrix manipulation or shaders needed
- Works immediately with existing hooks
Cons:
- Shows internal wireframe triangles, not just outer silhouette
- Visual quality inferior to proper outline techniques
- May appear cluttered on high-poly models
Relevance: Good for quick prototyping but not production-quality.
Technique E: Shader Injection/Replacement
How It Works:
Hook D3D9 SetVertexShader and SetPixelShader calls to inject custom shaders that:
- Extrude vertices along normals for outline pass
- Apply solid colors or special effects
- Bypass depth testing for through-wall visibility
D3D9 Hook Points:
IDirect3DDevice9::CreateVertexShader(vtable index varies)IDirect3DDevice9::SetVertexShader(vtable index varies)IDirect3DDevice9::SetPixelShader(vtable index varies)IDirect3DDevice9::SetVertexShaderConstantF- modify shader parameters
Custom Shader Workflow:
- Hook
SetVertexShaderorSetPixelShader - Detect when WoW's M2 model shaders are being set
- Replace with custom compiled shader that adds outline effect
- Use
D3DXCompileShader()to compile HLSL at runtime
Pros:
- Full control over rendering behavior
- Can implement sophisticated effects (rim lighting, Fresnel, etc.)
- Works within existing render pipeline
Cons:
- Complex - requires understanding WoW's shader system
- Must maintain compatibility with bone animation system
- May break with DXVK or other translation layers
- Requires reverse-engineering WoW's shader constants/inputs
Sources:
- Microsoft Learn: Using Shaders in Direct3D 9
- Microsoft Learn: IDirect3DDevice9::SetVertexShaderConstantF
- CodePal: Direct3D Hooks in C++
3. Recommended Approach for WoW 1.12.1 M2 Models
Given the specific constraints of your project:
- WoW 1.12.1 M2 models use custom vertex shaders for bone animation
- M2 models ignore standard D3D9 render states (D3DRS_ZENABLE, D3DRS_TEXTUREFACTOR, etc.)
- Models render BEFORE world geometry (walls occlude even with Z-disabled)
- You already have DrawIndexedPrimitive hook infrastructure
Primary Recommendation: Stencil Buffer + DrawIndexedPrimitive Hook
Implementation Strategy:
-
Hook DrawIndexedPrimitive (D3D9 vtable index 82)
- Already have vtable patching infrastructure from EndScene hook
- Can identify corpse models by comparing against tracked model pointers
-
Two-Pass Rendering with Stencil:
HRESULT WINAPI hkDrawIndexedPrimitive(
IDirect3DDevice9* pDevice,
D3DPRIMITIVETYPE Type,
INT BaseVertexIndex,
UINT MinVertexIndex,
UINT NumVertices,
UINT StartIndex,
UINT PrimitiveCount)
{
// Check if this model belongs to tracked corpse
bool isCorpseModel = IsTrackedCorpseModel();
if (isCorpseModel) {
// PASS 1: Render to stencil buffer
DWORD oldStencilEnable, oldStencilFunc, oldStencilRef, oldStencilPass;
pDevice->GetRenderState(D3DRS_STENCILENABLE, &oldStencilEnable);
pDevice->GetRenderState(D3DRS_STENCILFUNC, &oldStencilFunc);
pDevice->GetRenderState(D3DRS_STENCILREF, &oldStencilRef);
pDevice->GetRenderState(D3DRS_STENCILPASS, &oldStencilPass);
pDevice->SetRenderState(D3DRS_STENCILENABLE, TRUE);
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS);
pDevice->SetRenderState(D3DRS_STENCILREF, 1);
pDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE);
// Render model normally (writes to stencil)
oDrawIndexedPrimitive(pDevice, Type, BaseVertexIndex, MinVertexIndex,
NumVertices, StartIndex, PrimitiveCount);
// PASS 2: Render outline where stencil == 0 (edges only)
// Problem: Need to render scaled/enlarged model here
// WoW's transform matrices are managed internally
// May need to use wireframe as simpler alternative
// Restore stencil states
pDevice->SetRenderState(D3DRS_STENCILENABLE, oldStencilEnable);
pDevice->SetRenderState(D3DRS_STENCILFUNC, oldStencilFunc);
pDevice->SetRenderState(D3DRS_STENCILREF, oldStencilRef);
pDevice->SetRenderState(D3DRS_STENCILPASS, oldStencilPass);
return D3D_OK;
}
return oDrawIndexedPrimitive(pDevice, Type, BaseVertexIndex, MinVertexIndex,
NumVertices, StartIndex, PrimitiveCount);
}
Why This Approach:
- Works at D3D9 API level (after WoW's internal rendering)
- Can identify corpse models via your existing model tracking
- Stencil buffer approach proven to work in D3D9
- Doesn't require understanding WoW's internal transform matrices
Remaining Challenge: Model Scaling for Outline Pass
The stencil approach requires rendering a slightly enlarged version of the model in pass 2. Options:
Option 1: Wireframe Instead of Scaled Model
- Use
D3DRS_FILLMODE = D3DFILL_WIREFRAMEfor pass 2 - Simpler, no scaling needed
- Quality is lower but functional
Option 2: Capture and Modify Vertex Buffer
- In pass 1, capture vertex buffer pointer via
GetStreamSource - Lock vertex buffer, scale vertices along calculated normals
- Render modified vertices in pass 2
- Restore original vertex buffer
- Complex but provides true outline
Option 3: Vertex Shader Injection
- Create custom vertex shader that extrudes vertices along normals
- Inject via
SetVertexShaderhook when rendering corpse - Must preserve WoW's bone animation inputs
- Most technically sophisticated
4. Implementation Notes
A. Setting Up Stencil Buffer
Ensure Depth/Stencil Surface Format:
// During device initialization or reset
D3DPRESENT_PARAMETERS d3dpp;
d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; // 24-bit depth, 8-bit stencil
d3dpp.EnableAutoDepthStencil = TRUE;
Clearing Stencil at Frame Start:
// In BeginScene or frame start
pDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL,
0, 1.0f, 0);
B. Handling Through-Wall Visibility
The key issue discovered: M2 models render BEFORE world geometry, so disabling depth test at DIP level doesn't help.
Solution: Post-Render Pass in EndScene
// In EndScene hook (after all rendering complete)
void hkEndScene(IDirect3DDevice9* pDevice) {
// World geometry has been rendered
// Now re-render corpse models with Z-disabled
for (auto& corpseModel : g_trackedCorpseModels) {
// Set states for through-wall rendering
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
pDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
// Render cached corpse geometry here
// Challenge: Need to cache vertex/index buffers during normal render
}
// Call original EndScene
oEndScene(pDevice);
}
Caching Geometry During Normal Render:
// In DrawIndexedPrimitive hook
if (isCorpseModel) {
// Cache rendering parameters
IDirect3DVertexBuffer9* pVB;
UINT offset, stride;
pDevice->GetStreamSource(0, &pVB, &offset, &stride);
IDirect3DIndexBuffer9* pIB;
pDevice->GetIndices(&pIB);
// Store for later replay in EndScene
CacheCorpseGeometry(pVB, pIB, BaseVertexIndex, MinVertexIndex,
NumVertices, StartIndex, PrimitiveCount, Type);
}
C. Identifying Corpse Models in DrawIndexedPrimitive
You have model tracking via ModelOutline_AddDeadPlayerModel(). Need to correlate DIP calls:
Method 1: Model Pointer Correlation
- In WoW's internal hooks, store model memory address
- In DIP, check if current vertex buffer belongs to that model
- Difficult: No direct mapping from VB to model
Method 2: Stride/Count Fingerprinting
- Log stride, vertex count, primitive count for known corpse models
- Use these signatures to identify corpse draws
- Empirical but effective
Method 3: Render State Markers
- In WoW internal hook, set a unique render state when corpse model starts rendering
- In DIP, check for that marker state
- Example:
SetRenderState(D3DRS_TEXTUREFACTOR, CORPSE_MARKER_VALUE)
D. Code Pattern: Complete DIP Hook with Stencil Outline
HRESULT WINAPI hkDrawIndexedPrimitive(
IDirect3DDevice9* pDevice,
D3DPRIMITIVETYPE Type,
INT BaseVertexIndex,
UINT MinVertexIndex,
UINT NumVertices,
UINT StartIndex,
UINT PrimitiveCount)
{
// Save all states we'll modify
DWORD oldStencilEnable, oldStencilFunc, oldStencilRef;
DWORD oldStencilPass, oldFillMode, oldZEnable;
pDevice->GetRenderState(D3DRS_STENCILENABLE, &oldStencilEnable);
pDevice->GetRenderState(D3DRS_STENCILFUNC, &oldStencilFunc);
pDevice->GetRenderState(D3DRS_STENCILREF, &oldStencilRef);
pDevice->GetRenderState(D3DRS_STENCILPASS, &oldStencilPass);
pDevice->GetRenderState(D3DRS_FILLMODE, &oldFillMode);
pDevice->GetRenderState(D3DRS_ZENABLE, &oldZEnable);
// Check if this is a corpse model
bool isCorpse = IsTrackedCorpseModel(pDevice);
if (isCorpse) {
// PASS 1: Normal render + stencil write
pDevice->SetRenderState(D3DRS_STENCILENABLE, TRUE);
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS);
pDevice->SetRenderState(D3DRS_STENCILREF, 1);
pDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE);
oDrawIndexedPrimitive(pDevice, Type, BaseVertexIndex, MinVertexIndex,
NumVertices, StartIndex, PrimitiveCount);
// PASS 2: Wireframe outline where stencil != 1
pDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL);
pDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE); // Try for through-walls
// Set outline color
pDevice->SetRenderState(D3DRS_TEXTUREFACTOR, D3DCOLOR_ARGB(255, 255, 0, 255));
pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
oDrawIndexedPrimitive(pDevice, Type, BaseVertexIndex, MinVertexIndex,
NumVertices, StartIndex, PrimitiveCount);
// Restore states
pDevice->SetRenderState(D3DRS_STENCILENABLE, oldStencilEnable);
pDevice->SetRenderState(D3DRS_STENCILFUNC, oldStencilFunc);
pDevice->SetRenderState(D3DRS_STENCILREF, oldStencilRef);
pDevice->SetRenderState(D3DRS_STENCILPASS, oldStencilPass);
pDevice->SetRenderState(D3DRS_FILLMODE, oldFillMode);
pDevice->SetRenderState(D3DRS_ZENABLE, oldZEnable);
return D3D_OK;
}
// Normal rendering for non-corpse models
return oDrawIndexedPrimitive(pDevice, Type, BaseVertexIndex, MinVertexIndex,
NumVertices, StartIndex, PrimitiveCount);
}
5. Reference Links
Repository Analysis:
Stencil Buffer Techniques:
- Microsoft Learn: Stencil Buffer Techniques (Direct3D 9)
- Stack Overflow: Using stencil buffer in Direct3D
- Game Developer: Inside Direct3D Stencil Buffers
- LearnOpenGL: Stencil testing (OpenGL but concepts translate)
Outline Rendering Techniques:
- Game Dev Stack Exchange: How can I draw outlines around 3D models?
- RoveCoder: DirectX 11 Stencil Outline (DX11 but technique applies)
- Imaginary Blend: Backface culling based outlines
- Ameye.dev: Edge Detection Outlines
D3D9 Shader and Hooking:
- Microsoft Learn: Using Shaders in Direct3D 9
- Microsoft Learn: IDirect3DDevice9::SetPixelShader
- Microsoft Learn: IDirect3DDevice9::SetVertexShaderConstantF
- CodePal: Direct3D Hooks in C++
- Stack Overflow: D3D9 Hooking (EndScene + DrawIndexedPrimitive)
Depth and Render States:
- Microsoft Learn: D3DRENDERSTATETYPE enumeration
- Microsoft Learn: Depth Buffering State (Direct3D 9)
- Microsoft Learn: Changing Depth Buffer Comparison Functions
WoW-Specific:
Summary [OUTDATED - See "Working Implementation" at end]
This section documented early research plans. The final implementation is different. See "Working Implementation: Shader-Based Outline System" for the actual solution.
What was actually implemented:
- Custom vertex shader with full bone transforms + normal expansion
- Stencil-based rendering in EndScene (not wireframe)
- Create our own D24S8 depth/stencil surface (WoW's is D24X8)
- Per-category effects: dark halo for dead players, colored outlines for raid marks/targets
Solid Color Rendering Technique (WORKING - 2024-12)
Discovery: Back-Face Culling with Z-Disabled
Successfully implemented solid color rendering of corpse models visible through walls using:
- Geometry caching at DIP level - Store vertex/index buffers and shader constants
- EndScene replay - Re-render cached geometry after all world geometry
- Back-face culling - Cull front faces (D3DCULL_CW), render only back faces
- Z-disabled - Model visible through walls
Key Code Pattern:
// In ReplayCorpseOutlines (EndScene):
// Disable depth test - visible through walls
pDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
pDevice->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);
// Cull FRONT faces - only render back faces
pDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
// Solid color via texture factor (requires disabling pixel shader)
pDevice->SetPixelShader(NULL); // CRITICAL: Must disable WoW's pixel shader
pDevice->SetRenderState(D3DRS_TEXTUREFACTOR, solidColor);
pDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
pDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
// Re-render with cached vertex shader and bone matrices
pDevice->SetVertexShader(draw.pVertexShader);
pDevice->SetVertexShaderConstantF(0, draw.VSConstants, draw.VSConstantCount);
g_oDrawIndexedPrimitive(pDevice, ...);
Why This Works:
- M2 models use VERTEX shaders for bone animation (must keep these)
- M2 models use PIXEL shaders for texturing (must DISABLE these for solid color)
- Disabling pixel shader allows TEXTUREFACTOR to work
- Back-face culling shows back surfaces which are visible at silhouette edges
- EndScene replay happens AFTER walls render, so Z-disable works for through-wall
Result: Corpse renders as solid cyan color visible through walls.
Limitation: Without scaling, back faces cover the entire model instead of just the edges. Need to scale the model slightly larger so back faces only show around the silhouette.
Resolution: Custom Vertex Shader
Matrix scaling approaches didn't work well. Final solution uses a custom vertex shader that:
- Performs full bone transforms (same as WoW's shader)
- Expands vertices along their normals by a configurable thickness
- Uses stencil buffer to prevent outline from covering body
See "Working Implementation: Shader-Based Outline System" for shader code.
Shader Constant Analysis (2024-12)
Logged Vertex Shader Constants (c0-c7)
From corpse model rendering at DrawBatchProj:
c0: 0.000 0.000 0.000 0.000 <- Unused/zero
c1: 0.000 0.000 0.000 0.000 <- Unused/zero
c2: 1.054 0.000 0.000 0.000 <- X scale (aspect/FOV related)
c3: 0.000 1.874 0.000 0.000 <- Y scale (aspect/FOV related)
c4: 0.000 0.000 1.000 -0.080 <- Z with small offset (-0.08)
c5: 0.000 0.000 1.000 0.000 <- Z identity
c6: 1.000 0.000 0.000 0.000 <- X identity
c7: 0.000 1.000 0.000 0.000 <- Y identity
Key Observations
- NOT a standard view-projection matrix - Values are sparse scale/offset factors
- c2/c3 are aspect ratio scales - 1.054 and 1.874 relate to screen aspect ratio and FOV
- c4 has Z offset - The -0.080 suggests depth bias or near plane offset
- c6/c7 are identity-like - Possibly additional coordinate transforms
Scaling Experiment Results
Attempted: Scale c0.x, c1.y, c2.x, c3.y by g_outlineThickness (1.2x)
Result:
- Model silhouette appears larger [OK]
- But silhouette MOVES when view angle changes [X]
- Silhouette covers corpse instead of outlining it [X]
Root Cause: Scaling these projection-space constants distorts the view transformation, not the model. The model center isn't at origin in view space, so scaling pushes it in different directions based on camera angle.
Why Back-Face Culling Alone Doesn't Create Outlines
The back-face culling technique requires:
- Model A rendered at normal scale (front faces visible)
- Model B rendered SCALED LARGER (back faces only)
- The back faces of B extend beyond A's silhouette = visible outline
Current problem: We're not scaling the model in world space. We're distorting the view-projection, which:
- Shifts model position (not centered at origin)
- Doesn't uniformly enlarge in screen space
- Creates a moving silhouette, not an outline
Correct Scaling Requirements
For proper outline, need to scale vertices from the model's center point in world space:
scaled_vertex = model_center + (vertex - model_center) * scale_factor
This requires knowing model_center - the centroid of the model in world space.
Outline Approach Options Analysis
Option 1: Find Model Center from WoW Memory
Concept: Read the model's world-space position from WoW's object memory, use it as scale center.
Implementation:
- From corpse object, get position at
corpse + 0x9E8or viaGetUnitPosition(0x00606F50) - Pass model center to custom vertex shader via constants (c8+)
- In shader:
scaled = center + (vertex - center) * scale - Transform scaled vertex with original view-projection
Shader Code (vs_1_1):
vs_1_1
dcl_position v0
; c0-c7 = original WoW constants
; c8.xyz = model center (world space)
; c9.x = scale factor
; Offset vertex from center
sub r0.xyz, v0.xyz, c8.xyz ; r0 = vertex - center
mul r0.xyz, r0.xyz, c9.x ; r0 = (vertex - center) * scale
add r0.xyz, r0.xyz, c8.xyz ; r0 = center + scaled_offset
mov r0.w, v0.w ; preserve W
; Apply original transform
dp4 oPos.x, r0, c0
dp4 oPos.y, r0, c1
dp4 oPos.z, r0, c2
dp4 oPos.w, r0, c3
Pros:
- Correct mathematical approach
- Controllable thickness via scale factor
- Works with existing bone animation (vertices already skinned)
Cons:
- Requires reading model position from WoW memory
- Need to understand WoW's coordinate system
- Shader must match WoW's expected inputs
Thickness Control: Direct - scale factor of 1.02 = 2% larger = thin outline, 1.10 = thick outline
Option 3: Post-Process Edge Detection
Concept: Instead of scaling geometry, detect edges in the rendered image using depth/normal discontinuities.
Implementation:
- During corpse rendering, write depth to a separate render target
- In EndScene, run edge detection shader on depth buffer
- Draw detected edges as colored outline
Edge Detection Methods:
A. Sobel Filter on Depth:
// Sample depth buffer at 8 neighbors
float depthL = tex2D(depthSampler, uv + float2(-1, 0) * texelSize).r;
float depthR = tex2D(depthSampler, uv + float2(+1, 0) * texelSize).r;
// ... etc
// Compute Sobel gradient magnitude
float edge = length(sobelX) + length(sobelY);
B. Roberts Cross on Depth: Simpler 2x2 kernel, faster but less accurate.
C. Normal-based Edge Detection: Requires rendering normals to a texture - more complex setup.
D3D9 Implementation Challenges:
- Cannot directly read depth buffer as texture in D3D9
- Need to render depth to a R32F render target in first pass
- Requires pixel shader 2.0+ for edge detection
- May need Multiple Render Targets (MRT) or extra passes
Pros:
- True edge-only outline (no filled silhouette)
- Detects internal feature lines (eyes, armor details)
- No geometry manipulation needed
Cons:
- More complex (requires render targets, shaders)
- Higher performance cost
- D3D9 limitations (can't sample depth buffer directly)
- Outline is screen-space pixels, not world-space thickness
Thickness Control: Indirect - controlled by texel sampling distance. Harder to make consistent across distances.
Recommendation
Option 1 (Model Center Scaling) is more suitable because:
- Controllable world-space thickness
- Works with existing EndScene replay infrastructure
- Simpler implementation (single shader modification)
- Consistent outline regardless of distance
Option 3 (Post-Process) would require:
- Additional render target creation
- Multiple rendering passes
- New shader infrastructure
- Solving D3D9 depth buffer limitations
Coordinate Space Discovery (2024-12)
Key Finding: Vertices are in MODEL SPACE, not World Space
From vertex buffer analysis during draw call caching:
Model center (from vertex centroid): 0.03, -0.00, 2.00
This is model-local coordinates - the 2.0 Z value is approximately chest height on a humanoid character. The vertices are NOT pre-transformed to world space.
Shader Constants Contain World Transform Data
c9: -0.000, -1000.000, 500.000, -0.000 <- WORLD POSITION!
c10-c15: varying values per frame <- Bone matrices / animation
The c9 values (-1000, 500) are typical WoW world coordinates. This confirms:
- c2-c7: View-projection transform (aspect ratio, FOV, depth)
- c8-c9: World position / transform data
- c10-c15: Bone matrices (change with animation)
Transform Chain
WoW's vertex shader performs:
Model Space → (bone transforms c10-c15) → (world offset c8-c9) → (view-proj c2-c7) → Clip Space
Why Custom Shader Failed
The custom shader only implemented the last stage:
mul oPos.x, r0.x, c2.x // View-proj X
mul oPos.y, r0.y, c3.y // View-proj Y
This skipped the world positioning, causing the model to render at screen origin (top-down view on monitor).
Current Status
Using WoW's original shader with all cached constants renders the silhouette correctly positioned over the corpse, but without scaling (no outline effect yet).
Scaling Options Going Forward
Option A: Scaled Vertex Buffer Copy
- Create a dynamic vertex buffer during caching
- Copy vertices with positions scaled from model-space center
- Use scaled VB for outline pass, original VB for normal render
- Pro: Works with WoW's full transform chain
- Con: Memory overhead, VB creation per frame
Option B: Modify Cached Vertex Data In-Place
- Lock original VB and scale positions temporarily
- Render outline pass
- Restore original positions
- Pro: No extra memory
- Con: May cause visual glitches if timing is wrong
Option C: Inject Scale into Bone Matrices
- Identify which constants are bone matrices (c10-c15?)
- Multiply bone matrices by scale factor
- Pro: Cleaner than VB modification
- Con: Requires understanding bone matrix layout
Option D: Post-Process Edge Detection
- Render corpse to stencil/depth buffer
- Detect edges via pixel shader
- Pro: True edge-only outline
- Con: Complex, D3D9 limitations
Implementation Progress: Scaled Vertex Buffer Approach (2024-12) [SUPERSEDED]
Note: This approach was superseded by the shader-based stencil system. Key issues: Scaled VB didn't handle bone animations, created artifacts. See "Working Implementation: Shader-Based Outline System" for the final solution.
Lessons Learned
-
Stencil IS available: WoW uses D3DFMT_D24X8 (no stencil), but we can create our own D3DFMT_D24S8 surface and swap it in during EndScene.
-
Shader-based expansion is superior: Instead of pre-scaling vertex buffers:
- Custom vertex shader expands along normals in world space
- Properly handles bone animations via same bone transform as WoW
- No need to cache/create scaled VBs
-
Full bone transform required: WoW's M2 models use skeletal animation. Any outline expansion must happen AFTER bone transforms, not in model space.
Critical Discovery: Depth Buffer Cleared at EndScene (2024-12)
Problem
When rendering corpse body in EndScene with depth testing enabled:
D3DCMP_LESSEQUAL: Body never renders (fails everywhere)D3DCMP_GREATEREQUAL: Body always renders (passes everywhere)D3DCMP_ALWAYS: Body renders (as expected)
Analysis
This behavior indicates the depth buffer is cleared to 0 (near plane) before EndScene:
- If depth buffer = 0 everywhere:
- Body depth > 0, so LESSEQUAL (body ≤ 0) fails
- Body depth > 0, so GREATEREQUAL (body ≥ 0) passes
- GREATER (body > 0) also passes
Implications
- Cannot use depth buffer at EndScene for wall occlusion testing
- Scene depth information is lost by the time EndScene is called
- Depth bias has no effect - buffer is all zeros regardless of bias value
The Fundamental Conflict
Requirements:
- Silhouette visible through walls → must render AFTER walls (EndScene)
- Body covers silhouette → body must render AFTER silhouette
- Walls occlude body → body must use depth test against walls
But at EndScene, wall depth info is gone. We cannot satisfy all three requirements.
Attempted Solutions
| Approach | Result |
|---|---|
| EndScene: silhouette (depth off) + body (depth on) | Body fails depth test everywhere |
| EndScene: silhouette (GREATER) | Shows everywhere (buffer is 0) |
| Depth bias to push silhouette forward | No effect (buffer is 0) |
| Let body render in main pass, silhouette in EndScene | Silhouette covers body |
Potential Solutions
- Find hook point after walls but before depth clear - Need to identify where WoW clears depth
- Render silhouette in DIP hook - But then walls cover silhouette (no through-wall)
- Accept visual compromise - Semi-transparent outline that shows over body slightly
Resolution
The stencil buffer solved this problem:
- Mark body pixels in stencil (pass 1) - no color write
- Render dark halo/outline where stencil ≠ body (pass 2) - creates outline effect
- Depth buffer not needed - stencil provides the "body vs outline" distinction
See "Working Implementation: Shader-Based Outline System" below for details.
Working Implementation: Shader-Based Outline System (2024-12)
Overview
The final working system uses a custom vertex shader that performs full bone transforms, combined with stencil-based rendering in EndScene for proper layering and through-wall visibility.
Architecture
Frame Render Order:
1. WoW renders world geometry (terrain, walls, objects)
2. DrawIndexedPrimitive hook detects corpse/target/raid-marked models
3. Draw calls are cached with full D3D state
4. EndScene hook replays cached draws with stencil-based outline rendering
Key Components
1. Draw Call Caching (CacheCorpseDrawCall)
When a tracked model is detected during rendering, we cache:
- Primitive parameters (type, indices, vertex count)
- Vertex buffer, index buffer, vertex declaration
- Vertex shader and all 256 VS constants (bone matrices!)
- Pixel shader and PS constants
- Transforms (world, view, projection)
- Texture state
- Model category (TARGET, RAID_MARKED, DEAD_PLAYER)
- Calculated outline thickness based on distance
2. WoW M2 Vertex Format
WoW's character models use this vertex layout:
Offset 0x00: POSITION (float3) - Local vertex position
Offset 0x0C: BLENDWEIGHT (D3DCOLOR) - 4 bone weights (normalized 0-255)
Offset 0x10: BLENDINDICES (D3DCOLOR) - 4 bone indices (0-255)
Offset 0x14: NORMAL (float3) - Vertex normal
D3DCOLOR stores 4 bytes as ARGB (0xAARRGGBB), but when read as blend weights/indices:
- Component order in shader is .zyxw (BGRA swizzle)
- Bone weights are normalized (0-255 → 0.0-1.0)
- Bone indices are raw byte values
3. Bone Matrix Storage
WoW stores bone matrices in vertex shader constants starting at c31:
- Each bone uses 3 consecutive float4 constants (4x3 matrix rows)
- Bone N is at: c[N3 + 31], c[N3 + 32], c[N*3 + 33]
- To convert bone index to constant offset: index * 765 (when indices are 0-1 normalized)
Example: Bone index 0 → constants c31, c32, c33 Bone index 1 → constants c34, c35, c36
4. Custom Outline Vertex Shader
The shader performs full skeletal animation then expands along normals:
vs_2_0
dcl_position v0 ; position (float3)
dcl_blendweight v2 ; blend weights (D3DCOLOR normalized)
dcl_blendindices v3 ; blend indices (D3DCOLOR)
dcl_normal v1 ; normal (float3)
; Convert blend indices to bone constant offsets
; v3 components are 0-1 (D3DCOLOR normalized), multiply by 765 to get bone index * 3
; WoW uses zyxw swizzle order (BGRA)
mul r0.xyz, v3.zyxw, c251.x ; indices * 765
mova a0.xyz, r0 ; move to address register for indexed access
; First bone row (transforms to get world X)
mul r0, v2.y, c[a0.y + 31]
mad r0, c[a0.x + 31], v2.z, r0
mad r0, c[a0.z + 31], v2.x, r0
dp3 r3.x, r0, v1 ; normal.x after bone transform
dp4 r4.x, r0, v0 ; position.x after bone transform
; Second bone row (world Y)
mul r1, v2.y, c[a0.y + 32]
mad r1, c[a0.x + 32], v2.z, r1
mad r1, c[a0.z + 32], v2.x, r1
dp3 r3.y, r1, v1 ; normal.y
dp4 r4.y, r1, v0 ; position.y
; Third bone row (world Z)
mul r2, v2.y, c[a0.y + 33]
mad r2, c[a0.x + 33], v2.z, r2
mad r2, c[a0.z + 33], v2.x, r2
dp3 r3.z, r2, v1 ; normal.z
dp4 r4.z, r2, v0 ; position.z
; Now r4.xyz = world-space position, r3.xyz = world-space normal (unnormalized)
nrm r5.xyz, r3 ; normalize the normal
; Expand position along normal for outline effect
mul r6.xyz, r5.xyz, c250.x ; normal * thickness (c250.x)
add r4.xyz, r4.xyz, r6.xyz ; position += normal_offset
mov r4.w, c251.y ; w = 1.0
; Apply view-projection matrix (c2-c5 in WoW's constants)
dp4 oPos.x, c2, r4
dp4 oPos.y, c3, r4
dp4 oPos.z, c4, r4
dp4 oPos.w, c5, r4
Shader constants:
- c0-c249: WoW's original constants (bone matrices, transforms, lighting)
- c250.x: Outline thickness (world units)
- c251: Helper constants (765.0, 1.0, 0.0, 0.0)
The mova instruction is critical - it allows indexed access to bone matrix constants based on per-vertex bone indices.
5. Stencil-Based Rendering (EndScene)
Three-pass rendering for proper outline effect:
Pass 1: Mark body in stencil buffer
SetRenderState(D3DRS_STENCILENABLE, TRUE);
SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS);
SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE);
SetRenderState(D3DRS_STENCILREF, 1);
SetRenderState(D3DRS_COLORWRITEENABLE, 0); // No color, stencil only
// Render body at original size → marks stencil = 1 where body is
Pass 2: Dark halo (dead players only)
SetRenderState(D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL); // Not where body is
SetRenderState(D3DRS_STENCILREF, 1);
SetRenderState(D3DRS_TEXTUREFACTOR, 0x80000000); // 50% alpha black
// Render with shader at 4x thickness → dark halo around body
Pass 3: Bright outline (raid marks, targets)
SetRenderState(D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL);
SetRenderState(D3DRS_STENCILREF, 1);
SetRenderState(D3DRS_TEXTUREFACTOR, outlineColor); // Per-model color
// Render with shader at normal thickness → colored outline
All passes use D3DRS_ZENABLE = FALSE for through-wall visibility.
6. Per-Category Outline Handling
Three model categories with different visual treatment:
| Category | Base Thickness | Min | Max | Effect |
|---|---|---|---|---|
| TARGET | 0.08 | 0.06 | 0.375 | White outline |
| RAID_MARKED | 0.05 | 0.04 | 0.375 | Colored outline (matches marker) |
| DEAD_PLAYER | 0.02 | 0.02 | 0.15 | Dark halo (4x thickness, 50% black) |
Thickness scales with distance to maintain consistent screen-space size:
float scale = distance / OUTLINE_REFERENCE_DISTANCE; // 20 yards reference
float thickness = baseThickness * scale;
// Then clamp to per-category min/max
Target has larger minimum than raid marks so it's more prominent at close range, but converges to same maximum at long range.
7. Custom Depth/Stencil Surface
WoW uses D3DFMT_D24X8 (no stencil). We create our own D3DFMT_D24S8 surface:
pDevice->CreateDepthStencilSurface(width, height, D3DFMT_D24S8, ...);
// Swap in our surface for stencil passes, restore original after
Model Detection
Models are tracked via:
- Dead players: GUIDs added when unit has dead flag, removed when alive
- Raid marks: Model pointers added each frame for units with raid target icons
- Current target: Model pointer updated each frame from wow_get_target()
Self-outline prevention: Local player is excluded from target and raid mark outlines.
Files
d3d9_hook.cpp: EndScene/DIP hooks, stencil rendering, shader creationmodel_outline_hook.cpp: Model tracking, color/category lookups, thickness valuesdead_overlay.cpp: Frame update logic, populates model tracking from game state
Status
- [OK] Full bone transform shader working
- [OK] Stencil-based outline rendering
- [OK] Per-category visual effects (outline vs dark halo)
- [OK] Distance-based thickness scaling with min/max clamping
- [OK] Through-wall visibility
- [OK] Self-outline prevention