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>
15 KiB
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
- WorldBuilder does not render the sky at runtime today. The call to
_skyboxManager?.Render(...)is commented out inGameScene.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. - No weather or time message exists on the wire. ACE has exactly one environment-related message,
AdminEnvirons (0xEA60), carrying a singleEnvironChangeTypebyte (fog color OR sound). Chorizite.ACProtocol'sprotocol.xmlconfirms 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 theGameTimeandSkyDescdat files. - 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.
- WorldBuilder's sky class ignores keyframe colours entirely — it passes
SunlightColor = Vector3.Zero,AmbientColor = Vector3.Oneto a shared lighting UBO and re-uses the genericStaticObjectshader. 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)— L178Disable(DepthTest)— L179Disable(CullFace)— L180- Blend state is never touched — it is whatever
GameScene.Renderset before (L845:SrcAlpha / OneMinusSrcAlpha,BlendEquation FuncAdd, andEnable(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:
- Disables
CullFaceagain (L303) - Sets
aTextureIndexvertex attribute (L306) — "pick layer" - Binds the texture array and the sampler (wrap vs clamp) based on
batch.HasWrappingUVs 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 listEntity/SkyObject.cs— ditto for sky object propertiesEntity/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:
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 = 0xEA60is commented out.- Content-search across the
holtburgercrates forEnvironChange,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
0xEA60packets 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:
<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 areTransparent,Luminosity,MaxBright— scalar brightness/alpha, no RGB. (SkyObjectReplace.cs:9-12in 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-keyframeAmbColor(BGRA) ×AmbBrightis the ambient lighting used by retail. Our r12 deepdive and2026-04-22-sky-lighting-decompile.mdinfer that retail D3D set this asD3DRS_AMBIENTand 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 viaAdminEnvirons). - 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
- 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.
- 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
SkyTimeOfDayfield layout, (c) the r12 / 2026-04-22 deep-dives we authored. - Nothing from the network informs sky state beyond
AdminEnvironsfog / sound presets. If we implement weather, it must be client-driven from the DAT'sRegion.SkyInfo+GameTime, withAdminEnvironsstrictly applied as an override layer on top. - Lightning flashes must be client-driven from the current weather keyframe. No GameMessage exists to trigger them.
- The per-submesh
IsAdditiveswitch 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