diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index b304d37..17ed773 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -4232,7 +4232,17 @@ public sealed class GameWindow : IDisposable // Update the rain/snow particle emitters when the weather kind // changes. Keep the emitters fed by the ParticleSystem tick so // visuals stay alive frame-over-frame. - UpdateWeatherParticles(atmo); + // + // Bug A note (2026-04-26): retail rain is the world-space mesh + // 0x01004C42/0x01004C44 rendered in SkyRenderer.RenderWeather + // AFTER the scene — see docs/research/2026-04-26-sky-investigation-handoff.md. + // The camera-attached emitter path here is acdream's old + // pre-research workaround (broken alpha fade, fixed disk around + // camera). It's kept gated behind ACDREAM_FAKE_RAIN_PARTICLES=1 + // so the retail-faithful path can be A/B-tested against it + // without uninstalling the legacy code outright. + if (System.Environment.GetEnvironmentVariable("ACDREAM_FAKE_RAIN_PARTICLES") == "1") + UpdateWeatherParticles(atmo); // Phase E.3: advance live particle emitters AFTER animation tick // so emitters spawned by hooks fired this frame get integrated. @@ -4299,9 +4309,17 @@ public sealed class GameWindow : IDisposable // celestial meshes FIRST so the rest of the scene z-tests // on top of them (depth mask off, no depth writes). Skipped // when indoors; dungeons fully block sky visibility. + // + // Mirrors retail's LScape::draw at 0x00506330 which calls + // GameSky::Draw(0) (sky pass) BEFORE the landblock DrawBlock + // loop and GameSky::Draw(1) (weather pass) AFTER. The split + // matters because weather meshes (the 815m-tall rain + // cylinder 0x01004C42/0x01004C44) need to overlay terrain + // and entities to look volumetric — see the post-scene + // RenderWeather call further below. if (!cameraInsideCell) { - _skyRenderer?.Render(camera, camPos, (float)WorldTime.DayFraction, + _skyRenderer?.RenderSky(camera, camPos, (float)WorldTime.DayFraction, _activeDayGroup, kf); } @@ -4337,6 +4355,20 @@ public sealed class GameWindow : IDisposable if (_particleSystem is not null && _particleRenderer is not null) _particleRenderer.Draw(_particleSystem, camera, camPos); + // Bug A fix (post-#26 worktree, 2026-04-26): weather sky + // meshes (Properties & 0x04, e.g. the 815m-tall rain + // cylinder 0x01004C42/0x01004C44) render AFTER the scene so + // the additive rain streaks overlay terrain and entities + // instead of being painted over by them. This is the second + // half of retail's LScape::draw split — GameSky::Draw(1) + // fires after the DrawBlock loop. Same indoor gate as the + // sky pass: weather is suppressed inside cells. + if (!cameraInsideCell) + { + _skyRenderer?.RenderWeather(camera, camPos, (float)WorldTime.DayFraction, + _activeDayGroup, kf); + } + // Debug: draw collision shapes as wireframe cylinders around the // player so we can visually verify alignment with scenery meshes. if (_debugCollisionVisible && _debugLines is not null) diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs index 6fefea3..e6238cb 100644 --- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs +++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs @@ -70,8 +70,18 @@ public sealed unsafe class SkyRenderer : IDisposable } /// - /// Draw the sky for this frame. Called FIRST in the render loop — - /// terrain / meshes / debug lines / overlay land on top. + /// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds — + /// every SkyObject with Properties & 0x04 == 0). + /// Called BEFORE the scene; terrain / meshes / debug lines / overlay + /// land on top via depth-test. + /// + /// + /// Mirrors the first half of retail's LScape::draw at + /// 0x00506330: that function calls GameSky::Draw(0) + /// (sky pass) before the landblock loop, then GameSky::Draw(1) + /// (weather pass) after. acdream splits the same way — see + /// for the post-scene companion. + /// /// /// /// Each submesh renders with retail's per-vertex lighting formula: @@ -91,12 +101,50 @@ public sealed unsafe class SkyRenderer : IDisposable /// field. /// /// - public void Render( + public void RenderSky( ICamera camera, Vector3 cameraWorldPos, float dayFraction, DayGroupData? group, SkyKeyframe keyframe) + => RenderPass(camera, cameraWorldPos, dayFraction, group, keyframe, weatherPass: false); + + /// + /// Draw the WEATHER sky objects (the foreground rain mesh + /// 0x01004C42/0x01004C44 on Rainy DayGroups, plus the + /// per-storm 5cm flash dummies — every SkyObject with + /// Properties & 0x04 != 0). Called AFTER the scene so the + /// rain meshes paint on top of terrain and entities — that's the + /// retail-faithful order from LScape::draw at + /// 0x00506330, where GameSky::Draw(1) fires after the + /// DrawBlock loop. With depth-test disabled and additive blend + /// (the rain Surface flag 0x080000C5 includes Additive), the + /// 815m-tall rain cylinder's bright streak texels add over the scene + /// — making rain appear in the air between camera and character + /// instead of only at the horizon. + /// + public void RenderWeather( + ICamera camera, + Vector3 cameraWorldPos, + float dayFraction, + DayGroupData? group, + SkyKeyframe keyframe) + => RenderPass(camera, cameraWorldPos, dayFraction, group, keyframe, weatherPass: true); + + /// + /// Shared pass for and . + /// Sets up the same GL state for both (depth-test off, additive + + /// alpha-blend per submesh, camera-anchored translation) and iterates + /// only the SkyObjects matching the requested partition by + /// . + /// + private void RenderPass( + ICamera camera, + Vector3 cameraWorldPos, + float dayFraction, + DayGroupData? group, + SkyKeyframe keyframe, + bool weatherPass) { if (group is null || group.SkyObjects.Count == 0) return; @@ -149,6 +197,14 @@ public sealed unsafe class SkyRenderer : IDisposable for (int i = 0; i < group.SkyObjects.Count; i++) { var obj = group.SkyObjects[i]; + // Partition by weather flag — the caller chose either the + // pre-scene sky pass (non-weather) or the post-scene weather + // pass (weather only). Mirrors retail GameSky::Draw at + // 0x00506ff0 where arg2==0 iterates non-weather sky_obj + // entries (filtered by property bit 0x04 == 0 inside the + // loop) and arg2==1 draws after_sky_cell which only contains + // weather objects. + if (obj.IsWeather != weatherPass) continue; if (!obj.IsVisible(dayFraction)) continue; // Apply per-keyframe replace overrides. @@ -177,6 +233,26 @@ public sealed unsafe class SkyRenderer : IDisposable * Matrix4x4.CreateRotationZ(-headingRad) * Matrix4x4.CreateRotationY(-rotationRad); + // Retail weather Z-offset (GameSky::UpdatePosition at + // 0x00506dd0, decomp lines 0x506e96..0x506e98): + // + // if (((eax_13 & 4) != 0 && (eax_13 & 8) == 0)) + // int32_t var_4_1 = 0xc2f00000; // 0xc2f00000 == -120.0f + // + // Weather objects (property bit 0x04 set, bit 0x08 unset) + // have their frame origin set to player_pos + (0, 0, -120m). + // The rain cylinder GfxObjs 0x01004C42/0x01004C44 have local + // Z range 0.11..814.90 (815m tall, 113m radius). Without the + // offset the cylinder bottom sits at z=0.11 ABOVE the camera + // (skyView translation is zeroed so model-origin == camera); + // looking horizontally shows nothing, looking up shows a + // distant cylinder. With -120m the cylinder spans z = + // (camera-119.89)..(camera+694.90) in view space — camera + // is inside, looking in any direction shows surrounding + // walls — the volumetric foreground-rain look retail has. + if (weatherPass) + model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f); + _shader.SetMatrix4("uModel", model); // UV scroll accumulates real-time × velocity. Wrap to [0, 1] diff --git a/src/AcDream.Core/World/SkyDescLoader.cs b/src/AcDream.Core/World/SkyDescLoader.cs index dda09ca..409d51e 100644 --- a/src/AcDream.Core/World/SkyDescLoader.cs +++ b/src/AcDream.Core/World/SkyDescLoader.cs @@ -36,6 +36,23 @@ public sealed class SkyObjectData public uint GfxObjId; public uint Properties; + /// + /// True when this SkyObject is flagged as weather (Properties bit + /// 0x04). Per the named retail decomp, + /// GameSky::CreateDeletePhysicsObjects at 0x005073c0 + /// passes Properties & 0x04 as arg5 of + /// GameSky::MakeObject (0x00506ee0) — when set, the + /// CPhysicsObj is added to after_sky_cell instead of + /// before_sky_cell, and GameSky::Draw(arg2=1) at + /// 0x00506ff0 draws that cell after the scene. acdream + /// uses this flag to split the sky pass: non-weather objects render + /// pre-scene (so terrain and entities z-test on top), weather meshes + /// (e.g. the 815m-tall rain cylinders 0x01004C42/0x01004C44) + /// render post-scene with depth-test off so they overlay foreground + /// geometry — matching retail's volumetric foreground-rain look. + /// + public bool IsWeather => (Properties & 0x04u) != 0u; + /// Object is visible at day-fraction /// by retail's begin/end semantics (r12 §2). Three cases: ///