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
|
|
@ -7646,6 +7646,7 @@ public sealed class GameWindow : IDisposable
|
|||
playerLb,
|
||||
animatedIds,
|
||||
renderSky,
|
||||
renderWeather: playerSeenOutside,
|
||||
kf,
|
||||
environOverrideActive),
|
||||
// T1: retail's depth discipline (PView::DrawCells, Ghidra 0x005a4840).
|
||||
|
|
@ -7877,6 +7878,25 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.Core.Vfx.ParticleRenderPass.Scene);
|
||||
}
|
||||
}
|
||||
else if (clipRoot is { IsOutdoorNode: true }
|
||||
&& _particleSystem is not null && _particleRenderer is not null)
|
||||
{
|
||||
// T3 (BR-5): unattached emitters (campfires, ground effects —
|
||||
// AttachedObjectId == 0) under the OUTDOOR root. The unified
|
||||
// path's attached emitters draw via the landscape slice + the
|
||||
// per-cell callbacks; unattached ones had NO pass on
|
||||
// outdoor-node frames (the unattached-particles-dropped-
|
||||
// outdoors divergence, adjusted-confirmed). The outdoor root's
|
||||
// outside view is full-screen (cone pass-all); depth test
|
||||
// composites them against the world.
|
||||
sigSceneParticles = sigSceneParticles == "none" ? "unattached" : sigSceneParticles + "+unattached";
|
||||
_particleRenderer.Draw(
|
||||
_particleSystem,
|
||||
camera,
|
||||
camPos,
|
||||
AcDream.Core.Vfx.ParticleRenderPass.Scene,
|
||||
emitter => emitter.AttachedObjectId == 0);
|
||||
}
|
||||
|
||||
// Bug A fix (post-#26 worktree, 2026-04-26): weather sky
|
||||
// Outdoor LScape post-scene weather. Indoor weather through an exit portal is
|
||||
|
|
@ -9480,6 +9500,7 @@ public sealed class GameWindow : IDisposable
|
|||
uint? playerLb,
|
||||
HashSet<uint>? animatedIds,
|
||||
bool renderSky,
|
||||
bool renderWeather,
|
||||
AcDream.Core.World.SkyKeyframe kf,
|
||||
bool environOverrideActive)
|
||||
{
|
||||
|
|
@ -9510,6 +9531,11 @@ public sealed class GameWindow : IDisposable
|
|||
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
|
||||
MaybeFlushTerrainDiag();
|
||||
|
||||
// T3 (BR-5): entities draw OUTSIDE the clip bracket — retail meshes
|
||||
// are viewcone-CHECKED, never hard-clipped (Ghidra 0x0054c250); the
|
||||
// sphere pre-filter already ran in RetailPViewRenderer (OutdoorEntities
|
||||
// is the per-slice survivor set).
|
||||
DisableClipDistances();
|
||||
if (sliceCtx.OutdoorEntities.Count > 0)
|
||||
{
|
||||
var sceneryEntry = (playerLb ?? 0u, System.Numerics.Vector3.Zero, System.Numerics.Vector3.Zero,
|
||||
|
|
@ -9539,8 +9565,13 @@ public sealed class GameWindow : IDisposable
|
|||
&& _outdoorSceneParticleEntityIds.Contains(emitter.AttachedObjectId));
|
||||
}
|
||||
|
||||
// T3 (BR-5): weather gates on the PLAYER being outside, not the viewer
|
||||
// root — retail draws weather only when is_player_outside (the rain
|
||||
// cylinder rides the player; an indoor player gets NO rain even while
|
||||
// looking out a doorway). Closes the rain-through-doorways divergence
|
||||
// (weather-gate-player-vs-viewer, adjusted-confirmed).
|
||||
EnableClipDistances();
|
||||
if (renderSky)
|
||||
if (renderSky && renderWeather)
|
||||
{
|
||||
_skyRenderer?.RenderWeather(camera, camPos, (float)WorldTime.DayFraction,
|
||||
_activeDayGroup, kf, environOverrideActive);
|
||||
|
|
@ -9620,8 +9651,12 @@ public sealed class GameWindow : IDisposable
|
|||
if (_visibleSceneParticleEntityIds.Count == 0)
|
||||
return;
|
||||
|
||||
// T3 (BR-5): the scissor-AABB gate is DELETED — retail gates particles
|
||||
// like meshes (viewcone on the owner; depth does the pixels). The
|
||||
// CellEntities set is already the cone-surviving owner list, so the
|
||||
// id-predicate below IS the cone gate; the punch/seal depth discipline
|
||||
// composites the pixels.
|
||||
DisableClipDistances();
|
||||
bool scissor = BeginDoorwayScissor(true, sliceCtx.Slice.NdcAabb);
|
||||
_particleRenderer.Draw(
|
||||
_particleSystem,
|
||||
camera,
|
||||
|
|
@ -9629,8 +9664,6 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.Core.Vfx.ParticleRenderPass.Scene,
|
||||
emitter => emitter.AttachedObjectId != 0
|
||||
&& _visibleSceneParticleEntityIds.Contains(emitter.AttachedObjectId));
|
||||
if (scissor)
|
||||
_gl!.Disable(EnableCap.ScissorTest);
|
||||
DisableClipDistances();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
135
src/AcDream.App/Rendering/ViewconeCuller.cs
Normal file
135
src/AcDream.App/Rendering/ViewconeCuller.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// T3 (BR-5): the port of retail's <c>Render::viewconeCheck</c> (Ghidra
|
||||
/// 0x0054c250) — meshes (characters, statics, emitters) are CULLED per portal
|
||||
/// view by a bounding-sphere test against the view's edge planes, never
|
||||
/// clipped. Retail stores each view vertex with its 3D eye-edge plane
|
||||
/// (<c>view_vertex { Vec2D pt; Plane plane }</c>, acclient.h:32483) and tests
|
||||
/// the object's drawing sphere against the installed view's plane set;
|
||||
/// OUTSIDE → skipped (RenderDeviceD3D::DrawMesh per-view loop pc:429290-429310,
|
||||
/// and the DrawCells per-cell object epilogue, Ghidra 0x005a4840).
|
||||
///
|
||||
/// <para>Our views are clip-space half-planes (≤8 per slice,
|
||||
/// <see cref="ClipPlaneSet"/> output: (nx,ny,0,d) satisfied when
|
||||
/// nx·Cx + ny·Cy + d·Cw ≥ 0 for clip-space C). Lifting one to world space —
|
||||
/// the view_vertex.plane analog, a plane through the EYE and the view edge —
|
||||
/// is one matrix fold: with row-vector convention (System.Numerics),
|
||||
/// C = world·VP, so C·P = world·(VP·P); L = VP·P (rows of VP dotted with P)
|
||||
/// is the world-space homogeneous half-plane. Sphere-vs-half-plane keeps the
|
||||
/// sphere when L.xyz·c + L.w ≥ −r·|L.xyz| (not entirely outside).</para>
|
||||
///
|
||||
/// <para>A sphere is visible through a SLICE when it is not entirely outside
|
||||
/// any of the slice's planes (convex region); visible for a CELL when any of
|
||||
/// the cell's slices passes. A slice with zero planes is pass-all (the
|
||||
/// NoClipSlice / full-screen outdoor case). A cell with no views culls — in
|
||||
/// retail an object whose cell is not in the draw list is simply never
|
||||
/// reached.</para>
|
||||
/// </summary>
|
||||
public sealed class ViewconeCuller
|
||||
{
|
||||
private readonly Dictionary<uint, Vector4[][]> _cellPlanes = new();
|
||||
private Vector4[][] _outsidePlanes = Array.Empty<Vector4[]>();
|
||||
|
||||
/// <summary>True when the outside view is a full-screen pass-all (the
|
||||
/// synthetic outdoor root) — every outside-test passes.</summary>
|
||||
public bool OutsideIsFullScreen { get; private set; }
|
||||
|
||||
public static ViewconeCuller Build(ClipFrameAssembly assembly, in Matrix4x4 viewProjection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
var culler = new ViewconeCuller();
|
||||
|
||||
foreach (var (cellId, slices) in assembly.CellIdToViewSlices)
|
||||
{
|
||||
var lifted = new Vector4[slices.Length][];
|
||||
for (int s = 0; s < slices.Length; s++)
|
||||
lifted[s] = LiftPlanes(slices[s].Planes, viewProjection);
|
||||
culler._cellPlanes[cellId] = lifted;
|
||||
}
|
||||
|
||||
var outside = assembly.OutsideViewSlices;
|
||||
var outsideLifted = new Vector4[outside.Length][];
|
||||
bool fullScreen = false;
|
||||
for (int s = 0; s < outside.Length; s++)
|
||||
{
|
||||
outsideLifted[s] = LiftPlanes(outside[s].Planes, viewProjection);
|
||||
if (outside[s].Planes.Length == 0)
|
||||
fullScreen = true;
|
||||
}
|
||||
culler._outsidePlanes = outsideLifted;
|
||||
culler.OutsideIsFullScreen = fullScreen;
|
||||
return culler;
|
||||
}
|
||||
|
||||
private static Vector4[] LiftPlanes(Vector4[] clipPlanes, in Matrix4x4 m)
|
||||
{
|
||||
if (clipPlanes.Length == 0)
|
||||
return Array.Empty<Vector4>();
|
||||
var world = new Vector4[clipPlanes.Length];
|
||||
for (int i = 0; i < clipPlanes.Length; i++)
|
||||
{
|
||||
var p = clipPlanes[i];
|
||||
world[i] = new Vector4(
|
||||
m.M11 * p.X + m.M12 * p.Y + m.M13 * p.Z + m.M14 * p.W,
|
||||
m.M21 * p.X + m.M22 * p.Y + m.M23 * p.Z + m.M24 * p.W,
|
||||
m.M31 * p.X + m.M32 * p.Y + m.M33 * p.Z + m.M34 * p.W,
|
||||
m.M41 * p.X + m.M42 * p.Y + m.M43 * p.Z + m.M44 * p.W);
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
private static bool SphereInsidePlanes(Vector4[] planes, in Vector3 center, float radius)
|
||||
{
|
||||
for (int i = 0; i < planes.Length; i++)
|
||||
{
|
||||
var l = planes[i];
|
||||
float nLen = MathF.Sqrt(l.X * l.X + l.Y * l.Y + l.Z * l.Z);
|
||||
if (nLen < 1e-12f)
|
||||
continue; // degenerate plane — no constraint
|
||||
float dist = l.X * center.X + l.Y * center.Y + l.Z * center.Z + l.W;
|
||||
if (dist < -radius * nLen)
|
||||
return false; // entirely outside this edge plane
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Sphere-vs-the-cell's-views: visible when any slice passes.
|
||||
/// A cell with no views culls (not in the draw list ⇒ never reached in
|
||||
/// retail). A zero-plane slice is pass-all.</summary>
|
||||
public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius)
|
||||
{
|
||||
if (!_cellPlanes.TryGetValue(cellId, out var slices))
|
||||
return false;
|
||||
for (int s = 0; s < slices.Length; s++)
|
||||
if (SphereInsidePlanes(slices[s], center, radius))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Sphere-vs-the-outside-views (objects in outdoor space seen
|
||||
/// from an interior root through doorways; pass-all under the outdoor
|
||||
/// root's full-screen outside view).</summary>
|
||||
public bool SphereVisibleOutside(in Vector3 center, float radius)
|
||||
{
|
||||
if (OutsideIsFullScreen)
|
||||
return true;
|
||||
for (int s = 0; s < _outsidePlanes.Length; s++)
|
||||
if (SphereInsidePlanes(_outsidePlanes[s], center, radius))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Sphere vs ONE outside slice (the landscape pass draws per
|
||||
/// slice; its statics pre-filter tests against exactly that slice).</summary>
|
||||
public bool SphereVisibleInOutsideSlice(int sliceIndex, in Vector3 center, float radius)
|
||||
{
|
||||
if ((uint)sliceIndex >= (uint)_outsidePlanes.Length)
|
||||
return false;
|
||||
return SphereInsidePlanes(_outsidePlanes[sliceIndex], center, radius);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue