diff --git a/src/AcDream.App/Rendering/Shaders/sky.frag b/src/AcDream.App/Rendering/Shaders/sky.frag index 37e7401..8276be6 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.frag +++ b/src/AcDream.App/Rendering/Shaders/sky.frag @@ -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)); diff --git a/src/AcDream.App/Rendering/Shaders/sky.vert b/src/AcDream.App/Rendering/Shaders/sky.vert index 1d26ffd..87e011d 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.vert +++ b/src/AcDream.App/Rendering/Shaders/sky.vert @@ -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 }