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:
parent
1e1d3875f7
commit
3a117bd91a
2 changed files with 68 additions and 41 deletions
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,35 @@
|
|||
#version 430 core
|
||||
// Sky mesh vertex shader — UNLIT texture passthrough.
|
||||
// Sky mesh vertex shader — retail-verbatim D3D fixed-function lighting
|
||||
// ported to per-vertex GLSL. Evidence trail:
|
||||
//
|
||||
// Phase 2 experimented with per-vertex `emissive + ambient + diffuse×sun`
|
||||
// lighting driven from the Surface.Luminosity field. The Phase 3a live
|
||||
// verification (2026-04-23, user-observed against retail side-by-side
|
||||
// at MorntideAndHalf) produced a "blue-green-yellow sweep" across the
|
||||
// sky in acdream while retail showed a clean blue sky with white clouds.
|
||||
// That's the signature of `diffuse × (250,215,151) warm-gold sunColor`
|
||||
// tinting the cloud mesh's west-facing faces — retail does NOT do this.
|
||||
// docs/research/2026-04-23-sky-material-state.md
|
||||
// §Q2 — retail FUN_0059da60 writes D3DMATERIAL9 per-mesh:
|
||||
// Material.Emissive.rgb = (Surface.Luminosity, Lum, Lum, 1)
|
||||
// Material.Ambient/Diffuse from texture-modulate defaults
|
||||
// §Q4 — D3DRS_LIGHTING is ON for sky meshes
|
||||
// §Q6 — fragment formula:
|
||||
// lit = Emissive
|
||||
// + material.Ambient × light.Ambient
|
||||
// + material.Diffuse × light.Diffuse × max(dot(N, -sun), 0)
|
||||
//
|
||||
// Retail sky meshes render UNLIT. The time-of-day color variation users
|
||||
// observe (purple haze at night, warm dusk) comes from SkyObjectReplace
|
||||
// per-keyframe Luminosity + Transparent modulation, revealing/dimming
|
||||
// different mesh layers — NOT from per-vertex ambient multiply.
|
||||
// Our `uAmbientColor` = retail's light.Ambient (AmbColor × AmbBright,
|
||||
// pre-multiplied by SkyDescLoader). `uSunColor` = retail's light.Diffuse
|
||||
// (DirColor × DirBright). `uSunDir` is a unit vector FROM surface TO
|
||||
// sun (so `dot(N, uSunDir)` is the diffuse intensity directly; no
|
||||
// extra negation needed — see SkyStateProvider.SunDirectionFromKeyframe).
|
||||
// `uEmissive` is Surface.Luminosity for this submesh.
|
||||
//
|
||||
// See `docs/research/2026-04-23-sky-retail-verbatim.md` §6 for the
|
||||
// surviving hypotheses and the Phase 3b decision rationale.
|
||||
// Phase 2 (2026-04-23) tried the same formula and produced a visible
|
||||
// east/west "blue-green-yellow sweep" — in hindsight that was CORRECT
|
||||
// retail behaviour but paired with a wrong DayGroup pick ("Sunny" with
|
||||
// sharp warm sun when retail rolled "Cloudy" with diffuse overcast).
|
||||
// After Phase 3g fixed the LCG multiplier so acdream + retail agree on
|
||||
// the DayGroup, the same formula should now match retail visually.
|
||||
//
|
||||
// Uniforms for Ambient/Sun/Emissive stay declared below so the C#-side
|
||||
// plumbing doesn't need to change — they are simply UNUSED. A future
|
||||
// phase can revive them if the decompile hunt proves retail applies
|
||||
// lighting to sky through a different channel.
|
||||
// NOTE: no clamp at the vertex — retail's D3D fixed-function lighting
|
||||
// can produce lit values > 1.0 and the final clamp happens at the
|
||||
// framebuffer write. Doing that same "let it overbright" here keeps
|
||||
// the dome's emissive=1 saturation path intact.
|
||||
|
||||
layout(location = 0) in vec3 aPos;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
|
|
@ -31,16 +40,29 @@ uniform mat4 uSkyView;
|
|||
uniform mat4 uSkyProjection;
|
||||
uniform vec2 uUvScroll;
|
||||
|
||||
// Unused in Phase 3b — see header. Kept for forward-compat with the
|
||||
// C# renderer's push calls.
|
||||
uniform vec3 uAmbientColor;
|
||||
uniform vec3 uSunColor;
|
||||
uniform vec3 uSunDir;
|
||||
// Per-frame lighting (from SkyKeyframe):
|
||||
uniform vec3 uAmbientColor; // AmbColor × AmbBright (retail light.Ambient)
|
||||
uniform vec3 uSunColor; // DirColor × DirBright (retail light.Diffuse)
|
||||
uniform vec3 uSunDir; // unit vector FROM surface TO sun
|
||||
|
||||
// Per-submesh (from Surface.Luminosity float):
|
||||
uniform float uEmissive;
|
||||
|
||||
out vec2 vTex;
|
||||
out vec3 vTint;
|
||||
|
||||
void main() {
|
||||
vTex = aTex + uUvScroll;
|
||||
gl_Position = uSkyProjection * uSkyView * uModel * vec4(aPos, 1.0);
|
||||
|
||||
// uModel for sky is pure rotation (Z then Y) — orthonormal, so
|
||||
// mat3(uModel) transforms normals correctly without inverse-transpose.
|
||||
vec3 worldNormal = normalize(mat3(uModel) * aNormal);
|
||||
|
||||
// Retail per-vertex fixed-function lighting (AMBIENT=0 globally,
|
||||
// so the global ambient term drops; only light.Ambient contributes).
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue