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:
parent
9567597814
commit
3e0da496e0
3 changed files with 130 additions and 5 deletions
|
|
@ -4232,6 +4232,16 @@ 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.
|
||||
//
|
||||
// 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -70,8 +70,18 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>SkyObject</c> with <c>Properties & 0x04 == 0</c>).
|
||||
/// Called BEFORE the scene; terrain / meshes / debug lines / overlay
|
||||
/// land on top via depth-test.
|
||||
///
|
||||
/// <para>
|
||||
/// Mirrors the first half of retail's <c>LScape::draw</c> at
|
||||
/// <c>0x00506330</c>: that function calls <c>GameSky::Draw(0)</c>
|
||||
/// (sky pass) before the landblock loop, then <c>GameSky::Draw(1)</c>
|
||||
/// (weather pass) after. acdream splits the same way — see
|
||||
/// <see cref="RenderWeather"/> for the post-scene companion.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Each submesh renders with retail's per-vertex lighting formula:
|
||||
|
|
@ -91,12 +101,50 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
/// field.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void Render(
|
||||
public void RenderSky(
|
||||
ICamera camera,
|
||||
Vector3 cameraWorldPos,
|
||||
float dayFraction,
|
||||
DayGroupData? group,
|
||||
SkyKeyframe keyframe)
|
||||
=> RenderPass(camera, cameraWorldPos, dayFraction, group, keyframe, weatherPass: false);
|
||||
|
||||
/// <summary>
|
||||
/// Draw the WEATHER sky objects (the foreground rain mesh
|
||||
/// <c>0x01004C42</c>/<c>0x01004C44</c> on Rainy DayGroups, plus the
|
||||
/// per-storm 5cm flash dummies — every <c>SkyObject</c> with
|
||||
/// <c>Properties & 0x04 != 0</c>). Called AFTER the scene so the
|
||||
/// rain meshes paint on top of terrain and entities — that's the
|
||||
/// retail-faithful order from <c>LScape::draw</c> at
|
||||
/// <c>0x00506330</c>, where <c>GameSky::Draw(1)</c> fires after the
|
||||
/// <c>DrawBlock</c> loop. With depth-test disabled and additive blend
|
||||
/// (the rain Surface flag <c>0x080000C5</c> 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.
|
||||
/// </summary>
|
||||
public void RenderWeather(
|
||||
ICamera camera,
|
||||
Vector3 cameraWorldPos,
|
||||
float dayFraction,
|
||||
DayGroupData? group,
|
||||
SkyKeyframe keyframe)
|
||||
=> RenderPass(camera, cameraWorldPos, dayFraction, group, keyframe, weatherPass: true);
|
||||
|
||||
/// <summary>
|
||||
/// Shared pass for <see cref="RenderSky"/> and <see cref="RenderWeather"/>.
|
||||
/// 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
|
||||
/// <see cref="SkyObjectData.IsWeather"/>.
|
||||
/// </summary>
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -36,6 +36,23 @@ public sealed class SkyObjectData
|
|||
public uint GfxObjId;
|
||||
public uint Properties;
|
||||
|
||||
/// <summary>
|
||||
/// True when this SkyObject is flagged as weather (Properties bit
|
||||
/// <c>0x04</c>). Per the named retail decomp,
|
||||
/// <c>GameSky::CreateDeletePhysicsObjects</c> at <c>0x005073c0</c>
|
||||
/// passes <c>Properties & 0x04</c> as <c>arg5</c> of
|
||||
/// <c>GameSky::MakeObject</c> (<c>0x00506ee0</c>) — when set, the
|
||||
/// CPhysicsObj is added to <c>after_sky_cell</c> instead of
|
||||
/// <c>before_sky_cell</c>, and <c>GameSky::Draw(arg2=1)</c> at
|
||||
/// <c>0x00506ff0</c> draws that cell <i>after</i> 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 <c>0x01004C42</c>/<c>0x01004C44</c>)
|
||||
/// render post-scene with depth-test off so they overlay foreground
|
||||
/// geometry — matching retail's volumetric foreground-rain look.
|
||||
/// </summary>
|
||||
public bool IsWeather => (Properties & 0x04u) != 0u;
|
||||
|
||||
/// <summary>Object is visible at day-fraction <paramref name="t"/>
|
||||
/// by retail's begin/end semantics (r12 §2). Three cases:
|
||||
/// <list type="bullet">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue