From 2802fb21516d7ac63eff3c7806535917633bbcbf Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 24 Apr 2026 10:41:58 +0200 Subject: [PATCH] sky(phase-4b): clamp sky vTint at vertex + 1.0 fragment cap for retail parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After Phase 4 landed the per-vertex lighting formula, user observed acdream was still "a bit too bright" vs retail. Root cause: - My Phase 4 shader deliberately left vTint unclamped so D3D-style overbright contributions to emissive meshes (dome has Emissive=1 → lit could reach 2.0 with ambient + sun) would clamp naturally at the framebuffer. - But the frag cap was 1.2 (leaving "headroom for lightning flash"), letting dome vertices run 20% hotter than retail's per-channel 1.0. Retail's D3D fixed-function pipeline clamps vertex lit colour at D3DRS_COLORCLAMP=1 (default) BEFORE texture modulation. We now match: - Clamp `vTint = clamp(lit, 0, 1)` in sky.vert so the saturate happens at the vertex stage, exactly like D3D. - Drop normal-frame frag cap from 1.2 → 1.0 (the 3.0 flash relaxation stays so lightning strobes still visibly blow out). Expected visual: - Dome: identical appearance (was clamping to framebuffer 1.0 anyway), but pure retail-spec rendering so no sneaky 20% headroom. - Clouds: unchanged (already < 1.0 at morning Rainy keyframe). - Fragment flash during storm: unchanged — cap relaxes to 3.0 on flash. Build + 733 tests green. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/Shaders/sky.frag | 7 ++++--- src/AcDream.App/Rendering/Shaders/sky.vert | 13 ++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/AcDream.App/Rendering/Shaders/sky.frag b/src/AcDream.App/Rendering/Shaders/sky.frag index 8276be6..7015957 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.frag +++ b/src/AcDream.App/Rendering/Shaders/sky.frag @@ -50,10 +50,11 @@ void main() { float flash = uFogParams.z; rgb += flash * vec3(1.5, 1.5, 1.8); - // Soft clamp. Normal frame caps at 1.2 so the D3D-style overbright - // from Emissive+Ambient+Diffuse at day-time saturates cleanly; during + // Normal-frame cap at 1.0 (retail D3D framebuffer clamps at 1.0 + // per channel for RGBA8 output; vTint is already vertex-clamped so + // the only path above 1 is lightning flash additive bump). During // a flash the ceiling relaxes so the strobe blows out visibly. - float cap = mix(1.2, 3.0, clamp(flash, 0.0, 1.0)); + float cap = mix(1.0, 3.0, clamp(flash, 0.0, 1.0)); rgb = min(rgb, vec3(cap)); float a = sampled.a * (1.0 - uTransparency); diff --git a/src/AcDream.App/Rendering/Shaders/sky.vert b/src/AcDream.App/Rendering/Shaders/sky.vert index 87e011d..48e5987 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.vert +++ b/src/AcDream.App/Rendering/Shaders/sky.vert @@ -61,8 +61,15 @@ void main() { // Retail per-vertex fixed-function lighting (AMBIENT=0 globally, // so the global ambient term drops; only light.Ambient contributes). + // Clamp to [0,1] at the vertex — retail's D3DRS_COLORCLAMP defaults + // to clamping lit vertex colours to 1.0 BEFORE texture modulate. + // Without this, a dome vertex (uEmissive=1) picks up ambient+diff + // on top of already-saturated emissive, producing > 1.5 lit values + // that our framebuffer cap (1.2) lets through as 20% overbright + // vs retail's 1.0-clamped reference. User-observed 2026-04-23. float diff = max(dot(worldNormal, uSunDir), 0.0); - vTint = vec3(uEmissive) // material.Emissive - + uAmbientColor // material.Ambient(1) × light.Ambient - + uSunColor * diff; // material.Diffuse(1) × light.Diffuse × N·L + vec3 lit = vec3(uEmissive) // material.Emissive + + uAmbientColor // material.Ambient(1) × light.Ambient + + uSunColor * diff; // material.Diffuse(1) × light.Diffuse × N·L + vTint = clamp(lit, 0.0, 1.0); }