feat(sky): split SkyRenderer into pre-/post-scene passes + retail -120m weather Z offset

Bug A (foreground rain) from docs/research/2026-04-26-sky-investigation-handoff.md:
rain mesh was only visible at horizon, not in the air between camera and
character. Two retail mechanisms ported here:

1. **Render order split.** Retail's `LScape::draw` at 0x00506330 calls
   `GameSky::Draw(0)` BEFORE the landblock DrawBlock loop and
   `GameSky::Draw(1)` AFTER — i.e. weather meshes render after scene
   geometry so additive rain streaks paint on top of terrain and entities.
   Acdream was rendering both passes pre-scene, so terrain immediately
   painted over the rain.

   Refactored `SkyRenderer.Render` into `RenderSky` (filter !IsWeather)
   and `RenderWeather` (filter IsWeather) sharing a private `RenderPass`
   core that takes a `weatherPass` bool. Partition is per-SkyObject by
   `Properties & 0x04` (the WEATHER_BIT, mirroring tools/WeatherEnumerator).
   Added `SkyObjectData.IsWeather` getter for the partition.

   `GameWindow.OnRender` now calls `RenderSky` before terrain/static-mesh/
   particles (line ~4322) and `RenderWeather` after particles (line ~4368).

2. **Weather Z offset.** Retail `GameSky::UpdatePosition` at 0x00506dd0,
   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) get 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 sat just above the
   camera; with -120m the cylinder spans (camera-119.89)..(camera+694.90)
   so the camera is inside.

   `SkyRenderer.RenderPass` applies the -120m model translation when
   `weatherPass` is true (line ~253-254).

3. **Legacy camera-attached emitter gated.** `UpdateWeatherParticles` —
   the pre-research workaround that emitted camera-attached rain particles
   (broken alpha fade, fixed disk around camera) — is now gated behind
   `ACDREAM_FAKE_RAIN_PARTICLES=1`. Default off; the retail-faithful
   world-space mesh is the default path.

User-verified: rain is now visible in foreground from many perspectives,
but the cylinder's open-top rim is still visible when looking straight up.
That rim issue is a separate brightness-excess bug filed for follow-up
(Translucency float not plumbed to shader; surface.Translucency=0.5 ignored
so streaks render at 2× retail intensity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-27 08:49:42 +02:00
parent 9567597814
commit 3e0da496e0
3 changed files with 130 additions and 5 deletions

View file

@ -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)