fix(sky): partition sky pass on Properties bit 0x01, not bit 0x04

The pre/post-scene sky pass split was using SkyObjectData.IsWeather
(bit 0x04) — the wrong bit. Per the named retail decomp:

  GameSky::CreateDeletePhysicsObjects at 0x005073c0 / decomp 269036:
    MakeObject(this, gfx_id, &tex_velocity,
               (properties & 1),    // arg4: post-scene flag
               (properties & 4));   // arg5: weather gate

  GameSky::MakeObject at 0x00506ee0 / decomp 268656:
    if (arg4 != 0)
      AddObjectToSingleCell(result, after_sky_cell);   // post-scene
    else
      AddObjectToSingleCell(result, before_sky_cell);  // pre-scene

So bit 0x01 routes between before_sky_cell (rendered pre-scene by
GameSky::Draw(0)) and after_sky_cell (rendered post-scene by
GameSky::Draw(1)). Bit 0x04 is independent — it gates whether the
object is instantiated at all when LScape::weather_enabled is false.

In Dereth's Rainy DayGroup this matters for the rain cylinders:
  0x01004C42  Props=0x04 (bit 0x04 only)  → pre-scene + weather-gated
  0x01004C44  Props=0x05 (bits 0x01+0x04) → post-scene + weather-gated
  0x01004C35  Props=0x02 (bit 0x02 only)  → pre-scene (cloud, fog-hide)

Before this fix acdream put BOTH rain cylinders in the post-scene
pass (because both have bit 0x04). That double-rendered foreground
rain — explained why acdream's foreground rain looked thicker than
retail's. Now only 0x01004C44 is foreground; 0x01004C42 renders with
the sky dome.

Added SkyObjectData.IsPostScene (bit 0x01) with citations. Renamed
the internal RenderPass parameter weatherPass → postScenePass and
updated both the partition criterion and the -120m foreground-rain
Z offset to gate on it. Public RenderSky / RenderWeather entry
points kept their names for API stability; doc comments updated to
explain the bit semantics.

Independent confirmation from one of the user's external code-review
agents — the report's Setup-objects-silently-dropped finding is the
remaining defect in the same family (Setup IDs 0x020xxx aren't
loaded by EnsureMeshUploaded; deferred to a separate phase).

1227 tests pass.
This commit is contained in:
Erik 2026-04-27 22:43:14 +02:00
parent 05a8a7209f
commit 034a684f02
2 changed files with 72 additions and 37 deletions

View file

