Files
MarcelineVQ 69051fd486 Repo hygiene: drop internal planning files, clean docs for publication
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.
2026-08-01 18:03:46 -07:00

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 handler
  • SetTargetHighlight @ 0x00614550 - Sets highlight on target, calls SetModelAmbientColor
  • EnableTargetHighlight @ 0x004945e0 - Enables highlight, calls SetTargetHighlight
  • UnitHighlightWrapper @ 0x00492e70 - Wrapper for HandleUnitHighlight
  • RenderUnitSelectionIndicator @ 0x00611ff0 - Renders selection circle under units
  • SetupSelectionRenderStates @ 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 rendering
  • DrawBatch @ 0x0070cf70 - Draws model batches
  • DrawBatchDoodad @ 0x0070d330 - Draws doodad batches
  • SetModelAlpha @ 0x00710da0 - Sets model alpha (writes to offset 0x1c4)
  • SetModelAmbientColor @ sets RGB at offsets 0x190, 0x194, 0x198

Render State Functions:

  • SetRenderState @ wraps D3D_SetRenderState
  • D3D_SetRenderState @ uses CGxDeviceD3d__device

Depth/Stencil Functions:

  • setDepthTest @ 0x0071f9d0
  • depthFunc @ 0x005a4af0
  • depthMask @ 0x005a4ac0
  • CreateDepthStencilSurface @ 0x005999c0
  • stencilFuncSeparate @ 0x005a4810
  • stencilOpSeparate @ 0x005a49b0

Nameplate Rendering (renderUnitNameplate @ 0x006c6e90):

Shows how to render world-space elements with proper transform:

  1. SetTransformMatrix for identity
  2. BeginRender()
  3. SetRenderState calls for depth/alpha
  4. CreateVertexBuffer, LockVertexBuffer
  5. DrawPrimitive
  6. EndRender()

UnitXP_SP3 Source Analysis

Repository: https://codeberg.org/konaka/UnitXP_SP3

Key Files:

  • sceneBegin_sceneEnd.cpp - Hooks WoW's scene rendering pipeline
  • Vanilla1121_functions.cpp - Function addresses and hooks
  • worldText.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 @ 0x4818F0
  • worldToScreen @ 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)

  1. Render model to stencil buffer only (no color write)
  2. Disable depth test
  3. Render slightly scaled-up model with outline color where stencil != written
  4. Re-enable depth test

Option 2: Two-Pass with Depth Disable

  1. First pass: Render model normally
  2. Second pass: Disable Z-test, render wireframe or scaled silhouette

Option 3: Post-Process Edge Detection

  1. Hook EndScene
  2. Use pixel shader to detect edges based on depth discontinuities
  3. Overlay edges as outline

Option 4: Hook Model Rendering

  1. Hook DrawBatch or CM2SceneRenderDraw
  2. Identify corpse models being rendered
  3. 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:

  1. Hook DrawIndexedPrimitive (D3D9 vtable index 82)
  2. Identify target models by stride/vertex count/primitive count
  3. 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:

  1. Log stride/vertex/primitive values and correlate with visual inspection
  2. Hook WoW's internal functions to track which object is being rendered
  3. 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

  1. Hook DrawIndexedPrimitive
  2. Add logging mode to record stride/vertices/primitives
  3. Visually identify corpse-related values in game
  4. 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

  1. Hook CM2SceneRenderDraw @ 0x0070b360 using MinHook/detours
  2. In hook, check if rendering model belongs to dead player
  3. 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)

  1. Keep current skull/name rendering
  2. Add a colored circle/glow rendered at corpse position
  3. 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

  1. Create an outline/halo texture
  2. Render it in world-space at corpse position (like selection circles)
  3. Disable Z-buffer so it shows through walls
  4. Use WoW's BeginRender/EndRender pattern

Pros: Looks like selection circle but visible through walls Cons: Not model-conforming outline

Start with Strategy D (world-space outline sprite) as it:

  1. Works with our current EndScene hook
  2. Doesn't require DrawIndexedPrimitive model identification
  3. Can be enhanced later with model-specific rendering

