T3 (BR-5): viewconeCheck port - per-view sphere culling for statics/dynamics/particles, weather player-gate, unattached outdoor emitters
Ports Render::viewconeCheck (Ghidra 0x0054c250): meshes are sphere-CULLED per portal view, never hard-clipped. NEW ViewconeCuller lifts each slice's <=8 clip-space half-planes to world-space eye-edge planes (the view_vertex.plane analog, acclient.h:32483 - one matrix fold: L = VP rows . P) and tests bounding spheres from the entity's cached AABB (the dispatcher's own cull bounds source). Gating now matches retail's shape end to end: - Per-cell STATICS: sphere vs THEIR CELL's views - the statics-through- walls fix (the cottage phantom staircase's actual draw path: a static outside every view of its cell no longer paints through the wall). - DYNAMICS (last pass): sphere vs their cell's views; outdoor/unresolved vs the outside views (pass-all under the outdoor root). A dynamic in a non-flooded room culls HERE - retail never reaches an object whose cell is not in the draw list; the partition still routes it so the CULL is what drops it, retail's shape exactly. - OutdoorStatic (landscape pass): pre-filtered per outside slice; the per-slice entity gl_ClipDistance routing is DELETED (entities draw outside the clip bracket; terrain/sky keep their plane clip). - PARTICLES: the scissor-AABB gate is DELETED; emitters gate through their cone-surviving owners (candle-flames-through-walls fix). - WEATHER: gated on the PLAYER being outside (retail is_player_outside - an indoor player gets no rain even looking out a doorway). Closes weather-gate-player-vs-viewer. - UNATTACHED emitters (campfires) get their missing outdoor-root pass (closes unattached-particles-dropped-outdoors). Suites: App 226 green (flood gates included), Core baseline unchanged (1398 + 4 pre-existing #99-era). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
88f3ce1fa0
commit
a6aec8c32f
3 changed files with 268 additions and 24 deletions
|
|
@ -104,12 +104,17 @@ public sealed class RetailPViewRenderer
|
|||
// from the punch/seal depth writes + the z-buffer, and the dynamics-
|
||||
// last order is what makes the punch safe (the first BR-2 attempt
|
||||
// punched after dynamics and erased the player, reverted 88be519).
|
||||
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition);
|
||||
// T3 (BR-5): retail viewconeCheck — meshes are sphere-CULLED per view,
|
||||
// never clipped (Ghidra 0x0054c250). Built once per frame from the
|
||||
// assembled slices + this frame's view-projection.
|
||||
var viewcone = ViewconeCuller.Build(clipAssembly, ctx.ViewProjection);
|
||||
|
||||
DrawLandscapeThroughOutsideView(ctx, clipAssembly, partition, viewcone);
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(pvFrame);
|
||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition);
|
||||
DrawDynamicsLast(ctx, partition);
|
||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
||||
DrawDynamicsLast(ctx, partition, viewcone);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -228,9 +233,10 @@ public sealed class RetailPViewRenderer
|
|||
// drawn here: they belong exclusively to the frame's single last
|
||||
// entity pass (the outdoor root's DrawDynamicsLast), which prevents
|
||||
// double-draws of entities inside looked-into buildings.
|
||||
var viewcone = ViewconeCuller.Build(clipAssembly, ctx.ViewProjection);
|
||||
DrawExitPortalMasks(ctx, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(pvFrame);
|
||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition);
|
||||
DrawCellObjectLists(ctx, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
||||
RestoreNoClip(ctx.SetTerrainClipUbo);
|
||||
|
||||
return result;
|
||||
|
|
@ -239,7 +245,8 @@ public sealed class RetailPViewRenderer
|
|||
private void DrawLandscapeThroughOutsideView(
|
||||
RetailPViewDrawContext ctx,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result partition)
|
||||
InteriorEntityPartition.Result partition,
|
||||
ViewconeCuller viewcone)
|
||||
{
|
||||
if (clipAssembly.OutsideViewSlices.Length == 0)
|
||||
return;
|
||||
|
|
@ -249,11 +256,24 @@ public sealed class RetailPViewRenderer
|
|||
{
|
||||
_clipFrame.SetTerrainClip(slice.Planes);
|
||||
UploadClipFrame(ctx.SetTerrainClipUbo);
|
||||
_entities.SetClipRouting(clipAssembly.CellIdToSlot, slice.Slot, outdoorVisible: true);
|
||||
// T3 (BR-5): entities are never hard-clipped — retail viewcone-
|
||||
// CHECKS each mesh's sphere against the view (Ghidra 0x0054c250)
|
||||
// and draws it whole. The old per-slice entity clip routing
|
||||
// (gl_ClipDistance via SetClipRouting) is replaced by the sphere
|
||||
// pre-filter below; terrain/sky keep their per-slice plane clip.
|
||||
_entities.ClearClipRouting();
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeClipRouteEnabled)
|
||||
EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex);
|
||||
|
||||
_outdoorStaticScratch.Clear();
|
||||
foreach (var e in partition.OutdoorStatic)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
|
||||
_outdoorStaticScratch.Add(e);
|
||||
}
|
||||
probeSliceIndex++;
|
||||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.OutdoorStatic));
|
||||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
|
||||
}
|
||||
|
||||
// T1: retail clears the FULL depth buffer ONCE between the outside
|
||||
|
|
@ -402,15 +422,38 @@ public sealed class RetailPViewRenderer
|
|||
// (retail draws objects per cell AFTER cells and viewcone-culls them —
|
||||
// PView::DrawCells epilogue Ghidra 0x005a4840; the sphere-vs-view cull is
|
||||
// T3). Drawing dynamics last is what makes the aperture punch safe.
|
||||
// T3 (BR-5): each dynamic is viewcone-culled like retail — sphere vs its
|
||||
// cell's views; outdoor/unresolved vs the outside views (pass-all under
|
||||
// the outdoor root's full-screen outside view). A dynamic in a NON-flooded
|
||||
// room culls HERE — retail never reaches an object whose cell is not in
|
||||
// the draw list; the partition keeps routing it so the CULL (not the
|
||||
// visibility set) drops it, exactly retail's shape.
|
||||
private void DrawDynamicsLast(
|
||||
IRetailPViewCellDrawContext ctx,
|
||||
InteriorEntityPartition.Result partition)
|
||||
InteriorEntityPartition.Result partition,
|
||||
ViewconeCuller viewcone)
|
||||
{
|
||||
if (partition.Dynamics.Count == 0)
|
||||
return;
|
||||
|
||||
_dynamicsScratch.Clear();
|
||||
foreach (var e in partition.Dynamics)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
bool indoor = e.ParentCellId is uint cell
|
||||
&& (cell & 0xFFFFu) >= 0x0100u && (cell & 0xFFFFu) != 0xFFFFu;
|
||||
bool visible = indoor
|
||||
? viewcone.SphereVisibleInCell(e.ParentCellId!.Value, c, r)
|
||||
: viewcone.SphereVisibleOutside(c, r);
|
||||
if (visible)
|
||||
_dynamicsScratch.Add(e);
|
||||
}
|
||||
|
||||
if (_dynamicsScratch.Count == 0)
|
||||
return;
|
||||
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawEntityBucket(ctx, partition.Dynamics, visibleCellIds: null);
|
||||
DrawEntityBucket(ctx, _dynamicsScratch, visibleCellIds: null);
|
||||
}
|
||||
|
||||
private void DrawCellObjectLists(
|
||||
|
|
@ -418,13 +461,17 @@ public sealed class RetailPViewRenderer
|
|||
PortalVisibilityFrame pvFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
HashSet<uint> drawableCells,
|
||||
InteriorEntityPartition.Result partition)
|
||||
InteriorEntityPartition.Result partition,
|
||||
ViewconeCuller viewcone)
|
||||
{
|
||||
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
|
||||
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
|
||||
// the shells (retail DrawCells epilogue: PortalList = cell's views →
|
||||
// DrawObjCell, Ghidra 0x005a4840). Unclipped; per-view sphere culling
|
||||
// (viewconeCheck) is T3.
|
||||
// DrawObjCell, Ghidra 0x005a4840). T3 (BR-5): each static's sphere is
|
||||
// tested against ITS CELL's views (retail viewconeCheck) — the
|
||||
// statics-through-walls fix: a static whose sphere is outside every
|
||||
// view of its cell no longer paints through the wall (the cottage
|
||||
// phantom staircase's draw path).
|
||||
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = pvFrame.OrderedVisibleCells[i];
|
||||
|
|
@ -434,22 +481,51 @@ public sealed class RetailPViewRenderer
|
|||
if (!partition.ByCell.TryGetValue(cellId, out var bucket) || bucket.Count == 0)
|
||||
continue;
|
||||
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
_cellStaticScratch.Clear();
|
||||
foreach (var e in bucket)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (viewcone.SphereVisibleInCell(cellId, c, r))
|
||||
_cellStaticScratch.Add(e);
|
||||
}
|
||||
|
||||
// BR-2 phantom-site probe: static buckets draw unclipped +
|
||||
// un-viewcone'd until T3 — log the per-cell exposure.
|
||||
// BR-2 phantom-site probe (T3-updated): post-viewcone survivors.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled)
|
||||
EmitPhantomObjsProbe(cellId, bucket.Count);
|
||||
EmitPhantomObjsProbe(cellId, _cellStaticScratch.Count);
|
||||
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawEntityBucket(ctx, bucket, _oneCell);
|
||||
if (_cellStaticScratch.Count > 0)
|
||||
{
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawEntityBucket(ctx, _cellStaticScratch, _oneCell);
|
||||
}
|
||||
|
||||
// T3 (BR-5): particles gate through the SAME viewcone as their
|
||||
// owners — the callback receives the cone-surviving entity set, so
|
||||
// an emitter attached to a culled static no longer draws through
|
||||
// the wall (the candle-flames-through-walls fix). Consumed
|
||||
// synchronously within this iteration (scratch list reuse).
|
||||
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
||||
ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(cellId, slice, bucket));
|
||||
ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(cellId, slice, _cellStaticScratch));
|
||||
}
|
||||
}
|
||||
|
||||
// T3 scratch lists (render thread only; cleared per use).
|
||||
private readonly List<WorldEntity> _outdoorStaticScratch = new();
|
||||
private readonly List<WorldEntity> _cellStaticScratch = new();
|
||||
private readonly List<WorldEntity> _dynamicsScratch = new();
|
||||
|
||||
// Conservative bounding sphere from the entity's cached AABB — the same
|
||||
// bounds source the dispatcher's frustum cull uses.
|
||||
private static void EntitySphere(WorldEntity e, out Vector3 center, out float radius)
|
||||
{
|
||||
if (e.AabbDirty)
|
||||
e.RefreshAabb();
|
||||
center = (e.AabbMin + e.AabbMax) * 0.5f;
|
||||
radius = (e.AabbMax - e.AabbMin).Length() * 0.5f;
|
||||
}
|
||||
|
||||
// BR-2 phantom-site probe state: print-on-change per cell so the log stays
|
||||
// diffable while the condition persists. Throwaway apparatus — strip when
|
||||
// the #113 phantom residual closes. (The [phantom-shell] half died with
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue