sky(phase-4): retail-verbatim per-vertex lighting on sky meshes

Re-enables the Phase 2 lighting formula that was reverted in Phase 3b
due to a "blue-green-yellow sweep" across clouds. Root cause of that
earlier regression was NOT the formula — it was that we rolled the
wrong DayGroup (Sunny when retail was Cloudy), producing a sharp warm
sun against a sky that should have been rendered with diffuse
overcast light. After Phase 3g pinned the LCG multiplier to 360
(DaysPerYear) so retail + acdream agree on DayGroup, the same
per-vertex formula now faithfully reproduces retail's visuals.

The formula is verified in decompile agent Q2+Q4+Q6 results,
`docs/research/2026-04-23-sky-material-state.md`:

  D3DRS_LIGHTING = ON         (FUN_0059da60:10648)
  D3DRS_AMBIENT  = 0          (never written after init)
  Material.Emissive = (Luminosity, Luminosity, Luminosity, 1)
  Material.Ambient/Diffuse = defaults (≈1,1,1,1) for non-luminous
  light.Ambient = keyframe AmbColor × AmbBright (via SetDirectionalLight)
  light.Diffuse = keyframe DirColor × DirBright

Fixed-function lighting per vertex:
  lit = Emissive + Ambient × lightAmbient + Diffuse × lightDiffuse × max(N·L, 0)
      = Surface.Luminosity + AmbColor×AmbBright + DirColor×DirBright × max(N·L, 0)

Fragment: texture × lit × SkyObjectReplace.Luminosity.

Expected visual:
- Dome (Surface.Luminosity=1): `lit = 1 + amb + diff·N·L` saturates to 1
  → texture passthrough, baked gradient preserved.
- Clouds (Surface.Luminosity=0): `lit = 0 + amb + diff·N·L`
  → purple haze at night (ambient dominates, sun below horizon);
  → warm tan at dusk (ambient + warm sun on west-facing vertices);
  → pale cool gray at noon (ambient + white sun from above).
- Sun/moon (SurfaceType.Additive, Luminosity=1): same as dome +
  additive blend — stays bright regardless.

The shader uniforms (uAmbientColor, uSunColor, uSunDir, uEmissive)
were already wired in the C# renderer from Phase 2; Phase 3b just
stopped using them in the shader. This commit re-activates them.

No clamp at the vertex — retail's D3D lighting allows Emissive+sum
to exceed 1, relies on the framebuffer per-channel saturation. We
keep the 1.2 ceiling in the frag (for lightning flash overbright
headroom) consistent with that convention.

No fog yet (Q1 confirmed retail leaves fog enabled for sky; will add
in a follow-up if horizon looks too bright).

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:37:40 +02:00
parent 1e1d3875f7
commit 3a117bd91a
2 changed files with 68 additions and 41 deletions

View file

@ -1,29 +1,31 @@
#version 430 core
// Sky mesh fragment shader — UNLIT texture passthrough modulated by the
// per-keyframe SkyObjectReplace.Luminosity and .Transparent overrides.
// Sky mesh fragment shader — final composite matching retail's
// D3D fixed-function:
//
// fragment.rgb = texture.rgb * uLuminosity + lightning_flash
// fragment.a = texture.a * (1 - uTransparency)
// fragment.rgb = texture.rgb × vTint × uLuminosity + lightning_flash
// fragment.a = texture.a × (1 - uTransparency)
//
// uLuminosity defaults to 1.0 (no dim). A SkyObjectReplace entry with
// Luminosity_raw=11 (11%) sets uLuminosity to 0.11 — mesh renders at
// 11% brightness. MaxBright is min-clamped into uLuminosity by the C#
// renderer before it reaches the shader.
// uTransparency defaults to 0.0. Replace.Transparent_raw=100 (100%) sets
// uTransparency to 1.0 — alpha is zeroed and the pixel discarded
// (cloud hidden so the dome behind shows through).
// vTint arrives from the vertex shader with retail's per-vertex
// lighting formula baked in (Emissive + lightAmbient + lightDiffuse ×
// max(N·L, 0)) — see sky.vert for the decompile citation.
//
// See `docs/research/2026-04-23-sky-retail-verbatim.md` §6 + Phase 3b
// rationale in sky.vert.
// uLuminosity is the per-keyframe SkyObjectReplace.Luminosity override
// (0..1, /100 in SkyDescLoader). It's a SEPARATE field from the
// Surface.Luminosity that feeds uEmissive in the vertex shader — they
// compose multiplicatively in retail too.
//
// See `docs/research/2026-04-23-sky-material-state.md`.
in vec2 vTex;
in vec3 vTint;
out vec4 fragColor;
uniform sampler2D uDiffuse;
uniform float uTransparency;
uniform float uLuminosity;
uniform float uTransparency; // 0 = fully visible, 1 = fully transparent
uniform float uLuminosity; // SkyObjectReplace.Luminosity override (0..1)
// Shared SceneLighting UBO — only fog-flash channel used (lightning).
// Shared SceneLighting UBO — only need the fog-flash channel for
// client-driven lightning strobes; sun/ambient already baked into vTint.
struct Light {
vec4 posAndKind;
vec4 dirAndRange;
@ -41,13 +43,16 @@ layout(std140, binding = 1) uniform SceneLighting {
void main() {
vec4 sampled = texture(uDiffuse, vTex);
// Unlit passthrough with per-keyframe dim.
vec3 rgb = sampled.rgb * uLuminosity;
// Composite: texture × per-vertex lit × per-keyframe dim.
vec3 rgb = sampled.rgb * vTint * uLuminosity;
// Lightning additive bump (client-driven during storm keyframes).
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
// a flash the ceiling relaxes so the strobe blows out visibly.
float cap = mix(1.2, 3.0, clamp(flash, 0.0, 1.0));
rgb = min(rgb, vec3(cap));