@ -107,21 +107,27 @@ public sealed unsafe class SkyRenderer : IDisposable
float dayFraction, float dayFraction,
DayGroupData? group, DayGroupData? group,
SkyKeyframe keyframe) SkyKeyframe keyframe)
=> RenderPass(camera, cameraWorldPos, dayFraction, group, keyframe, weatherPass: false); => RenderPass(camera, cameraWorldPos, dayFraction, group, keyframe, postScenePass: false);
/// <summary> /// <summary>
/// Draw the WEATHER sky objects (the foreground rain mesh /// Draw the POST-SCENE sky objects (the foreground rain mesh
/// <c>0x01004C42</c>/<c>0x01004C44</c> on Rainy DayGroups, plus the /// <c>0x01004C44</c> on Rainy DayGroups, plus any other SkyObject with
/// per-storm 5cm flash dummies — every <c>SkyObject</c> with /// <c>Properties &amp; 0x01 != 0</c>). Called AFTER the scene so these
/// <c>Properties &amp; 0x04 != 0</c>). Called AFTER the scene so the /// meshes paint on top of terrain and entities — retail-faithful order
/// rain meshes paint on top of terrain and entities — that's the /// from <c>LScape::draw</c> at <c>0x00506330</c>, where
/// retail-faithful order from <c>LScape::draw</c> at /// <c>GameSky::Draw(1)</c> fires after the <c>DrawBlock</c> loop and
/// <c>0x00506330</c>, where <c>GameSky::Draw(1)</c> fires after the /// renders the <c>after_sky_cell</c> contents. With depth-test
/// <c>DrawBlock</c> loop. With depth-test disabled and additive blend /// disabled and additive blend (the rain Surface flag includes
/// (the rain Surface flag <c>0x080000C5</c> includes Additive), the /// Additive), the 815m-tall rain cylinder's bright streak texels add
/// 815m-tall rain cylinder's bright streak texels add over the scene /// over the scene — making rain appear in the air between camera and
/// — making rain appear in the air between camera and character /// character instead of only at the horizon.
/// instead of only at the horizon. /// <para>
/// Method name kept as <c>RenderWeather</c> for API stability; the
/// pass actually partitions on <see cref="SkyObjectData.IsPostScene"/>
/// (Properties bit <c>0x01</c>), not <see cref="SkyObjectData.IsWeather"/>
/// (bit <c>0x04</c>). The two bits are independent in retail per
/// <c>GameSky::CreateDeletePhysicsObjects</c> at <c>0x005073c0</c>.
/// </para>
/// </summary> /// </summary>
public void RenderWeather( public void RenderWeather(
ICamera camera, ICamera camera,
@ -129,14 +135,15 @@ public sealed unsafe class SkyRenderer : IDisposable
float dayFraction, float dayFraction,
DayGroupData? group, DayGroupData? group,
SkyKeyframe keyframe) SkyKeyframe keyframe)
=> RenderPass(camera, cameraWorldPos, dayFraction, group, keyframe, weatherPass: true); => RenderPass(camera, cameraWorldPos, dayFraction, group, keyframe, postScenePass: true);
/// <summary> /// <summary>
/// Shared pass for <see cref="RenderSky"/> and <see cref="RenderWeather"/>. /// Shared pass for <see cref="RenderSky"/> and <see cref="RenderWeather"/>.
/// Sets up the same GL state for both (depth-test off, additive + /// Sets up the same GL state for both (depth-test off, additive +
/// alpha-blend per submesh, camera-anchored translation) and iterates /// alpha-blend per submesh, camera-anchored translation) and iterates
/// only the SkyObjects matching the requested partition by /// only the SkyObjects matching the requested partition by
/// <see cref="SkyObjectData.IsWeather"/>. /// <see cref="SkyObjectData.IsPostScene"/> — bit <c>0x01</c> per the
/// retail decomp at <c>GameSky::MakeObject</c> (<c>0x00506ee0</c>).
/// </summary> /// </summary>
private void RenderPass( private void RenderPass(
ICamera camera, ICamera camera,
@ -144,7 +151,7 @@ public sealed unsafe class SkyRenderer : IDisposable
float dayFraction, float dayFraction,
DayGroupData? group, DayGroupData? group,
SkyKeyframe keyframe, SkyKeyframe keyframe,
bool weatherPass) bool postScenePass)
{ {
if (group is null || group.SkyObjects.Count == 0) return; if (group is null || group.SkyObjects.Count == 0) return;
@ -197,14 +204,20 @@ public sealed unsafe class SkyRenderer : IDisposable
for (int i = 0; i < group.SkyObjects.Count; i++) for (int i = 0; i < group.SkyObjects.Count; i++)
{ {
var obj = group.SkyObjects[i]; var obj = group.SkyObjects[i];
// Partition by weather flag — the caller chose either the // Partition by post-scene flag (Properties bit 0x01) — the
// pre-scene sky pass (non-weather) or the post-scene weather // caller chose either the pre-scene sky pass (bit clear) or
// pass (weather only). Mirrors retail GameSky::Draw at // the post-scene pass (bit set). Mirrors retail
// 0x00506ff0 where arg2==0 iterates non-weather sky_obj // GameSky::CreateDeletePhysicsObjects at 0x005073c0 / decomp
// entries (filtered by property bit 0x04 == 0 inside the // line 269036 which routes (Properties & 1) into
// loop) and arg2==1 draws after_sky_cell which only contains // before_sky_cell vs after_sky_cell, and GameSky::Draw at
// weather objects. // 0x00506ff0 which renders those cells in the two passes.
if (obj.IsWeather != weatherPass) continue; // NOTE: bit 0x04 (IsWeather) is independent — it gates whether
// the object is instantiated when weather_enabled is false.
// Earlier acdream incorrectly used IsWeather for this
// partition, putting the outer rain cylinder 0x01004C42
// (Props=0x04, NO bit 0x01) into the post-scene pass with the
// foreground rain — double-thick rain not matching retail.
if (obj.IsPostScene != postScenePass) continue;
if (!obj.IsVisible(dayFraction)) continue; if (!obj.IsVisible(dayFraction)) continue;
// Apply per-keyframe replace overrides. // Apply per-keyframe replace overrides.
@ -267,7 +280,7 @@ public sealed unsafe class SkyRenderer : IDisposable
// (camera-119.89)..(camera+694.90) in view space — camera // (camera-119.89)..(camera+694.90) in view space — camera
// is inside, looking in any direction shows surrounding // is inside, looking in any direction shows surrounding
// walls — the volumetric foreground-rain look retail has. // walls — the volumetric foreground-rain look retail has.
if (weatherPass) if (postScenePass)
model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f); model = model * Matrix4x4.CreateTranslation(0f, 0f, -120f);
_shader.SetMatrix4("uModel", model); _shader.SetMatrix4("uModel", model);

View file

@ -37,22 +37,44 @@ public sealed class SkyObjectData
public uint Properties; public uint Properties;
/// <summary> /// <summary>
/// True when this SkyObject is flagged as weather (Properties bit /// True when this SkyObject is gated on the weather system (Properties
/// <c>0x04</c>). Per the named retail decomp, /// bit <c>0x04</c>). Per the named retail decomp,
/// <c>GameSky::CreateDeletePhysicsObjects</c> at <c>0x005073c0</c> /// <c>GameSky::CreateDeletePhysicsObjects</c> at <c>0x005073c0</c>
/// passes <c>Properties &amp; 0x04</c> as <c>arg5</c> of /// passes <c>Properties &amp; 4</c> as <c>arg5</c> of
/// <c>GameSky::MakeObject</c> (<c>0x00506ee0</c>) — when set, the /// <c>GameSky::MakeObject</c> (<c>0x00506ee0</c>); the inner
/// CPhysicsObj is added to <c>after_sky_cell</c> instead of /// <c>(arg5 == 0 || LScape::weather_enabled != 0)</c> guard at decomp
/// <c>before_sky_cell</c>, and <c>GameSky::Draw(arg2=1)</c> at /// line 268630 means weather-flagged objects only get instantiated when
/// <c>0x00506ff0</c> draws that cell <i>after</i> the scene. acdream /// the global weather flag is on. This bit does <b>not</b> control
/// uses this flag to split the sky pass: non-weather objects render /// pre/post-scene placement — that's <see cref="IsPostScene"/>.
/// pre-scene (so terrain and entities z-test on top), weather meshes /// acdream currently always renders weather-flagged objects (we don't
/// (e.g. the 815m-tall rain cylinders <c>0x01004C42</c>/<c>0x01004C44</c>) /// honor a weather_enabled toggle yet); when we add one, this flag is
/// render post-scene with depth-test off so they overlay foreground /// the gate.
/// geometry — matching retail's volumetric foreground-rain look.
/// </summary> /// </summary>
public bool IsWeather => (Properties & 0x04u) != 0u; public bool IsWeather => (Properties & 0x04u) != 0u;
/// <summary>
/// True when this SkyObject renders <i>after</i> the world scene
/// (Properties bit <c>0x01</c>) — i.e. as foreground over terrain and
/// entities. Per the named retail decomp,
/// <c>GameSky::CreateDeletePhysicsObjects</c> passes
/// <c>Properties &amp; 1</c> as <c>arg4</c> of
/// <c>GameSky::MakeObject</c> (decomp line 269036); MakeObject at
/// decomp 268656 routes <c>arg4 != 0</c> objects into
/// <c>after_sky_cell</c> instead of <c>before_sky_cell</c>, and
/// <c>GameSky::Draw(arg2=1)</c> at <c>0x00506ff0</c> draws
/// <c>after_sky_cell</c> as a separate post-scene pass.
/// <para>
/// In Dereth's Rainy DayGroup this distinguishes the two rain
/// cylinders: <c>0x01004C44</c> (Props=0x05) is foreground rain
/// rendered after terrain; <c>0x01004C42</c> (Props=0x04 alone) is
/// background rain rendered <i>with</i> the sky dome. Earlier
/// versions of acdream incorrectly split on <see cref="IsWeather"/>
/// (bit 0x04) so both rain meshes ended up in the post-scene pass,
/// double-rendering rain in the foreground.
/// </para>
/// </summary>
public bool IsPostScene => (Properties & 0x01u) != 0u;
/// <summary>Object is visible at day-fraction <paramref name="t"/> /// <summary>Object is visible at day-fraction <paramref name="t"/>
/// by retail's begin/end semantics (r12 §2). Three cases: /// by retail's begin/end semantics (r12 §2). Three cases:
/// <list type="bullet"> /// <list type="bullet">