From 93e2c6503339d4be5042e9748c610d379be3b985 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Sat, 29 Aug 2026 10:34:02 +0200 Subject: [PATCH] V35: prototype retail-like dilation and halo outline --- src/outline/d3d9_hook.zig | 215 ++++++++++++++++++++++---------------- 1 file changed, 126 insertions(+), 89 deletions(-) diff --git a/src/outline/d3d9_hook.zig b/src/outline/d3d9_hook.zig index 3abb6ca..f3d6df4 100644 --- a/src/outline/d3d9_hook.zig +++ b/src/outline/d3d9_hook.zig @@ -159,7 +159,8 @@ var outline_ps: ?*anyopaque = null; // flat-color PS (solid silhouettes) var outline_alpha_ps: ?*anyopaque = null; // texture-alpha-aware silhouette PS var outline_rgb_ps: ?*anyopaque = null; // additive/modulated texture coverage PS var material_mask_ps: ?*anyopaque = null; // exact-material scratch -> binary mask -var jfa_init_ps: ?*anyopaque = null; // JFA seed init PS +var retail_outline_ps: ?*anyopaque = null; // V35 retail-like dilation/ring/halo composite +var jfa_init_ps: ?*anyopaque = null; // legacy JFA seed init PS var jfa_prop_ps: ?*anyopaque = null; // JFA propagation PS var jfa_decode_ps: ?*anyopaque = null; // JFA decode + composite PS var debug_sil_ps: ?*anyopaque = null; // debug: composite silhouette directly @@ -628,6 +629,97 @@ const material_mask_src = "texkill r1\n" ++ "mov oC0, c0\n"; +/// V35 EXPERIMENTAL RETAIL-LIKE OUTLINE. +/// Direct screen-space morphology from the clean silhouette RT; no JFA. +/// c0.xy = one screen texel in UV space. +/// c1 = pale target colour. +/// c2 = (core_radius_px, halo_radius_px, core_alpha, halo_alpha). +/// +/// The 8-neighbour max at core_radius forms a thin outer ring. A second, +/// wider 8-neighbour max contributes only the low-opacity outer halo. +/// Subtracting the centre silhouette keeps the model itself untouched. +const retail_outline_src = + "ps_3_0\n" ++ + "def c3, 0.0, 1.0, 0.0, 0.0\n" ++ + "def c4, -1.0, -1.0, 0.0, 0.0\n" ++ + "def c5, -1.0, 0.0, 0.0, 0.0\n" ++ + "def c6, -1.0, 1.0, 0.0, 0.0\n" ++ + "def c7, 0.0, -1.0, 0.0, 0.0\n" ++ + "def c8, 0.0, 1.0, 0.0, 0.0\n" ++ + "def c9, 1.0, -1.0, 0.0, 0.0\n" ++ + "def c10, 1.0, 0.0, 0.0, 0.0\n" ++ + "def c11, 1.0, 1.0, 0.0, 0.0\n" ++ + "dcl_2d s0\n" ++ + "dcl_texcoord0 v0\n" ++ + // Centre silhouette coverage. + "texld r0, v0, s0\n" ++ + "mov r1.x, r0.a\n" ++ // core dilation max + "mov r2.x, r0.a\n" ++ // halo dilation max + // Sub-pixel radii + linear sampling give a softer edge than hard integer taps. + "mul r7.xy, c0.xy, c2.xx\n" ++ + "mul r8.xy, c0.xy, c2.yy\n" ++ + // Core radius: 8 neighbours. + "mad r4.xy, c4.xy, r7.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r1.x, r1.x, r5.a\n" ++ + "mad r4.xy, c5.xy, r7.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r1.x, r1.x, r5.a\n" ++ + "mad r4.xy, c6.xy, r7.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r1.x, r1.x, r5.a\n" ++ + "mad r4.xy, c7.xy, r7.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r1.x, r1.x, r5.a\n" ++ + "mad r4.xy, c8.xy, r7.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r1.x, r1.x, r5.a\n" ++ + "mad r4.xy, c9.xy, r7.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r1.x, r1.x, r5.a\n" ++ + "mad r4.xy, c10.xy, r7.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r1.x, r1.x, r5.a\n" ++ + "mad r4.xy, c11.xy, r7.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r1.x, r1.x, r5.a\n" ++ + // Halo radius: 8 neighbours. + "mad r4.xy, c4.xy, r8.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r2.x, r2.x, r5.a\n" ++ + "mad r4.xy, c5.xy, r8.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r2.x, r2.x, r5.a\n" ++ + "mad r4.xy, c6.xy, r8.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r2.x, r2.x, r5.a\n" ++ + "mad r4.xy, c7.xy, r8.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r2.x, r2.x, r5.a\n" ++ + "mad r4.xy, c8.xy, r8.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r2.x, r2.x, r5.a\n" ++ + "mad r4.xy, c9.xy, r8.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r2.x, r2.x, r5.a\n" ++ + "mad r4.xy, c10.xy, r8.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r2.x, r2.x, r5.a\n" ++ + "mad r4.xy, c11.xy, r8.xy, v0.xy\n" ++ + "texld r5, r4, s0\n" ++ + "max r2.x, r2.x, r5.a\n" ++ + // Ring = dilated core minus original; halo = wide dilation minus core. + "sub r3.x, r1.x, r0.a\n" ++ + "sub r3.y, r2.x, r1.x\n" ++ + "max r3.x, r3.x, c3.x\n" ++ + "max r3.y, r3.y, c3.x\n" ++ + "mul r3.x, r3.x, c2.z\n" ++ + "mad r3.x, r3.y, c2.w, r3.x\n" ++ + "min r3.x, r3.x, c3.y\n" ++ + "mov r6.xyz, c1.xyz\n" ++ + "mov r6.w, r3.x\n" ++ + "mov oC0, r6\n"; + /// JFA init: sample silhouette, output own UV as seed or sentinel (-1,-1). /// Sentinel must be outside [0,1] UV space so it never wins distance comparisons. const jfa_init_src = @@ -808,26 +900,12 @@ fn ensureShaders(device: *anyopaque) void { }; debug_shader_stage = 4; // silhouette/material-mask PS variants ready - // --- JFA Init PS --- - jfa_init_ps = assemblePS(device, assemble, jfa_init_src, jfa_init_src.len) orelse { + // --- V35 Retail-like morphology composite PS --- + retail_outline_ps = assemblePS(device, assemble, retail_outline_src, retail_outline_src.len) orelse { releaseShaders(); return; }; - debug_shader_stage = 5; // JFA init ready - - // --- JFA Propagation PS --- - jfa_prop_ps = assemblePS(device, assemble, jfa_prop_src, jfa_prop_src.len) orelse { - releaseShaders(); - return; - }; - debug_shader_stage = 6; // JFA propagation ready - - // --- JFA Decode + Composite PS --- - jfa_decode_ps = assemblePS(device, assemble, jfa_decode_src, jfa_decode_src.len) orelse { - releaseShaders(); - return; - }; - debug_shader_stage = 7; // JFA decode ready + debug_shader_stage = 7; // retail composite ready // --- Debug silhouette composite PS (only when diagnostic enabled) --- if (DEBUG_SHOW_SILHOUETTE) { @@ -893,7 +971,7 @@ fn assemblePS(device: *anyopaque, assemble: D3DXAssembleShaderFn, src: [*]const } fn releaseShaders() void { - inline for (.{ &outline_ps, &outline_alpha_ps, &outline_rgb_ps, &material_mask_ps, &jfa_init_ps, &jfa_prop_ps, &jfa_decode_ps, &debug_sil_ps }) |ps| { + inline for (.{ &outline_ps, &outline_alpha_ps, &outline_rgb_ps, &material_mask_ps, &retail_outline_ps, &jfa_init_ps, &jfa_prop_ps, &jfa_decode_ps, &debug_sil_ps }) |ps| { if (ps.*) |p| { comRelease(p); ps.* = null; @@ -1131,10 +1209,9 @@ fn runJfaPipeline(device: *anyopaque) void { }; // Verify all resources and shaders - if (rt_material_tex == null or rt_material_surf == null or rt_silhouette_tex == null or rt_jfa_a_surf == null or rt_jfa_b_surf == null) return; + if (rt_material_tex == null or rt_material_surf == null or rt_silhouette_tex == null or rt_silhouette_surf == null) return; if (!shaders_attempted) ensureShaders(device); - if (jfa_init_ps == null or jfa_prop_ps == null or jfa_decode_ps == null) return; - if (outline_ps == null or material_mask_ps == null or rt_silhouette_surf == null) return; + if (outline_ps == null or material_mask_ps == null or retail_outline_ps == null) return; debug_pipeline_ready_seen = true; @@ -1346,86 +1423,46 @@ fn runJfaPipeline(device: *anyopaque) void { } else { // ===================================================================== - // Phase 2: JFA pipeline (silhouette → outline composite) + // V35 Phase 2: Retail-like screen-space outline composite. + // No distance field / JFA: directly dilate the clean silhouette in + // screen space, subtract its interior, then add a low-opacity halo. // ===================================================================== + if (saved_rt0) |rt| deviceSetRenderTarget(device, 0, rt); deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, null); deviceSetPtrOrNull(device, types.VT.SetVertexShader, null); deviceSetFVF(device, types.D3DFVF_XYZRHW | types.D3DFVF_TEX1); deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE); deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0); - deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 0); deviceSetRS(device, types.D3DRS.CULLMODE, types.D3DCULL_NONE); deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0); deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F); - - deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP); - deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP); - deviceSetSamplerState(device, 0, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT); - deviceSetSamplerState(device, 0, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT); - deviceSetSamplerState(device, 0, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE); - deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP); - deviceSetSamplerState(device, 1, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP); - deviceSetSamplerState(device, 1, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT); - deviceSetSamplerState(device, 1, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT); - deviceSetSamplerState(device, 1, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE); - - const quad = buildFullscreenQuad(vp.Width, vp.Height); - const qstride: u32 = @sizeOf(QuadVertex); - const fw = @as(f32, @floatFromInt(@max(vp.Width, 1))); - const fh = @as(f32, @floatFromInt(@max(vp.Height, 1))); - - // Pass 1: JFA Init (silhouette → JFA_A). - deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?); - deviceSetTexture(device, 0, rt_silhouette_tex); - deviceSetPtr(device, types.VT.SetPixelShader, jfa_init_ps.?); - deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride); - - // JFA Propagation: steps [8, 4, 2, 1] ping-ponging between A and B. - deviceSetPtr(device, types.VT.SetPixelShader, jfa_prop_ps.?); - var c0: [4]f32 = undefined; - - // step=8 (JFA_A → JFA_B) - deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?); - deviceSetTexture(device, 0, rt_jfa_a_tex); - c0 = .{ 8.0 / fw, 8.0 / fh, 0.0, 0.0 }; - deviceSetPSConstF(device, 0, &c0); - deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride); - - // step=4 (JFA_B → JFA_A) - deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?); - deviceSetTexture(device, 0, rt_jfa_b_tex); - c0 = .{ 4.0 / fw, 4.0 / fh, 0.0, 0.0 }; - deviceSetPSConstF(device, 0, &c0); - deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride); - - // step=2 (JFA_A → JFA_B) - deviceSetRenderTarget(device, 0, rt_jfa_b_surf.?); - deviceSetTexture(device, 0, rt_jfa_a_tex); - c0 = .{ 2.0 / fw, 2.0 / fh, 0.0, 0.0 }; - deviceSetPSConstF(device, 0, &c0); - deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride); - - // step=1 (JFA_B → JFA_A) - deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?); - deviceSetTexture(device, 0, rt_jfa_b_tex); - c0 = .{ 1.0 / fw, 1.0 / fh, 0.0, 0.0 }; - deviceSetPSConstF(device, 0, &c0); - deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride); - - // Pass 4: Decode + Composite (JFA_A + silhouette → backbuffer) - if (saved_rt0) |rt| deviceSetRenderTarget(device, 0, rt); - deviceSetTexture(device, 0, rt_jfa_a_tex); - deviceSetTexture(device, 1, rt_silhouette_tex); - // V34: hard 3px edge unchanged; JFA seed precision is now full-float. - c0 = [4]f32{ fw, fh, 3.0, 0.0 }; - deviceSetPSConstF(device, 0, &c0); - deviceSetPtr(device, types.VT.SetPixelShader, jfa_decode_ps.?); deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 1); deviceSetRS(device, types.D3DRS.SRCBLEND, types.D3DBLEND_SRCALPHA); deviceSetRS(device, types.D3DRS.DESTBLEND, types.D3DBLEND_INVSRCALPHA); - deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), qstride); - } // end else (normal JFA path) + + // Linear sampling at fractional texel radii provides the subtle + // anti-aliased falloff that the hard JFA decode did not have. + deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP); + deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP); + deviceSetSamplerState(device, 0, types.D3DSAMP.MAGFILTER, types.D3DTEXF_LINEAR); + deviceSetSamplerState(device, 0, types.D3DSAMP.MINFILTER, types.D3DTEXF_LINEAR); + deviceSetSamplerState(device, 0, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE); + deviceSetTexture(device, 0, rt_silhouette_tex); + deviceSetPtr(device, types.VT.SetPixelShader, retail_outline_ps.?); + + const quad = buildFullscreenQuad(vp.Width, vp.Height); + const fw = @as(f32, @floatFromInt(@max(vp.Width, 1))); + const fh = @as(f32, @floatFromInt(@max(vp.Height, 1))); + const texel: [4]f32 = .{ 1.0 / fw, 1.0 / fh, 0.0, 0.0 }; + const retail_color: [4]f32 = .{ 1.0, 0.95, 0.58, 1.0 }; + // 1.25px pale core + 2.25px low-opacity outer halo. + const retail_shape: [4]f32 = .{ 1.25, 2.25, 0.82, 0.26 }; + deviceSetPSConstF(device, 0, &texel); + deviceSetPSConstF(device, 1, &retail_color); + deviceSetPSConstF(device, 2, &retail_shape); + deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), @sizeOf(QuadVertex)); + } // end else (V35 retail-like path) // ===================================================================== // Restore ALL state