fix(render): FW4 slice 5 - straddling cell statics'' particles emit pre-clear
The cathedral falls'' true identity, pinned by the emitter/owner dumps
and the owner''s retail-vs-ACE oracle (retail renders them perfectly):
they are EnvCell STAB-LIST statics (interior-cell dat objects, ids
minted by InteriorEntityIdAllocator) whose geometry hangs out over the
lake. Our cell-statics route submitted every interior flood cell''s
static particle owners POST-clear ("walls own depth") - true for
content that stays inside, but a straddler''s outside half meets
seal/cleared depth and splats across the vista.
Retail''s mechanism is the outdoor shadow-cell draw: an object
overlapping outdoor landcells draws during LScape::draw (DrawBlock
pc:430056-430064, once per overlapped shadow cell), so its particles
submit in the landscape scope and drain at the pre-clear boundary
flush over true terrain depth; the cell''s own turn skips them
(drawn-once). This slice ports that rule for particles: after Collect,
the interior flood cells'' static records are classified by the shared
exit-plane straddle test (extracted as SphereStraddlesExitPlane from
the #118 dynamic split); straddlers'' owners submit in the pre-clear
closure and the post-replay cell-owners union subtracts them, so every
owner emits exactly once. Also adds the [walk-emit] probe dump that
falsified the unattached-route theory.
Hermetic 6,762/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f3a03efc81
commit
0d6cd5c06e
3 changed files with 108 additions and 0 deletions
|
|
@ -81,6 +81,18 @@ public sealed class RetailPViewRenderer
|
|||
// shine-through, fourth surface).
|
||||
private readonly HashSet<uint> _preClearParticleOwnerScratch = new();
|
||||
|
||||
// FW4 slice 5: interior flood-cell STATICS whose bounds straddle their
|
||||
// cell's exit-portal plane (the cathedral falls — EnvCell stab-list
|
||||
// objects hanging out over the lake). Retail draws such an object
|
||||
// during LScape::draw via the outdoor shadow-cell lists (DrawBlock
|
||||
// pc:430056-430064, once per overlapped shadow cell), so its particles
|
||||
// submit in the landscape scope and drain at the pre-clear boundary
|
||||
// flush over true terrain depth; the cell's own post-clear turn skips
|
||||
// them (drawn-once). Computed after Collect from the walk's flood;
|
||||
// submitted by the pre-clear closure; subtracted from the post-replay
|
||||
// cell-owners union.
|
||||
private readonly HashSet<uint> _preClearCellStaticOwnerScratch = new();
|
||||
|
||||
// MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
|
||||
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
|
||||
// + 2 Lists, plus one List<WorldEntity> per visible cell) every DrawInside
|
||||
|
|
@ -354,9 +366,15 @@ public sealed class RetailPViewRenderer
|
|||
walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
|
||||
walkDriver = new Walk.WalkFrameDriver(walkExecutor!.Dispatcher, leafRenderer, _walkWorldData);
|
||||
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
{
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame =
|
||||
_probeWalkRootFrame % 90 == 0;
|
||||
}
|
||||
walkDriver.Collect(
|
||||
_frameWalk, ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext,
|
||||
ctx.ViewProjection, ctx.CameraWorldPosition);
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.WalkPortalProbeThisFrame = false;
|
||||
|
||||
// FW4 slice 1: an interior root's terrain/sky/punch clip slices
|
||||
// come from THE WALK'S OWN outside_view — retail's one
|
||||
|
|
@ -576,6 +594,37 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
}
|
||||
|
||||
// FW4 slice 5: classify the interior flood cells' STATICS by the
|
||||
// exit-plane straddle test — a straddler's particles (the
|
||||
// cathedral falls: EnvCell stab objects hanging over the lake)
|
||||
// submit pre-clear like retail's outdoor shadow-cell draw; see
|
||||
// _preClearCellStaticOwnerScratch's field comment. GetCellStatics
|
||||
// is per-frame cached, so this also pre-warms the post-replay
|
||||
// cell-owners pass.
|
||||
_preClearCellStaticOwnerScratch.Clear();
|
||||
if (walkActive && !ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
foreach (uint cellId in walkDriver!.VisitedCells)
|
||||
{
|
||||
if ((cellId & 0xFFFFu) < 0x100u || _lookInCellIds.Contains(cellId))
|
||||
continue;
|
||||
LoadedCell? staticCell = ctx.Cells.Find(cellId);
|
||||
if (staticCell is null)
|
||||
continue;
|
||||
Walk.WalkFrameStaticRecords cellRecords =
|
||||
_walkWorldData!.GetCellStatics(cellId);
|
||||
foreach (RenderProjectionRecord record in cellRecords.Records)
|
||||
{
|
||||
if (record.Source.LocalEntityId == 0)
|
||||
continue;
|
||||
Vector3 c = (record.Bounds.Minimum + record.Bounds.Maximum) * 0.5f;
|
||||
float r = (record.Bounds.Maximum - record.Bounds.Minimum).Length() * 0.5f;
|
||||
if (SphereStraddlesExitPlane(staticCell, c, r))
|
||||
_preClearCellStaticOwnerScratch.Add(record.Source.LocalEntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (walkActive)
|
||||
{
|
||||
// Campaign FW3.2b-2: the walk owns every static draw —
|
||||
|
|
@ -608,6 +657,17 @@ public sealed class RetailPViewRenderer
|
|||
// BEFORE the pre-clear drain so they composite against
|
||||
// still-true landscape depth, never the seals.
|
||||
SubmitWalkLandscapeStaticParticles(ctx, walkExecutor!, capturedDriver);
|
||||
// FW4 slice 5: straddling flood-cell statics' emitters
|
||||
// (the falls) — retail's shadow-cell landscape draw.
|
||||
if (_preClearCellStaticOwnerScratch.Count > 0)
|
||||
{
|
||||
_preClearParticleOwnerScratch.UnionWith(
|
||||
_preClearCellStaticOwnerScratch);
|
||||
passes.DrawLandscapeStaticParticles(
|
||||
ctx,
|
||||
new RetailPViewLandscapeStaticParticleContext(
|
||||
_preClearCellStaticOwnerScratch));
|
||||
}
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
DrawLandscapeDynamicsPhase(
|
||||
ctx,
|
||||
|
|
@ -1403,6 +1463,10 @@ public sealed class RetailPViewRenderer
|
|||
continue;
|
||||
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch);
|
||||
}
|
||||
// FW4 slice 5: exit-plane straddlers (the falls) already emitted at
|
||||
// the pre-clear boundary — retail's shadow-cell landscape draw;
|
||||
// every owner emits exactly once.
|
||||
_cellParticleOwnerScratch.ExceptWith(_preClearCellStaticOwnerScratch);
|
||||
if (_cellParticleOwnerScratch.Count > 0)
|
||||
{
|
||||
passes.DrawCellParticles(
|
||||
|
|
@ -2617,6 +2681,16 @@ public sealed class RetailPViewRenderer
|
|||
if (cell is null)
|
||||
return false;
|
||||
|
||||
return SphereStraddlesExitPlane(cell, sphereCenter, sphereRadius);
|
||||
}
|
||||
|
||||
/// <summary>The exit-plane straddle test shared by the #118 dynamic
|
||||
/// stage split and the FW4 slice-5 cell-static particle split — retail
|
||||
/// draws a straddling object once per overlapped outdoor shadow cell
|
||||
/// inside LScape::draw (DrawBlock pc:430056-430064).</summary>
|
||||
internal static bool SphereStraddlesExitPlane(
|
||||
LoadedCell cell, Vector3 sphereCenter, float sphereRadius)
|
||||
{
|
||||
var localC = Vector3.Transform(sphereCenter, cell.InverseWorldTransform);
|
||||
int n = Math.Min(cell.Portals.Count, cell.ClipPlanes.Count);
|
||||
for (int i = 0; i < n; i++)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue