69051fd486
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.
2269 lines
84 KiB
Markdown
2269 lines
84 KiB
Markdown
# 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):
|
|
```cpp
|
|
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:
|
|
```cpp
|
|
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:
|
|
```cpp
|
|
// 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:
|
|
```cpp
|
|
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
|
|
|
|
### Recommended Approach
|
|
|
|
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`:
|
|
|
|
```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`:
|
|
```cpp
|
|
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
|
|
```cpp
|
|
// __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](https://codeberg.org/konaka/UnitXP_SP3)
|
|
- [UnitXP_SP3 GitHub Fork](https://github.com/jrc13245/UnitXP_SP3)
|
|
- [wowdev.wiki Rendering](https://wowdev.wiki/Rendering)
|
|
- [wowdev.wiki M2](https://wowdev.wiki/M2)
|
|
- [wowdev.wiki M2/Rendering](https://wowdev.wiki/M2/Rendering)
|
|
- [D3D9 Chams Tutorial](https://niemand.com.ar/2019/01/13/creating-your-own-wallhack/)
|
|
- [Stack Overflow D3D9 Hooking](https://stackoverflow.com/questions/47652902/d3d9-hooking-endscene-drawindexedprimitive)
|
|
|
|
---
|
|
|
|
## Detailed Structure Analysis (Ghidra Decompilation)
|
|
|
|
### CM2SceneRenderDraw @ 0x0070b360 (FULL ANALYSIS)
|
|
|
|
```cpp
|
|
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:**
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
CGUnitShouldRender = 0x00607da0
|
|
CGUnitPreAnimate = 0x00607ed0
|
|
CGUnitAnimate = 0x00608560
|
|
OnWorldRender = 0x00483460
|
|
```
|
|
|
|
### CGUnit_ShouldRender Analysis (0x00607da0)
|
|
|
|
**Function Signature:**
|
|
```cpp
|
|
// __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):**
|
|
```asm
|
|
00607da0: PUSH EBX ; 1 byte
|
|
00607da1: MOV EBX,ESP ; 2 bytes
|
|
00607da3: SUB ESP,0x8 ; 3 bytes
|
|
```
|
|
|
|
**Key Logic:**
|
|
```cpp
|
|
// 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)
|
|
|
|
```cpp
|
|
// 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:**
|
|
```cpp
|
|
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**
|
|
```cpp
|
|
// 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**
|
|
```cpp
|
|
// 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`:
|
|
```cpp
|
|
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)
|
|
|
|
```cpp
|
|
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`:
|
|
```cpp
|
|
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:
|
|
```cpp
|
|
// 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:
|
|
|
|
```cpp
|
|
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):
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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)
|
|
|
|
```cpp
|
|
// 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:**
|
|
```cpp
|
|
// 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](https://learn.microsoft.com/en-us/windows/win32/direct3d9/stencil-buffer-techniques)
|
|
- [Stack Overflow: Using stencil buffer in Direct3D](https://stackoverflow.com/questions/6183791/using-stencil-buffer-in-direct3d)
|
|
- [Game Developer: Inside Direct3D Stencil Buffers](https://www.gamedeveloper.com/programming/inside-direct3d----stencil-buffers)
|
|
|
|
#### **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:**
|
|
```cpp
|
|
// 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:
|
|
```hlsl
|
|
// 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?](https://gamedev.stackexchange.com/questions/68401/how-can-i-draw-outlines-around-3d-models)
|
|
- [RoveCoder: DirectX 11 Stencil Outline](https://rovecoder.net/article/directx-11/stencil-outline)
|
|
- [Imaginary Blend: Backface culling based outlines](https://imaginaryblend.com/2018/07/15/533/)
|
|
|
|
#### **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:**
|
|
- [Medium: Three.js Post-processing outline Effect](https://medium.com/@coderfromnineteen/three-js-post-processing-outline-effect-6dff6a2fe3c0)
|
|
- [Ameye.dev: Edge Detection Outlines](https://ameye.dev/notes/edge-detection-outlines/)
|
|
- [Stack Overflow: Multiple Render Targets in DirectX9](https://stackoverflow.com/questions/10157734/multiple-render-targets-in-directx9)
|
|
|
|
#### **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:**
|
|
```cpp
|
|
// 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:**
|
|
- [Microsoft Learn: Using Shaders in Direct3D 9](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-using-shaders-9)
|
|
- [Microsoft Learn: IDirect3DDevice9::SetVertexShaderConstantF](https://learn.microsoft.com/en-us/windows/win32/api/d3d9/nf-d3d9-idirect3ddevice9-setvertexshaderconstantf)
|
|
- [CodePal: Direct3D Hooks in C++](https://codepal.ai/code-generator/query/ubPbNvDO/implementing-direct3d-hooks-cpp)
|
|
|
|
### 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:**
|
|
|
|
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:**
|
|
|
|
```cpp
|
|
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:**
|
|
```cpp
|
|
// 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:**
|
|
```cpp
|
|
// 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**
|
|
|
|
```cpp
|
|
// 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:**
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
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:**
|
|
- [UnitXP_SP3 on Codeberg](https://codeberg.org/konaka/UnitXP_SP3)
|
|
- [perf_boost on Gitea](https://gitea.com/avitasia/perf_boost)
|
|
- [VanillaHelpers on GitHub](https://github.com/isfir/VanillaHelpers)
|
|
|
|
**Stencil Buffer Techniques:**
|
|
- [Microsoft Learn: Stencil Buffer Techniques (Direct3D 9)](https://learn.microsoft.com/en-us/windows/win32/direct3d9/stencil-buffer-techniques)
|
|
- [Stack Overflow: Using stencil buffer in Direct3D](https://stackoverflow.com/questions/6183791/using-stencil-buffer-in-direct3d)
|
|
- [Game Developer: Inside Direct3D Stencil Buffers](https://www.gamedeveloper.com/programming/inside-direct3d----stencil-buffers)
|
|
- [LearnOpenGL: Stencil testing](https://learnopengl.com/Advanced-OpenGL/Stencil-testing) (OpenGL but concepts translate)
|
|
|
|
**Outline Rendering Techniques:**
|
|
- [Game Dev Stack Exchange: How can I draw outlines around 3D models?](https://gamedev.stackexchange.com/questions/68401/how-can-i-draw-outlines-around-3d-models)
|
|
- [RoveCoder: DirectX 11 Stencil Outline](https://rovecoder.net/article/directx-11/stencil-outline) (DX11 but technique applies)
|
|
- [Imaginary Blend: Backface culling based outlines](https://imaginaryblend.com/2018/07/15/533/)
|
|
- [Ameye.dev: Edge Detection Outlines](https://ameye.dev/notes/edge-detection-outlines/)
|
|
|
|
**D3D9 Shader and Hooking:**
|
|
- [Microsoft Learn: Using Shaders in Direct3D 9](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-using-shaders-9)
|
|
- [Microsoft Learn: IDirect3DDevice9::SetPixelShader](https://learn.microsoft.com/en-us/windows/win32/api/d3d9/nf-d3d9-idirect3ddevice9-setpixelshader)
|
|
- [Microsoft Learn: IDirect3DDevice9::SetVertexShaderConstantF](https://learn.microsoft.com/en-us/windows/win32/api/d3d9/nf-d3d9-idirect3ddevice9-setvertexshaderconstantf)
|
|
- [CodePal: Direct3D Hooks in C++](https://codepal.ai/code-generator/query/ubPbNvDO/implementing-direct3d-hooks-cpp)
|
|
- [Stack Overflow: D3D9 Hooking (EndScene + DrawIndexedPrimitive)](https://stackoverflow.com/questions/47652902/d3d9-hooking-endscene-drawindexedprimitive)
|
|
|
|
**Depth and Render States:**
|
|
- [Microsoft Learn: D3DRENDERSTATETYPE enumeration](https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3drenderstatetype)
|
|
- [Microsoft Learn: Depth Buffering State (Direct3D 9)](https://learn.microsoft.com/en-us/windows/win32/direct3d9/depth-buffering-state)
|
|
- [Microsoft Learn: Changing Depth Buffer Comparison Functions](https://learn.microsoft.com/en-us/windows/win32/direct3d9/changing-depth-buffer-comparison-functions)
|
|
|
|
**WoW-Specific:**
|
|
- [wowdev.wiki: Rendering](https://wowdev.wiki/Rendering)
|
|
- [wowdev.wiki: M2](https://wowdev.wiki/M2)
|
|
- [wowdev.wiki: M2/Rendering](https://wowdev.wiki/M2/Rendering)
|
|
- [wowdev.wiki: M2/.skin](https://wowdev.wiki/M2/.skin)
|
|
|
|
---
|
|
|
|
## 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:**
|
|
```cpp
|
|
// 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):**
|
|
```hlsl
|
|
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:**
|
|
```hlsl
|
|
// 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:
|
|
```hlsl
|
|
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[N*3 + 31], c[N*3 + 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:
|
|
|
|
```asm
|
|
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**
|
|
```cpp
|
|
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)**
|
|
```cpp
|
|
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)**
|
|
```cpp
|
|
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:
|
|
```cpp
|
|
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:
|
|
```cpp
|
|
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
|