The uncommitted uTint=AmbientColor-for-alpha-submeshes experiment (from
the 2026-04-22 inference) dimmed the sky dome's baked gradient — a
user-verified visual regression. Reverting to the eeae83a baseline
(uTint=Vector4.One for every submesh) while we execute the proper
retail-verbatim port.
Research: three parallel decompile-hunt agents landed verifying
retail's ground-truth sky pipeline for the first time (prior audits
searched for stripped symbol names; the trail opened via the Region
dat-type-index 0x1c registration at chunk_00410000.c:12952). Key
retail functions now mapped in chunk_00500000.c:1097-7535:
- FUN_00501530: keyframe bracket-picker (with 1.0f wrap denominator)
- FUN_00501600: sun+ambient interpolator (sunVec = DirBright ×
(sin yaw·cos pit, cos yaw·cos pit, sin pit))
- FUN_00501860: fog interpolator
- FUN_00502820: SkyDesc::Unpack (2 doubles + DayGroup list)
- FUN_00502a10: build per-frame sky-object table
- FUN_00505f30: apply light state + per-cell AdjustPlanes relight
- FUN_005062e0: per-frame sky tick (throttled by LightTickSize)
- FUN_00508010: sky-object render loop (enqueues through the NORMAL
mesh pipeline via FUN_00514b90 — not a bespoke path)
Surprise findings:
- D3DRS_AMBIENT is set to 0 once at init and NEVER changes per-frame
(chunk_005A0000.c). The r12-inferred "clouds = texture × D3DRS_
AMBIENT" formula is falsified. Retail instead routes keyframe
AmbColor through per-vertex lighting on non-Luminous sky meshes
via _DAT_008682bc/c0/c4.
- Retail does NOT anchor the sky to the camera or use a separate
sky projection. Sky meshes live in world space and follow the
camera via scene-graph parent.
- FUN_00532440 (AdjustPlanes) re-lights every terrain cell on every
keyframe tick — the "terrain follows the sky" effect we don't yet
reproduce.
Phase 1 code change (this commit):
- src/AcDream.App/Rendering/Sky/SkyRenderer.cs: revert uTint to white
for all submeshes (the per-submesh blend split stays — sun gets
additive, clouds get alpha). Keep the `keyframe` parameter in the
signature for Phase 2 readiness. Comments now cite the retail
functions and reference docs instead of the (disproven) r12 formula.
- src/AcDream.Core/World/SkyDescLoader.cs: ACDREAM_DUMP_SKY=1 logs
the entire Region SkyDesc on load — DayGroups, SkyObjects, every
SkyTimeOfDay keyframe, and every SkyObjectReplace with RAW pre-/100
Transparent/Luminosity/MaxBright values so we can settle the unit
question empirically.
- src/AcDream.App/Rendering/Sky/SkyRenderer.cs: ACDREAM_DUMP_SKY=1
additionally logs each sky GfxObj's Surfaces and their SurfaceType
flags on first load, so we can identify which meshes carry the
Luminous bit (dome? sun? moon? stars?) vs which are lit.
- src/AcDream.App/Rendering/GameWindow.cs: passes the interpolated
keyframe to the sky renderer (kept — needed for Phase 2).
Research docs (pushed as part of this commit):
- docs/research/2026-04-23-sky-retail-verbatim.md: full synthesis
with retail function map, struct layouts, globals, pseudocode, and
a 4-phase port plan.
- docs/research/2026-04-23-sky-decompile-hunt-{A,B,C}.md: raw hunt
outputs.
- docs/research/2026-04-23-sky-references-crossref.md: WorldBuilder/
ACE/ACViewer/holtburger/Chorizite coverage.
- docs/research/2026-04-23-sky-dat-schema.md: full dat schema + unit
analysis.
- docs/research/2026-04-22-sky-lighting-decompile.md: prior agent's
(superseded) inference — kept for provenance.
Phase 2 will port Surface.Luminous-flag-aware per-vertex lighting for
sky submeshes once the dump resolves the open questions (Luminous-flag
distribution per Dereth sky mesh; _DAT_007a1870 scale constant value).
Build + 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
251 lines
7.7 KiB
Markdown
251 lines
7.7 KiB
Markdown
# 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.
|
||
|