Fix game object occlusion: render outline targets last in batch order

Outline targets now render after all other M2 models instead of before,
so the depth buffer contains full scene geometry (game objects, other
characters, NPCs) when stencil marks are written. Previously only
terrain+WMO depth was available, causing outlines to show through
fences, mailboxes, lamp posts, and other game objects.
This commit is contained in:
MarcelineVQ
2026-02-24 19:06:10 -08:00
parent 89ac4ec7f3
commit 661e1562d9
2 changed files with 23 additions and 15 deletions
+10 -8
View File
@@ -8,10 +8,11 @@
//! marks where models pass the terrain depth test (stencil=1 = visible). //! marks where models pass the terrain depth test (stencil=1 = visible).
//! - **Reset**: forces D24S8 depth/stencil format, releases resources. //! - **Reset**: forces D24S8 depth/stencil format, releases resources.
//! //!
//! Batch reordering in model_hook.zig ensures outline targets render first in //! Batch reordering in model_hook.zig ensures outline targets render LAST in
//! CM2SceneRenderDraw, when only terrain+WMO depth exists. The DIP hook writes //! CM2SceneRenderDraw, after all other M2 models (game objects, characters,
//! stencil marks using the game's own depth buffer; EndScene replay uses these //! NPCs) have filled the depth buffer. The DIP hook writes stencil marks
//! marks to gate silhouette drawing (terrain/WMO occlusion without DS copy). //! using the game's own depth buffer; EndScene replay uses these marks to
//! gate silhouette drawing (full scene occlusion including game objects).
const std = @import("std"); const std = @import("std");
const hook = @import("hook"); const hook = @import("hook");
@@ -796,10 +797,11 @@ fn hkDIP(
frame_has_outlines = true; frame_has_outlines = true;
} }
// Mark terrain-visible pixels in stencil for this outline target. // Mark visible pixels in stencil for this outline target.
// At this point (outline targets draw first due to batch reordering), // At this point (outline targets draw last due to batch reordering),
// the game's DS has only terrain+WMO depth. Pixels that pass the depth // the game's DS has terrain+WMO+all non-outline M2 model depth.
// test get stencil=1; pixels behind terrain fail and keep stencil=0. // Pixels that pass the depth test get stencil=1; pixels behind any
// scene geometry fail and keep stencil=0.
// EndScene uses these marks to gate silhouette rendering. // EndScene uses these marks to gate silhouette rendering.
const s_enable = deviceGetRS(device, types.D3DRS.STENCILENABLE); const s_enable = deviceGetRS(device, types.D3DRS.STENCILENABLE);
const s_func = deviceGetRS(device, types.D3DRS.STENCILFUNC); const s_func = deviceGetRS(device, types.D3DRS.STENCILFUNC);
+13 -7
View File
@@ -1,7 +1,7 @@
//! WoW model rendering pipeline hooks. //! WoW model rendering pipeline hooks.
//! //!
//! Hooks three WoW functions to integrate outline rendering: //! Hooks three WoW functions to integrate outline rendering:
//! - CM2SceneRenderDraw — captures terrain depth before M2 models draw. //! - CM2SceneRenderDraw — reorders batches so outline targets render last.
//! - CM2Model_ManageRenderListNode — classifies models on render-list add. //! - CM2Model_ManageRenderListNode — classifies models on render-list add.
//! - CM2Scene_DrawBatchProjected — flags the DIP hook for outline rendering. //! - CM2Scene_DrawBatchProjected — flags the DIP hook for outline rendering.
//! //!
@@ -62,9 +62,11 @@ var reordered_indices: [MAX_REORDER]i32 = undefined;
// __thiscall(this, viewMatrix, batchData, batchIndices, batchCount) // __thiscall(this, viewMatrix, batchData, batchIndices, batchCount)
// Native thiscall detour — no thunk needed. // Native thiscall detour — no thunk needed.
// //
// Reorders batch indices so outline targets draw first, when the game's // Reorders batch indices so outline targets draw LAST. Non-outline models
// depth buffer contains only terrain + WMO geometry. The DIP hook renders // (game objects, other characters, NPCs) render first, filling the depth
// silhouettes using the game's own DS for depth testing (ZWRITEENABLE=FALSE). // buffer with full scene geometry. When outline targets then render, the
// DIP hook writes stencil marks against this complete depth buffer, so
// outlines are properly occluded by all scene objects (not just terrain/WMOs).
fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) callconv(THISCALL) void { fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) callconv(THISCALL) void {
// One-time: install D3D9 hooks now that the game is actively rendering. // One-time: install D3D9 hooks now that the game is actively rendering.
@@ -96,9 +98,13 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
return; return;
} }
// Pass 2: partition — outline targets first, then everything else. // Pass 2: partition — non-outline models first, outline targets last.
var outline_pos: u32 = 0; // Rendering non-outline models first fills the depth buffer with game
var normal_pos: u32 = outline_count; // objects, other characters, etc., so outline target stencil marks
// respect full scene occlusion (not just terrain+WMO).
const normal_count = batch_count - outline_count;
var normal_pos: u32 = 0;
var outline_pos: u32 = normal_count;
for (0..batch_count) |i| { for (0..batch_count) |i| {
const batch_idx = indices[i]; const batch_idx = indices[i];
const idx_u: u32 = @bitCast(batch_idx); const idx_u: u32 = @bitCast(batch_idx);