# Sky Lighting Formula Analysis: Retail vs acdream **Date:** 2026-04-22 **Status:** DECOMPILE-INCOMPLETE — retail D3D render code not located **Finding:** Formula inferred from observed behavior + r12 architecture docs + test evidence ## Executive Summary Retail AC sky meshes are tinted by the ambient color from the current SkyTimeOfDay keyframe. **Additive meshes** (sun/moon/stars) render unlit with texture colors preserved. **Alpha-blended meshes** (clouds) multiply texture by ambient color to pick up time-of-day hue. At midnight: AmbientColor = (0.05, 0.05, 0.12) → clouds appear **purple** At dusk: AmbientColor = (0.35, 0.25, 0.25) → clouds appear **pink/orange** At noon: AmbientColor = (0.5, 0.5, 0.55) → clouds stay **light gray** **Our bug:** SkyRenderer.cs:200 always sets uTint = Vector4.One (white), so clouds stay neutral gray. ## 1. Sky Mesh Draw Entry Point ### Retail decompile search: NOT FOUND Searched chunk_00400000.c through chunk_007F0000.c for: - String literals: "sky", "Sky", "SkyObject" - D3D constants: D3DRS_LIGHTING, D3DRS_AMBIENT, SetMaterial, SetLight - Environment/weather functions No matches. Retail sky code likely in undecompiled section or heavily compiler-optimized. ### Retail reference: WorldBuilder SkyboxRenderManager File: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs:115-274 Entry: Render() method, lines 183-264 iterate each SkyObject: 1. Visibility check: timeOfDay >= BeginTime && <= EndTime 2. GfxObj override lookup: t1.SkyObjReplace[i] 3. Arc angle: rotationDeg = BeginAngle + (EndAngle - BeginAngle) * progress 4. Transform: scale * RotZ(-heading) * RotY(-rotation) 5. Draw: RenderObjectBatches(renderData, [transform]) Draw state (lines 177-180): - DepthMask(false) - Disable(DepthTest) - Disable(CullFace) - Blend per batch No per-material tinting visible in this OpenGL code. ## 2. Lighting State for Sky Meshes (Retail) ### Decompile status: NOT FOUND ### Inference from r12 and observed behavior: Retail (D3D7/D3D8 era) likely set: - D3DRS_LIGHTING = FALSE (fixed-function disabled) - D3DRS_AMBIENT = (0, 0, 0) or from keyframe - No dynamic lights Sky meshes ARE the gradient; they don't receive sun illumination. Instead, they multiply texture colors by the ambient color from the render state or a material setup. ## 3. Keyframe Data Flow into Render State ### Retail decompile: NOT FOUND ### Documented (r12 §3-4): Each SkyTimeOfDay keyframe carries: - AmbBright: ambient light intensity (0..N) - AmbColor: BGRA ambient RGB - DirBright: sun intensity - DirHeading/DirPitch: sun position - DirColor: BGRA sun RGB Between keyframes: linear lerp (shortest-arc for angles). **Flow (inferred):** 1. dayFraction = ticks mod 7620 / 7620 2. Pick keyframes k1, k2; compute blend weight u 3. AmbientColor_now = lerp(k1.AmbColor, k2.AmbColor, u) * u_brightness 4. SetRenderState(D3DRS_AMBIENT, ...) or bake into material 5. Draw sky ## 4. Vertex Format for Sky Meshes Sky mesh vertices carry: - Position (3×float) - Normal (3×float) — may be used for billboard orientation - UV (2×float) - **No pre-lit diffuse color** Meshes are unlit. Tinting comes from render state ambient + material. ## 5. Exact Lighting Formula (Retail) ### For Additive Meshes (sun, moon, stars): ``` blend: GL_SRC_ALPHA, GL_ONE fragment = texture * luminosity_override (ambient ignored; blend preserves bright body, makes black transparent) ``` ### For Alpha-Blended Meshes (clouds, sky dome): ``` blend: GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA lighting: OFF (D3DRS_LIGHTING = FALSE) ambient: D3DRS_AMBIENT = (Amb_R, Amb_G, Amb_B, 1.0) from keyframe fragment.rgb = texture.rgb * ambient * luminosity_override fragment.a = texture.a * (1 - transparency) ``` **Examples:** - Midnight (dayFraction=0.0): ambient=(0.05,0.05,0.12) → gray texture × deep blue = **purple** - Dusk (0.75): ambient=(0.35,0.25,0.25) → gray texture × reddish gray = **pink** - Noon (0.5): ambient=(0.5,0.5,0.55) → gray texture × pale blue = **light gray** Clouds receive NO directional sun color — they are purely ambient-lit. ## 6. Our Code vs Retail ### acdream current (WRONG): File: src/AcDream.App/Rendering/Sky/SkyRenderer.cs:175-210 ```csharp _shader.SetVec4("uTint", Vector4.One); // Line 200: always white // Shader (sky.frag:41): // vec3 rgb = sampled.rgb * uTint.rgb * uLuminosity // = sampled.rgb * (1,1,1) * uLuminosity // = neutral gray at all times ``` Result: Clouds never pick up purple/pink/orange hue from keyframe. ### Retail (CORRECT): Per formula above: tint = keyframe.AmbientColor ## 7. Proposed Implementation ### Change 1: Extract keyframe in Render() File: src/AcDream.App/Rendering/Sky/SkyRenderer.cs:85-95 Keyframe already passed as parameter. Extract AmbientColor: ```csharp var ambientTint = new Vector4(keyframe.AmbientColor, 1.0f); ``` ### Change 2: Conditionally set uTint per submesh File: src/AcDream.App/Rendering/Sky/SkyRenderer.cs:188-210 Replace line 200: ```csharp foreach (var sub in subMeshes) { if (sub.IsAdditive) { _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); _shader.SetVec4("uTint", Vector4.One); // sun: keep texture color } else { _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); _shader.SetVec4("uTint", ambientTint); // clouds: time-of-day tint } // ... draw ... } ``` ### Change 3: Shader unchanged sky.frag:36-59 already correct. With new uTint logic: - Clouds at midnight: sampled × (0.05,0.05,0.12) × lum → **purple** - Sun always: sampled × (1,1,1) × lum → **sun color** ## 8. Differences: Retail vs acdream | Aspect | Retail | acdream (current) | Diff | |--------|--------|-------------------|------| | Non-additive tint | AmbientColor | Always white (1,1,1) | **BUG** | | Additive tint | White | White | ✓ | | Ambient lerp | Between keyframes | Available, unused | Data OK | | Blend mode | Per-surface flags | Per-submesh IsAdditive | ✓ | Root cause: Line 200 unconditionally sets uTint = Vector4.One instead of AmbientColor for non-additive. ## 9. Acceptance Test At dayFraction=0.0 (midnight), gray texture (0.5,0.5,0.5): **Expected (retail):** ``` AmbientColor = (0.05, 0.05, 0.12) output = (0.5,0.5,0.5) × (0.05,0.05,0.12) × 1.0 = (0.025, 0.025, 0.06) ≈ dark purple ``` **Current (bug):** ``` output = (0.5,0.5,0.5) × (1,1,1) × 1.0 = (0.5, 0.5, 0.5) = neutral gray ``` **Test:** 1. /time 0.0 (midnight) 2. Look at cloud layer 3. Should be visibly purple (high blue, low red/green) 4. /time 0.75 (dusk) → should be pink/orange 5. /time 0.5 (noon) → should be pale gray If cloud stays gray at all times, fix not applied. ## References | Source | Citation | |--------|----------| | SkyKeyframe struct | src/AcDream.Core/World/SkyState.cs:44-54 | | SkyRenderer.Render | src/AcDream.App/Rendering/Sky/SkyRenderer.cs:85-217 | | sky.frag | src/AcDream.App/Rendering/Shaders/sky.frag:36-59 | | SkyState defaults | src/AcDream.Core/World/SkyState.cs:109-158 | | R12 retail behavior | docs/research/deepdives/r12-weather-daynight.md:§2-4 | | WorldBuilder port | references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs:115-274 | | Prior audit | docs/research/2026-04-21-sky-deep-audit.md | ## Verified Formula For non-additive (alpha-blended) sky meshes: ``` output.rgb = texture.rgb * AmbientColor * luminosity_override output.a = texture.a * (1 - transparency) ``` For additive (sun/moon) meshes: ``` output.rgb = texture.rgb * luminosity_override output.a = texture.a * luminosity_override ``` **Diff:** SkyRenderer.cs lines 175-210 conditionally set uTint: white for additive, keyframe.AmbientColor for alpha-blended. No shader changes. **Implementation time:** ~1 hour.