Implementation steps:

  1. Create a circular outline texture (or use D3DX to draw circle)
  2. In RenderDeadOverlay, for each corpse:
    • Transform corpse world position to screen
    • Disable Z-buffer
    • Render circle at ground level
    • Re-enable Z-buffer
  3. 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

  1. Create trampoline for CM2SceneRenderDraw at 0x0070b360
  2. 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


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:

  1. unit+0x88 = CM2Model instance (created by createModelAttachment, ~0x428 bytes)
  2. 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

  1. Log batch[1] structure contents - read batch[1]+0x00 through +0x40 to find 0x3AE98090
  2. Follow the chain - check if batch[1]+0x30 -> ... -> leads to unit's model
  3. 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:

  1. From batch[1], read batch[1]+0x3C0 (owner unit pointer, if it exists)
  2. If valid, read (batch[1]+0x3C0)+0x30 to get GUID
  3. 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:

  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
  2. 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:

  1. GUID high bits contain type info
  2. 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

  1. Abuzee's GUID (0x003DAEE9) comes from corpse owner field (CORPSE_FIELD_OWNER at object+0x118)
  2. Two corpse objects exist: 0x3B060008 and 0x3B060370 - both are minimal 0x18-byte objects
  3. All visible players have isDead=0 - none are dead CGUnit objects
  4. The hook finds one model with owner GUID 0x00186B75 (different player)
  5. Abuzee's GUID never appears in the hook output

The Problem

When a player dies and releases spirit:

  1. Their CGUnit is despawned (removed from object list)
  2. A minimal CGCorpse object remains (just a marker for resurrection)
  3. 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:

  1. Creates a model via createModelAttachment()
  2. Stores it at corpse+0xD8 via SetDisplayHandler()
  3. Calls InitializeModelWithParameters(model, callback, corpse) which sets model+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+0x28 should 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:

  1. Corpse objects DO have model pointers at +0xD8 (not NULL)
  2. Direct model pointer tracking works - we store corpse+0xD8 and match in hook
  3. 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:

  1. Modify render states when matched model is detected
  2. Re-render with depth test disabled and colored shader
  3. 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

  1. Determine corpse batch type - Add logging to RenderBatches to see what batch type corpse models use
  2. Hook the correct function - May need to hook a different draw function
  3. 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:

  1. Pass 1: Render model normally
  2. Pass 2: Scale model slightly larger (1.05x), cull front faces, render solid color
  3. 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

  1. Hook EndScene
  2. Read depth buffer and detect edges where depth changes sharply
  3. Draw outline at detected edges

Pros: True edge outline Cons: Complex, requires shaders, may have performance impact

Approach 4: Stencil Buffer Outline

  1. Render model to stencil buffer (increment)
  2. 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 walls
  • D3DRS_ZFUNC = D3DCMP_ALWAYS → Model still hidden by walls
  • D3DRS_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:

  1. Models (characters, corpses, objects) - rendered first
  2. 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:

  1. Use its own bone/transform matrices (ignoring D3DTS_WORLD)
  2. Use pixel shaders (ignoring fixed-function TEXTUREFACTOR)
  3. 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)

  1. In DIP hook: Cache vertex buffer, index buffer, transforms when corpse is detected
  2. In EndScene: After all world rendering is complete, replay the cached draw calls with Z-disabled
  3. Challenge: Need to capture and replay all necessary state

Option B: Hook World Geometry Rendering

  1. Find where WoW renders walls/terrain
  2. Insert our corpse redraw AFTER wall rendering
  3. Challenge: Need to identify the right hook point

Option C: Enhanced 2D Overlay (Simple, Working)

  1. Keep current skull/name markers in EndScene
  2. Add screen-space indicators (arrows, distance, direction)
  3. Works reliably since 2D overlay renders after everything

Option D: Hybrid - World-Space Outline Sprite

  1. Create an outline/halo texture
  2. Render it in world-space at corpse position (like selection circles)
  3. Disable Z-buffer so it shows through walls
  4. 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 renderContext pointers when corpse detected
  • In EndScene, call DrawBatchProj again 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:

  1. Render contexts contain nested pointers to stack-local data
  2. Those structures are not relocatable via simple memory copy
  3. By EndScene time, original stack memory is reused

Alternative approaches needed:

  1. Find a simpler render function that takes just model pointer + transforms
  2. Use stencil buffer marking during normal render, draw overlay in EndScene
  3. Hook at a different point in the render pipeline (after walls but before present)
  4. 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/h implements scene lifecycle hooks for device lost logic and FPS management
  • Visual Effects: worldText.cpp/h handles floating combat text rendering with custom serif fonts
  • Distance Calculations: distanceBetween.cpp and modernNameplateDistance.cpp/h for 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:

  1. First Pass: Render the model normally while writing a value (e.g., 1) to the stencil buffer
  2. 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:

Technique B: Scaled Backface Silhouette (Inverted Hull)

How It Works:

  1. First Pass: Render model normally with standard culling
  2. 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:

Technique C: Edge Detection Post-Process

