diff --git a/src/AcDream.App/Rendering/Shaders/sky.frag b/src/AcDream.App/Rendering/Shaders/sky.frag index 7015957..95eaaeb 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.frag +++ b/src/AcDream.App/Rendering/Shaders/sky.frag @@ -16,16 +16,18 @@ // // See `docs/research/2026-04-23-sky-material-state.md`. -in vec2 vTex; -in vec3 vTint; +in vec2 vTex; +in vec3 vTint; +in float vFogFactor; // 1 = no fog (near), 0 = full fog color (far) out vec4 fragColor; uniform sampler2D uDiffuse; uniform float uTransparency; // 0 = fully visible, 1 = fully transparent uniform float uLuminosity; // SkyObjectReplace.Luminosity override (0..1) -// Shared SceneLighting UBO — only need the fog-flash channel for -// client-driven lightning strobes; sun/ambient already baked into vTint. +// Shared SceneLighting UBO — fog params drive the mix, flash channel +// bumps sky brightness during lightning strikes. Matches sky.vert's +// declaration exactly. struct Light { vec4 posAndKind; vec4 dirAndRange; @@ -46,14 +48,24 @@ void main() { // Composite: texture × per-vertex lit × per-keyframe dim. vec3 rgb = sampled.rgb * vTint * uLuminosity; - // Lightning additive bump (client-driven during storm keyframes). + // Retail vertex fog: lerp(fogColor, scene, fogFactor). At distant + // horizon dome vertices (distance > FOGEND) the sky saturates to + // the keyframe's WorldFogColor — that's retail's horizon-glow + // mechanism at dusk/dawn. See docs/research/2026-04-23-sky-fog.md. + rgb = mix(uFogColor.rgb, rgb, vFogFactor); + + // Lightning additive bump — client-driven during storm flashes. + // NOTE: the exact retail mechanism for lightning visual is still + // under research (agent #5, 2026-04-23). Keeping the uFogParams.z + // channel wired so if it ends up being a per-frame flash uniform + // that's what it becomes; if lightning turns out to be a particle + // system effect instead, this bump becomes a no-op (flash stays 0). float flash = uFogParams.z; rgb += flash * vec3(1.5, 1.5, 1.8); - // 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. + // Normal-frame cap at 1.0 (retail D3D framebuffer clamps per-channel + // on output). Flash relaxes ceiling to 3.0 so storm strobes blow + // out visibly. float cap = mix(1.0, 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 48e5987..1a2427f 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.vert +++ b/src/AcDream.App/Rendering/Shaders/sky.vert @@ -48,8 +48,25 @@ uniform vec3 uSunDir; // unit vector FROM surface TO sun // Per-submesh (from Surface.Luminosity float): uniform float uEmissive; +// Shared SceneLighting UBO — we need uFogParams.xy (fog start/end) to +// compute the vertex fog factor. Must match sky.frag's declaration. +struct Light { + vec4 posAndKind; + vec4 dirAndRange; + vec4 colorAndIntensity; + vec4 coneAngleEtc; +}; +layout(std140, binding = 1) uniform SceneLighting { + Light uLights[8]; + vec4 uCellAmbient; + vec4 uFogParams; // x=fogStart, y=fogEnd, z=flash, w=fogMode + vec4 uFogColor; + vec4 uCameraAndTime; +}; + out vec2 vTex; out vec3 vTint; +out float vFogFactor; // 1 = no fog (close), 0 = full fog (far) void main() { vTex = aTex + uUvScroll; @@ -72,4 +89,27 @@ void main() { + uAmbientColor // material.Ambient(1) × light.Ambient + uSunColor * diff; // material.Diffuse(1) × light.Diffuse × N·L vTint = clamp(lit, 0.0, 1.0); + + // Retail vertex-fog in 3D-range mode (FOGVERTEXMODE=LINEAR, + // RANGEFOGENABLE=1, FOGTABLEMODE=NONE per device init — never + // toggled per frame). Distance = `|worldPos - cameraPos|`. Since + // our sky view matrix has translation zeroed (sky is camera- + // centered), the post-uModel position IS the camera-relative + // world-space vector, so its length is the 3D range distance. + // See docs/research/2026-04-23-sky-fog.md. + // + // Formula: fogFactor = clamp((fogEnd - dist) / (fogEnd - fogStart), 0, 1) + // 1.0 → no fog contribution (scene color wins) + // 0.0 → full fog color (sky color fades to fog) + // + // Sky meshes have intrinsic radii in the thousands of meters (dome + // / stars / moon are authored at large distances in the dat); at + // typical keyframe FOGEND=2400m, the dome saturates to fogColor at + // its horizon band. THAT is how retail colors the horizon at dusk. + vec4 worldPos = uModel * vec4(aPos, 1.0); + float dist = length(worldPos.xyz); + float fogStart = uFogParams.x; + float fogEnd = uFogParams.y; + float span = max(fogEnd - fogStart, 1e-3); + vFogFactor = clamp((fogEnd - dist) / span, 0.0, 1.0); }