acdream/docs/research/2026-04-23-sky-references-crossref.md
Erik 58afd4850f sky(phase-1): revert speculative tint, add ACDREAM_DUMP_SKY diagnostic
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>
2026-04-23 18:06:52 +02:00

191 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sky rendering: cross-reference across all four references
**Date:** 2026-04-23
**Status:** research / no code change
**Purpose:** Verify (or refute) audit claims about how WorldBuilder, ACViewer, ACE, holtburger, and Chorizite.ACProtocol handle sky rendering and weather/time wire traffic, so acdream's sky implementation has a defensible reference baseline.
## 0. Headline findings
1. **WorldBuilder does not render the sky at runtime today.** The call to `_skyboxManager?.Render(...)` is commented out in `GameScene.cs:959`. The class exists; the code is dead. Any "what does WorldBuilder do" answer is therefore "what would it do if re-enabled," read from the class.
2. **No weather or time message exists on the wire.** ACE has exactly one environment-related message, `AdminEnvirons (0xEA60)`, carrying a single `EnvironChangeType` byte (fog color OR sound). Chorizite.ACProtocol's `protocol.xml` confirms the same single-field layout. holtburger does not parse or send it (its opcode is commented out). **Time-of-day is client-only**, driven from the `GameTime` and `SkyDesc` dat files.
3. **ACViewer has no sky renderer.** The Entity/Sky*.cs files are tree-view inspectors that format field values into UI nodes. There is no draw call.
4. **WorldBuilder's sky class ignores keyframe colours entirely** — it passes `SunlightColor = Vector3.Zero`, `AmbientColor = Vector3.One` to a shared lighting UBO and re-uses the generic `StaticObject` shader. Sky meshes render at raw texture colour, fully bright, with no per-batch blend mode and no per-keyframe tint. This confirms the audit claim.
## 1. WorldBuilder render pipeline end-to-end
File: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs`
### 1.1 Shader used
Line 446 of `GameScene.cs`: `_skyboxManager.Initialize(_sceneryShader, _graphicsDevice.SceneDataBuffer);`. The shader is `_sceneryShader`, created at `GameScene.cs:333` as `StaticObject` (files `Shaders/StaticObject.vert` + `.frag`). There is **no dedicated sky shader.**
### 1.2 Per-frame GL state (SkyboxRenderManager.cs)
- `DepthMask(false)` — L178
- `Disable(DepthTest)` — L179
- `Disable(CullFace)` — L180
- Blend state is **never touched** — it is whatever `GameScene.Render` set before (L845: `SrcAlpha / OneMinusSrcAlpha`, `BlendEquation FuncAdd`, and `Enable(Blend)` inherited from L844).
- Depth test + depth mask restored at L271272.
### 1.3 Per-object transform (L244250)
```
transform = Scale(1.0) * RotZ(-headingRad) * RotY(-rotationRad)
```
Built from `SkyObject.BeginAngle/EndAngle` lerped by day-fraction progress (L217240). `headingDeg` comes from `SkyObjectReplace.Rotate` if set, else 0.
### 1.4 Shader inputs (L143156)
The only uniform set for sky is `uRenderPass = SinglePass (2)` (L159). Scene UBO is filled with:
```
SunlightColor = Vector3.Zero
AmbientColor = Vector3.One
LightDirection = regionInfo.LightDirection (unused for sky — clouds have no sun)
```
The `StaticObject.vert` computes (line 54):
`LightingColor = clamp(uAmbientColor + uSunlightColor * diff + 0.15, 0.0, 1.0)`
With `ambient=1, sun=0`, this clamps to `1.0`. The sky fragment (line 34) then does `color.rgb *= LightingColor = 1.0`. **No keyframe tint reaches the shader.**
### 1.5 Per-submesh batches (RenderObjectBatches, L276325)
For each batch it:
1. Disables `CullFace` again (L303)
2. Sets `aTextureIndex` vertex attribute (L306) — "pick layer"
3. Binds the texture array and the sampler (wrap vs clamp) based on `batch.HasWrappingUVs`
4. `DrawElementsInstancedBaseVertex`
It **never inspects `batch.IsAdditive`** even though that flag exists on `ObjectRenderBatch` (defined `ObjectMeshManager.cs:177`). No `BlendFunc` call. No per-surface material.
**Conclusion:** The audit claim "WorldBuilder does not call `BlendFunc` per batch" is correct. The sky draws with whatever blend state was inherited from the previous pass (default `SrcAlpha/OneMinusSrcAlpha`) and uses the scenery shader's full-bright path. It is architecturally unfinished, which is why `GameScene.cs:959` comments the whole call out.
## 2. ACViewer sky handling
ACViewer is a tree-view DAT inspector. The four Sky files:
- `Entity/SkyDesc.cs``BuildTree()` produces UI nodes (TickSize, LightTickSize, DayGroups)
- `Entity/SkyTimeOfDay.cs``BuildTree()` produces nodes for Begin, DirBright/Heading/Pitch/Color, AmbBright/Color, MinWorldFog, MaxWorldFog, WorldFogColor, WorldFog, SkyObjReplace list
- `Entity/SkyObject.cs` — ditto for sky object properties
- `Entity/SkyObjectReplace.cs` — ditto for override records
A content-search over `ACViewer/Render/` for "SkyDesc", "SkyInfo", "TimeOfDay", "LightIntensity", "DayGroup" returns zero matches. **ACViewer does not render the sky.** Not a useful reference for the renderer side.
## 3. ACE weather/time wire protocol
### 3.1 Complete inventory of environment messages
`references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageAdminEnvirons.cs` — the only environment-related server-to-client message in the whole ACE project:
```csharp
public GameMessageAdminEnvirons(Session session, EnvironChangeType environChange = EnvironChangeType.Clear)
: base(GameMessageOpcode.AdminEnvirons, GameMessageGroup.UIQueue, 8)
{
Writer.Write((uint)environChange); // L10
}
```
Opcode: `AdminEnvirons = 0xEA60` (`GameMessageOpcode.cs:38`). Payload: one `uint` = `EnvironChangeType`. That's the entire message.
### 3.2 EnvironChangeType enumeration
`references/ACE/Source/ACE.Entity/Enum/EnvironChangeType.cs`:
```
Clear=0x00, RedFog=0x01, BlueFog=0x02, WhiteFog=0x03, GreenFog=0x04,
BlackFog=0x05, BlackFog2=0x06,
RoarSound=0x65, BellSound=0x66, Chant1Sound=0x67, Chant2Sound=0x68,
DarkWhispers1Sound=0x69, DarkWhispers2Sound=0x6A, DarkLaughSound=0x6B,
DarkWindSound=0x6C, DarkSpeechSound=0x6D, DrumsSound=0x6E,
GhostSpeakSound=0x6F, BreathingSound=0x70, HowlSound=0x71,
LostSoulsSound=0x72, SquealSound=0x75,
Thunder1Sound=0x76..Thunder6Sound=0x7B
```
Extension methods: `IsFog` (≤ 0x06), `IsSound` (≥ 0x65). The enum is **exactly two concepts** in one message: a fog-colour override OR a one-shot environmental sound cue (including thunder). **There is no lightning flash opcode.** Thunder is audio-only.
### 3.3 No TimeSync / GameTime / Weather messages
`references/ACE/Source/ACE.DatLoader/Entity/GameTime.cs` parses a DAT structure — `ZeroTimeOfYear`, `ZeroYear`, `DayLength`, `DaysPerYear`, `YearSpec`, `TimesOfDay[]`, `DaysOfTheWeek[]`, `Seasons[]`. This is loaded from `client_portal.dat`, not sent over the wire. Content-search across the ACE codebase for "TimeSync", "DayFraction", "SendTime", "class.*Weather", "SyncTime" returns zero hits in the network layer. **The server has no time-of-day or weather channel.** The client computes day fraction locally from `GameTime.ZeroTimeOfYear + elapsed`.
### 3.4 `SendEnvironChange` usage
`references/ACE/Source/ACE.Server/WorldObjects/Player_Networking.cs:399``SendEnvironChange(EnvironChangeType)` wraps `Session.Network.EnqueueSend(new GameMessageAdminEnvirons(...))`. Called from admin/content commands; not driven by a time or weather simulation.
## 4. holtburger
- `crates/holtburger-protocol/src/opcodes.rs:192``AdminEnvirons = 0xEA60` is **commented out**.
- Content-search across the `holtburger` crates for `EnvironChange`, `Thunder`, `SkyDesc`, `DayGroup`, `TimeOfDay`, "weather" (non-commented), "sky" returns no rendering or parsing code.
- holtburger is a TUI client; it has no notion of sky and discards whatever `0xEA60` packets arrive (no handler). No reference value for our sky port.
## 5. Chorizite.ACProtocol field documentation
### 5.1 No Sky types in the protocol
The generated `Types/` directory has no `SkyDesc.*`, `SkyObject.*`, `SkyTimeOfDay.*` files (glob confirmed). Sky is a DAT structure, not a wire message — Chorizite correctly omits it.
### 5.2 Admin_Environs (protocol.xml:8236)
Direct quote:
```xml
<type name="Admin_Environs" queue="UIQueue" text="This appears to be an admin command to change the environment (light, fog, sounds, colors)" type="0xEA60" lastUpdater="zegeger" lastUpdate="20170913">
<field type="EnvrionChangeType" name="EnvrionOption" text="Id of option set to change the environs" />
</type>
```
One field. Confirms ACE's layout byte-for-byte.
### 5.3 EnvrionChangeType enum (generated: `Enums/EnvrionChangeType.generated.cs`)
The XML summary: `"The EnvrionChangeType identifies the environment option set."` Each value carries a comment — Clear ("Removes all overrides"), RedFog ("Sets Red Fog"), …, Thunder1Sound…Thunder6Sound ("Play Thunder1 Sound" … "Play Thunder6 Sound"). No lightning flash. No tint field. No colour override beyond the six fog presets. Admin-only per the protocol doc.
### 5.4 `DisableMostWeatherEffects` player option (protocol.xml:1372)
```
<value name="DisableMostWeatherEffects" value="0x00010000" />
```
This is a **client-side player preference bit** inside the gameplay options bitfield, not a server control. The fact this option is client-owned is another strong signal that all weather/sky simulation happens on the client from dat data.
## 6. Cloud-tint origin (answer to "what drives clouds purple at dusk?")
- **Not from the wire.** Nothing in ACE, Chorizite, or holtburger sends a per-object tint.
- **Not from WorldBuilder's reference.** Its renderer is turned off and even when turned on would pass white.
- **Not from `SkyObjectReplace`.** Its only colour-ish fields are `Transparent`, `Luminosity`, `MaxBright` — scalar brightness/alpha, no RGB. (`SkyObjectReplace.cs:9-12` in ACE.DatLoader and the identical struct in ACViewer.)
- **Source of truth per the DAT:** `SkyTimeOfDay.AmbColor`/`AmbBright` (`ACE.DatLoader/Entity/SkyTimeOfDay.cs:13-16`). The per-keyframe `AmbColor` (BGRA) × `AmbBright` is the ambient lighting used by retail. Our r12 deepdive and `2026-04-22-sky-lighting-decompile.md` infer that retail D3D set this as `D3DRS_AMBIENT` and rendered clouds unlit, so texture × ambient = clouds' time-of-day colour. This matches observed retail behaviour (purple midnight, warm tan dawn).
The upshot: acdream's current `SkyRenderer.cs:220222` ("alpha-blended submeshes tinted by `keyframe.AmbientColor`") is **architecturally correct** and ahead of every open-source reference on this point. The only code that does anything with these keyframe colours in the entire open-source AC ecosystem is ours.
## 7. Lightning / storm flash
- No GameMessage opcode for a lightning flash (checked ACE and Chorizite exhaustively).
- Thunder is audio-only (`Thunder1Sound`..`Thunder6Sound` = 0x76..0x7B via `AdminEnvirons`).
- Retail's visible lightning flash is therefore **client-driven** — triggered by the current sky/weather keyframe state in the DAT, not by a server push. This is not implemented in WorldBuilder, ACViewer, or holtburger.
## 8. Matrix: which keyframe fields reach the render
Legend: **W** = WorldBuilder SkyboxRenderManager (class as-written; remember it is not actually called), **V** = ACViewer, **A** = acdream `SkyRenderer.cs`.
| Keyframe field | W | V | A |
|------------------------------|-----------|--------------|---------------------------------------------------|
| `DirColor` (sun RGB) | ignored | tree-only | not sampled in sky shader (it's for terrain/mesh) |
| `DirBright` | ignored | tree-only | not sampled in sky shader |
| `DirHeading` / `DirPitch` | ignored | tree-only | fed to `SceneLightingUbo` for scene, not sky |
| `AmbColor` | ignored | tree-only | **`uTint` on alpha submeshes** (`SkyRenderer:222`) |
| `AmbBright` | ignored | tree-only | premultiplied into `AmbientColor` in loader |
| `WorldFogColor` | ignored | tree-only | `SkyKeyframe.FogColor`, available but not in sky |
| `MinWorldFog` / `MaxWorldFog`| ignored | tree-only | present in keyframe, not yet consumed |
| `WorldFog` | ignored | tree-only | present, not consumed |
| **Per-replace: `Luminosity`**| ignored | tree-only | `uLuminosity` uniform |
| **Per-replace: `Transparent`**| ignored | tree-only | `uTransparency` uniform |
| **Per-replace: `MaxBright`** | ignored | tree-only | min-clamped into `luminosity` |
Every "ignored" cell is dead because WorldBuilder's sky class is itself dead (call commented out). If it were re-enabled, the StaticObject shader would still drop every colour because `SunlightColor=0, AmbientColor=1` is hard-coded in the sky UBO write.
## 9. Implications for acdream
1. **The audit claim is correct:** WorldBuilder does not drive the cloud tint from keyframe data, and does not call BlendFunc per batch. We cannot use WorldBuilder's output as an oracle for cloud colour.
2. **There is no reference client that renders the retail-style coloured sky.** acdream is extending beyond every peer. Our only ground truth is (a) retail behaviour observed in-game, (b) the `SkyTimeOfDay` field layout, (c) the r12 / 2026-04-22 deep-dives we authored.
3. **Nothing from the network informs sky state** beyond `AdminEnvirons` fog / sound presets. If we implement weather, it must be client-driven from the DAT's `Region.SkyInfo` + `GameTime`, with `AdminEnvirons` strictly applied as an override layer on top.
4. **Lightning flashes must be client-driven** from the current weather keyframe. No GameMessage exists to trigger them.
5. The per-submesh `IsAdditive` switch acdream already does (`SkyRenderer.cs:196-223`) is the right model — no reference does this, but it's the only sensible mapping from retail's mixed mesh surfaces (sun/moon additive, clouds alpha) to modern GL blend state.
## 10. File / line index
- WorldBuilder: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SkyboxRenderManager.cs:115-325`
- WorldBuilder sky call-site (commented): `references/WorldBuilder/Chorizite.OpenGLSDLBackend/GameScene.cs:957-962`
- WorldBuilder StaticObject shader: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Shaders/StaticObject.{vert,frag}`
- ACE wire message: `references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageAdminEnvirons.cs:1-13`
- ACE opcode: `references/ACE/Source/ACE.Server/Network/GameMessages/GameMessageOpcode.cs:38`
- ACE enum: `references/ACE/Source/ACE.Entity/Enum/EnvironChangeType.cs:1-48`
- ACE SkyDesc parse: `references/ACE/Source/ACE.DatLoader/Entity/SkyDesc.cs:1-22`
- ACE SkyTimeOfDay parse: `references/ACE/Source/ACE.DatLoader/Entity/SkyTimeOfDay.cs:1-47`
- ACE SkyObjectReplace parse: `references/ACE/Source/ACE.DatLoader/Entity/SkyObjectReplace.cs:1-26`
- ACE GameTime parse: `references/ACE/Source/ACE.DatLoader/Entity/GameTime.cs:1-39`
- ACViewer (no rendering): `references/ACViewer/ACViewer/Entity/Sky*.cs`
- Chorizite protocol: `references/Chorizite.ACProtocol/Chorizite.ACProtocol/protocol.xml:140, 8236-8238, 1909`
- Chorizite generated enum: `references/Chorizite.ACProtocol/Chorizite.ACProtocol/Enums/EnvrionChangeType.generated.cs`
- holtburger opcode (commented): `references/holtburger/crates/holtburger-protocol/src/opcodes.rs:191-192`
- Our SkyRenderer: `src/AcDream.App/Rendering/Sky/SkyRenderer.cs:85-234`
- Our SkyKeyframe: `src/AcDream.Core/World/SkyState.cs:48-50`