How It Works:

  1. First Pass: Render scene with normals/depth to separate render target
  2. Second Pass: Full-screen post-process applies edge detection filter (Sobel, etc.) to detect discontinuities
  3. 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:

Technique D: Wireframe Overlay (Simple)

How It Works: Render the model twice:

  1. First Pass: Normal textured render
  2. 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:

  1. Hook SetVertexShader or SetPixelShader
  2. Detect when WoW's M2 model shaders are being set
  3. Replace with custom compiled shader that adds outline effect
  4. 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:

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:

  1. Hook DrawIndexedPrimitive (D3D9 vtable index 82)

    • Already have vtable patching infrastructure from EndScene hook
    • Can identify corpse models by comparing against tracked model pointers
  2. 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_WIREFRAME for 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 SetVertexShader hook 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);
}

Repository Analysis:

Stencil Buffer Techniques:

Outline Rendering Techniques:

D3D9 Shader and Hooking:

Depth and Render States:

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:

  1. Geometry caching at DIP level - Store vertex/index buffers and shader constants
  2. EndScene replay - Re-render cached geometry after all world geometry
  3. Back-face culling - Cull front faces (D3DCULL_CW), render only back faces
  4. 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:

  1. Performs full bone transforms (same as WoW's shader)
  2. Expands vertices along their normals by a configurable thickness
  3. 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

  1. NOT a standard view-projection matrix - Values are sparse scale/offset factors
  2. c2/c3 are aspect ratio scales - 1.054 and 1.874 relate to screen aspect ratio and FOV
  3. c4 has Z offset - The -0.080 suggests depth bias or near plane offset
  4. 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:

  1. Model A rendered at normal scale (front faces visible)
  2. Model B rendered SCALED LARGER (back faces only)
  3. 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:

  1. From corpse object, get position at corpse + 0x9E8 or via GetUnitPosition (0x00606F50)
  2. Pass model center to custom vertex shader via constants (c8+)
  3. In shader: scaled = center + (vertex - center) * scale
  4. 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:

  1. During corpse rendering, write depth to a separate render target
  2. In EndScene, run edge detection shader on depth buffer
  3. 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:

  1. Controllable world-space thickness
  2. Works with existing EndScene replay infrastructure
  3. Simpler implementation (single shader modification)
  4. Consistent outline regardless of distance

Option 3 (Post-Process) would require:

  1. Additional render target creation
  2. Multiple rendering passes
  3. New shader infrastructure
  4. 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

  1. Create a dynamic vertex buffer during caching
  2. Copy vertices with positions scaled from model-space center
  3. Use scaled VB for outline pass, original VB for normal render
  4. Pro: Works with WoW's full transform chain
  5. Con: Memory overhead, VB creation per frame

Option B: Modify Cached Vertex Data In-Place

  1. Lock original VB and scale positions temporarily
  2. Render outline pass
  3. Restore original positions
  4. Pro: No extra memory
  5. Con: May cause visual glitches if timing is wrong

Option C: Inject Scale into Bone Matrices

  1. Identify which constants are bone matrices (c10-c15?)
  2. Multiply bone matrices by scale factor
  3. Pro: Cleaner than VB modification
  4. Con: Requires understanding bone matrix layout

Option D: Post-Process Edge Detection

  1. Render corpse to stencil/depth buffer
  2. Detect edges via pixel shader
  3. Pro: True edge-only outline
  4. 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

  1. Stencil IS available: WoW uses D3DFMT_D24X8 (no stencil), but we can create our own D3DFMT_D24S8 surface and swap it in during EndScene.

  2. 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
  3. 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

  1. Cannot use depth buffer at EndScene for wall occlusion testing
  2. Scene depth information is lost by the time EndScene is called
  3. Depth bias has no effect - buffer is all zeros regardless of bias value

The Fundamental Conflict

Requirements:

  1. Silhouette visible through walls → must render AFTER walls (EndScene)
  2. Body covers silhouette → body must render AFTER silhouette
  3. 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

  1. Find hook point after walls but before depth clear - Need to identify where WoW clears depth
  2. Render silhouette in DIP hook - But then walls cover silhouette (no through-wall)
  3. Accept visual compromise - Semi-transparent outline that shows over body slightly

Resolution

The stencil buffer solved this problem:

  1. Mark body pixels in stencil (pass 1) - no color write
  2. Render dark halo/outline where stencil ≠ body (pass 2) - creates outline effect
  3. 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:

  1. Dead players: GUIDs added when unit has dead flag, removed when alive
  2. Raid marks: Model pointers added each frame for units with raid target icons
  3. 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 creation
  • model_outline_hook.cpp: Model tracking, color/category lookups, thickness values
  • dead_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