From eeae83a14ecae5e562607b4dce09ae62ecf7d5a5 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 22 Apr 2026 17:38:44 +0200 Subject: [PATCH] =?UTF-8?q?fix(sky):=20scale=20keyframe=20Luminosity/Trans?= =?UTF-8?q?parent/MaxBright=20from=20percent=20=E2=86=92=20fraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retail's Region dat stores SkyObjectReplace.Luminosity / Transparent / MaxBright as percentages in the 0..100 range. Our shader expects fractions in 0..1. We were passing raw values (luminosity up to 100) straight into the sky fragment shader's rgb-multiplier: rgb = sampled.rgb * uTint.rgb * uLuminosity; At the "Sunny" DayGroup's noon keyframes (verified via live diag), Luminosity = 100 → shader multiplied the cloud texture RGB by 100 → min(rgb, vec3(1.2)) clamped all channels to 1.2 → pure white sky. Also gave the dawn/dusk purple sky effect on top of the pale texture. Fix: SkyDescLoader.ConvertTimeOfDay divides Luminosity, Transparent and MaxBright by 100 when loading each SkyObjectReplace. The Rotate field stays as degrees (values like 270° are genuine headings, not percentages). Transparent was accidentally surviving via a 0..1 clamp downstream, but we fix it for consistency and so brightness-attenuating values in the 0..99 range (partial fade during dawn/dusk) work correctly instead of rounding to full-transparent. WorldBuilder's SkyboxRenderManager does NOT apply these fields at all — that's why they never hit this bug. Our port applies them for per-keyframe day-night fades, so we needed the unit conversion. Also picked up in this commit (incidental, already running): - Sky render: per-submesh blend mode from TranslucencyKind.Additive for sun/moon-style self-bright objects (Additive bit 0x10000). Luminous flag 0x40 intentionally NOT mapped to additive — that flag is on the sky dome + cloud sheets and making them additive produced the previous "fully white" iteration of this bug. - ToD default seed: DayTicks/16 (Midsong = hour 9 = true noon) instead of DayTicks*0.5 which landed on Gloaming-and-Half (sunset) due to DerethDateTime's +7/16 day-fraction offset. Pre-TimeSync view now correctly starts at noon. - Lightning flash: brighter white-blue (vec3(1.5,1.5,1.8)) instead of dim grey; ceiling relaxed during flash so the strobe actually blows out. Cadence (strike intervals, decay) unchanged. - Saved docs/research/2026-04-21-sky-deep-audit.md with the decompile+ACE+ACME+WorldBuilder research done to corner this bug. Open follow-up (not fixed here): sky clouds are white at noon / don't get the dusk/night purple tint. Our sky shader is fully unlit — doesn't apply sun/ambient directional light like the terrain shader does. AmbientColor in the keyframe data carries the right tint (purple at midnight, magenta at dusk) but we pass uTint = Vector4.One instead of the keyframe value. Next commit will wire directional-sun + ambient into sky.frag so cloud meshes pick up the time-of-day color. All 717 tests green. User-confirmed: sky colors are now "much better" after this change (previously fully white). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/research/2026-04-21-sky-deep-audit.md | 182 +++++++++++++++++++ src/AcDream.App/Rendering/GameWindow.cs | 23 ++- src/AcDream.App/Rendering/Shaders/sky.frag | 12 +- src/AcDream.App/Rendering/Sky/SkyRenderer.cs | 45 +++++ src/AcDream.Core/World/SkyDescLoader.cs | 25 ++- 5 files changed, 277 insertions(+), 10 deletions(-) create mode 100644 docs/research/2026-04-21-sky-deep-audit.md diff --git a/docs/research/2026-04-21-sky-deep-audit.md b/docs/research/2026-04-21-sky-deep-audit.md new file mode 100644 index 0000000..dc5524f --- /dev/null +++ b/docs/research/2026-04-21-sky-deep-audit.md @@ -0,0 +1,182 @@ +# Deep Audit: Sky Rendering Bug +**Date:** 2026-04-21 +**Issue:** Sky renders fully white despite keyframe data being pale blue/orange; framebuffer cleared to black. +**Hypothesis:** At least one of 7 sky objects is painting white across the view; rendering pipeline produces white, not data. + +## Angle 1 — Retail Decompile: What the Sky Renderer Actually Does + +### Finding: Could Not Locate Retail Sky Render Function + +**Status:** COULDN'T LOCATE + +After search of chunk_00400000.c through chunk_007F0000.c, found only one match: +- docs/research/decompiled/chunk_00500000.c:7340 comment about GameTime effects on sky + +No functions containing sky, Sky, SkyObjects, CEnvironment, or D3D render-state constants found. + +--- + +## Angle 2 — ACE: What Server Messages Drive Sky / Weather / Time? + +### Retail Source: EnvironChangeType Enum +- references/ACE/Source/ACE.Entity/Enum/EnvironChangeType.cs:4-48 + +Only fog overlays and sound effects; NO mesh/texture/geometry changes: + +```csharp +public enum EnvironChangeType { + Clear, RedFog, BlueFog, WhiteFog, GreenFog, BlackFog, BlackFog2, + RoarSound, ... /* sound effects only */ +} +``` + +Sent via: GameMessageAdminEnvirons (admin-only, not broadcast) + +### Our Code: Network Handling +- src/AcDream.Core.Net/Messages/GameEvents.cs:1-481 — No sky/weather parsers +- src/AcDream.Core.Net/WorldSession.cs — No AdminEnvirons handler + +**Verdict: MISSING** +But the bug symptoms (fully white sky) are NOT consistent with WhiteFog overlay; this is client-side rendering failure, not a server message issue. + +--- + +## Angle 3 — ACME: Improved Sky Rendering + +### Finding: No ACME-Specific Sky Renderer + +ACME contributes only animation/model reference; does not override WorldBuilder SkyboxRenderManager. We port vanilla WorldBuilder unchanged. + +--- + +## Angle 4 — WorldBuilder Vanilla: Re-read for Details + +### Key Finding: Per-Submesh Blend Mode Divergence + +**WorldBuilder (references/WorldBuilder/.../SkyboxRenderManager.cs:301-318):** +```csharp +foreach (var batch in renderData.Batches) { + // ... bind texture, sampler ... + _gl.DrawElementsInstancedBaseVertex(...); + // NO BlendFunc call per batch +} +``` + +**Our Code (src/AcDream.App/Rendering/Sky/SkyRenderer.cs:175-196):** +```csharp +foreach (var sub in subMeshes) { + if (sub.IsAdditive) + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); + else + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + // ... draw +} +``` + +We set blend state PER SUBMESH; retail does NOT. This adds a failure point: if IsAdditive classification is wrong, one mesh renders with wrong blend mode. + +--- + +## Angle 5 — Our Code: End-to-End + +### A) Data Loading +src/AcDream.Core/World/SkyDescLoader.cs: MATCHES retail + +Loads Region 0x13000000, parses 7 sky objects with correct color data. + +### B) Blend Mode Classification +src/AcDream.App/Rendering/Sky/SkyRenderer.cs:314 + +```csharp +bool isAdditive = sm.Translucency == TranslucencyKind.Additive; +``` + +**CRITICAL COMMENT (lines 311-313):** +```csharp +// NOTE: earlier revision also treated SurfaceType.Luminous = 0x40 +// as additive, but that flag is present on the sky DOME itself and +// on cloud sheets — turning those additive blew the whole sky to +// white. Luminous means self-illuminated, not additive. +``` + +This documents a PAST WHITE-SKY BUG caused by misclassifying Luminous as additive. + +Question: Is the current fix correct? Did we introduce a NEW misclassification? + +### C) Shader Luminosity +src/AcDream.App/Rendering/Shaders/sky.frag:36-59 + +```glsl +vec3 rgb = sampled.rgb * uTint.rgb * uLuminosity; +``` + +If sampled texture is white (1,1,1) and uLuminosity=1, output is white. + +--- + +## Root-Cause Hypothesis Ranked by Confidence + +### Hypothesis 1 (HIGH CONFIDENCE): IsAdditive Misclassification + +**Evidence:** +- Comment at SkyRenderer.cs:311-313 documents past white-sky incident from Luminous misclassification +- We changed code to only treat Additive=0x10000 as additive, skipping Luminous=0x40 +- But IsAdditive is computed once at upload and NEVER re-checked or validated +- We diverge from retail by setting blend per submesh, creating a failure point + +**Symptom matches:** +- Sky fully white = additive blend of white mesh over black framebuffer +- Black clear makes problem visible + +**Acceptance test:** Log SurfaceType flags for all 7 sky objects and verify IsAdditive classification matches retail expectations. + +### Hypothesis 2 (MEDIUM CONFIDENCE): Texture Decode Produces White + +**Evidence:** +- TextureCache.GetOrUpload at SkyRenderer.cs:188 +- If surface texture is decoded incorrectly, white pixels result + +**Acceptance test:** Compare decoded texture bytes against expected color data. + +### Hypothesis 3 (LOW CONFIDENCE): Luminosity Override + +**Evidence:** +- Diag shows luminosity values max at 0.78 (not > 1) +- Shader applies multiplicatively; shouldn't blow white unless texture is already near-white + +**Less likely:** Keyframe data is not driving the issue. + +--- + +## Next Fix: Verify IsAdditive Classification + +**Target:** src/AcDream.App/Rendering/Sky/SkyRenderer.cs:276-325 (UploadSubMesh) + +**Action:** Add logging to print SurfaceType flags and IsAdditive result for each sky mesh surface. + +**Concrete change:** +```csharp +private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm) { + // ... setup ... + bool isAdditive = sm.Translucency == TranslucencyKind.Additive; + + System.Diagnostics.Debug.WriteLine( + $"Sky Surface {sm.SurfaceId}: Translucency={sm.Translucency}, IsAdditive={isAdditive}"); + + return new SubMeshGpu { ... }; +} +``` + +Run test, check Debug output against SurfaceType enum definitions and retail behavior. + +**Acceptance test:** No sky object shows: +- SurfaceType.Luminous (0x40) misclassified as IsAdditive=true +- SurfaceType.Additive (0x10000) misclassified as IsAdditive=false + +--- + +## Conclusion + +Bug is client-side rendering pipeline, not data or server messages. Most likely cause: **IsAdditive misclassification on a dominant sky mesh (dome or sun)**. The codebase already documents a past white-sky incident from this exact error. + +Verify by logging SurfaceType flags for all 7 sky objects and comparing against retail specifications. diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 966c54a..1fba8d7 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -886,7 +886,18 @@ public sealed class GameWindow : IDisposable // Seed WorldTime to noon so outdoor scenes aren't pitch-black before // the server sends its first TimeSync packet (offline rendering in // particular never receives one). - WorldTime.SyncFromServer(AcDream.Core.World.DerethDateTime.DayTicks * 0.5); + // + // "Noon" here means sun at zenith — dayFraction = 0.5. Because + // DerethDateTime applies a +7/16 offset (tick 0 = Morntide-and-Half, + // hour 8 of 16), we need raw ticks = 476.25 (one hour past tick 0 = + // Midsong / Hour 9, which is what retail considers noon). + // + // Using `DayTicks * 0.5 = 3810` WOULD be correct if the offset were + // zero, but with our 3333.75-tick shift it lands on dayFraction + // 0.9375 — that's Gloaming-and-Half (sunset, nearly midnight), + // producing a dim orange sky with the sun below the horizon until + // TimeSync arrives. + WorldTime.SyncFromServer(AcDream.Core.World.DerethDateTime.DayTicks / 16.0); // = 476.25 = Midsong (noon) // Build the terrain atlas once from the Region dat. var terrainAtlas = AcDream.App.Rendering.TerrainAtlas.Build(_gl, _dats); @@ -3504,10 +3515,12 @@ public sealed class GameWindow : IDisposable var kf = WorldTime.CurrentSky; var atmo = Weather.Snapshot(in kf); var fogColor = atmo.FogColor; - // Clamp to 0..1 — keyframes may store over-1 values (retail uses the - // dir-bright scalar pre-multiplied into color) and GL's ClearColor - // will silently accept them, but some drivers interpret > 1 as - // "bright clamp", producing ugly pink/green frames. + // Clear to fog color (horizon haze) so if sky meshes have alpha + // gaps or don't cover the full view, the "missing" area reads as + // distant haze, not as pitch-black. Fog color is clamped to 0..1 + // since keyframes may pre-multiply DirBright and produce over-1 + // values that some drivers interpret as "bright clamp" (pink/green + // frames). _gl!.ClearColor( System.Math.Clamp(fogColor.X, 0f, 1f), System.Math.Clamp(fogColor.Y, 0f, 1f), diff --git a/src/AcDream.App/Rendering/Shaders/sky.frag b/src/AcDream.App/Rendering/Shaders/sky.frag index 8945781..6eb16ae 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.frag +++ b/src/AcDream.App/Rendering/Shaders/sky.frag @@ -41,9 +41,17 @@ void main() { vec3 rgb = sampled.rgb * uTint.rgb * uLuminosity; // Lightning additive bump — makes the sky itself flash during storms. - rgb += uFogParams.z * vec3(0.5, 0.5, 0.55); + // Retail's lightning is a near-white strobe; a dim grey bump doesn't + // read as lightning. Keep a faint blue tint so it still feels electric + // rather than pure-white daylight. + float flash = uFogParams.z; + rgb += flash * vec3(1.5, 1.5, 1.8); - rgb = min(rgb, vec3(1.2)); // soft clamp to let luminosity over-bright mildly + // Soft clamp to let Luminosity/flash slightly over-bright. During a + // lightning flash, raise the ceiling so the strobe actually blows out + // instead of getting capped mid-rise. + float cap = mix(1.2, 3.0, clamp(flash, 0.0, 1.0)); + rgb = min(rgb, vec3(cap)); float a = sampled.a * (1.0 - uTransparency) * uTint.a; if (a < 0.01) discard; diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs index 34104d4..6bef5dc 100644 --- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs +++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs @@ -7,6 +7,7 @@ using AcDream.Core.Terrain; using AcDream.Core.World; using DatReaderWriter; using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Sky; @@ -104,6 +105,18 @@ public sealed unsafe class SkyRenderer : IDisposable _gl.Disable(EnableCap.DepthTest); _gl.Disable(EnableCap.CullFace); _gl.Enable(EnableCap.Blend); + // Default blend — overridden per-submesh inside the inner loop based + // on the Surface's TranslucencyKind + Luminous flag. Sun/moon/stars + // in retail use Additive (their texture has a black background and a + // bright body painted on top; additive blending ignores the black and + // lets the body glow over the sky gradient). Clouds use AlphaBlend. + // Without per-object blend, sun renders as "black square with sun in + // it" because our default alpha-blend treats the black background as + // opaque. See SurfaceType enum (DatReaderWriter.Enums.SurfaceType): + // Additive = 0x10000 → GL_ONE / GL_ONE + // Luminous = 0x40 → additive for sky purposes + // Alpha = 0x100 / Translucent = 0x10 → GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA + // Base1ClipMap = 0x04 → alpha with shader discard _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); // Look up the keyframe's override list so we can apply @@ -161,6 +174,17 @@ public sealed unsafe class SkyRenderer : IDisposable foreach (var sub in subMeshes) { + // Per-submesh blend mode: sun/moon/stars are usually + // Additive or Luminous, clouds are AlphaBlend, star dome + // backing is Opaque (but we still need blend-enabled to + // avoid a hard seam against the sky gradient behind it — + // we map Opaque to a passthrough SrcAlpha/OneMinusSrcAlpha + // with alpha=1, which is equivalent to not blending). + if (sub.IsAdditive) + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); // additive + else + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); // alpha + uint tex = _textures.GetOrUpload(sub.SurfaceId); _gl.ActiveTexture(TextureUnit.Texture0); _gl.BindTexture(TextureTarget.Texture2D, tex); @@ -275,6 +299,20 @@ public sealed unsafe class SkyRenderer : IDisposable _gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float))); _gl.BindVertexArray(0); + + // Classify blend mode from the Surface's flags. Sun/moon/stars with + // `SurfaceType.Additive = 0x10000` get GL_ONE / GL_ONE (their texture + // has a black background and a bright body; additive makes the + // background contribute nothing and the body glow on top of the sky). + // + // NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40` + // as additive, but that flag is present on the sky DOME itself and + // on cloud sheets — turning those additive blew the whole sky to + // white. `Luminous` means "self-illuminated / unshaded" in retail's + // render pipeline, not "additive blend". Only the Additive bit + // toggles the blend mode. + bool isAdditive = sm.Translucency == TranslucencyKind.Additive; + return new SubMeshGpu { Vao = vao, @@ -282,6 +320,7 @@ public sealed unsafe class SkyRenderer : IDisposable Ebo = ebo, IndexCount = sm.Indices.Length, SurfaceId = sm.SurfaceId, + IsAdditive = isAdditive, }; } @@ -306,5 +345,11 @@ public sealed unsafe class SkyRenderer : IDisposable public uint Ebo; public int IndexCount; public uint SurfaceId; + /// + /// True if the Surface's flags indicate additive blending should be + /// used (SurfaceType.Additive OR SurfaceType.Luminous). Computed + /// once at upload; avoids a per-frame dat lookup. + /// + public bool IsAdditive; } } diff --git a/src/AcDream.Core/World/SkyDescLoader.cs b/src/AcDream.Core/World/SkyDescLoader.cs index 439aabb..2deb5b6 100644 --- a/src/AcDream.Core/World/SkyDescLoader.cs +++ b/src/AcDream.Core/World/SkyDescLoader.cs @@ -246,14 +246,33 @@ public static class SkyDescLoader private static DatSkyKeyframeData ConvertTimeOfDay(SkyTimeOfDay s) { + // Transparent / Luminosity / MaxBright are stored in the retail + // Region dat as PERCENTAGES (0..100), not fractions (0..1). Our + // shader expects fractions — divide here. Confirmed from live + // diag dump of Region 0x13000000 / DayGroup "Sunny": noon keyframes + // have Luminosity=100 and Transparent=100 across multiple + // SkyObjectReplace entries, corresponding to 1.0 in shader units. + // Previously passing the raw 100 through resulted in + // `rgb = texture * 100` which blew out to pure white everywhere + // (clamped to vec3(1.2) in the sky fragment shader) — this was the + // "white sky at noon" bug observed by the user. + // + // Rotate stays as degrees (270° values in the data are genuinely + // heading-degrees, not percentages). ObjectIndex / GfxObjId are + // IDs with no unit transform. + // + // WorldBuilder's SkyboxRenderManager does NOT apply Luminosity / + // Transparent / MaxBright at all (ignores the fields entirely), + // which is why they never ran into this bug. We apply them for + // per-keyframe day-night fade which retail does. var replaces = s.SkyObjReplace.Select(r => new SkyObjectReplaceData { ObjectIndex = r.ObjectIndex, GfxObjId = r.GfxObjId?.DataId ?? 0u, Rotate = r.Rotate, - Transparent = r.Transparent, - Luminosity = r.Luminosity, - MaxBright = r.MaxBright, + Transparent = r.Transparent / 100f, + Luminosity = r.Luminosity / 100f, + MaxBright = r.MaxBright / 100f, }).ToList(); var fogMode = s.WorldFog switch