# 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 L271–272.

### 1.3 Per-object transform (L244–250)
```
transform = Scale(1.0) * RotZ(-headingRad) * RotY(-rotationRad)
```
Built from `SkyObject.BeginAngle/EndAngle` lerped by day-fraction progress (L217–240). `headingDeg` comes from `SkyObjectReplace.Rotate` if set, else 0.

### 1.4 Shader inputs (L143–156)
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, L276–325)
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:220–222` ("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`
