sky(phase-4b): clamp sky vTint at vertex + 1.0 fragment cap for retail parity

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) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-24 10:41:58 +02:00
parent 3a117bd91a
commit 2802fb2151
2 changed files with 14 additions and 6 deletions

View file

@ -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);

View file

@ -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
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);
}