using System; using System.Collections.Generic; using System.Numerics; namespace AcDream.App.Rendering; /// /// T3 (BR-5): the port of retail's Render::viewconeCheck (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 /// (view_vertex { Vec2D pt; Plane plane }, 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). /// /// Our views are clip-space half-planes (≤8 per slice, /// 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). /// /// 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. /// public sealed class ViewconeCuller { private const int MaxRetainedCellPlaneSets = 512; private const int MaxRetainedPlanesPerCell = 256; private const int MaxRetainedSlicesPerCell = 64; private readonly Dictionary _cellPlanes = new(); private readonly Stack _planeSetPool = new(); private PlaneSet _outsidePlanes = new(); private readonly record struct SliceRange(int Start, int Count); private readonly record struct LiftedPlane(Vector4 Equation, float NormalLength); /// /// Contiguous per-cell plane storage. Reusing two Lists per visible cell /// avoids rebuilding a jagged array graph on every render frame while /// retaining the exact slice boundaries used by retail's any-view test. /// private sealed class PlaneSet { public List Planes { get; } = new(); public List Slices { get; } = new(); public bool IsRetainable => Planes.Capacity <= MaxRetainedPlanesPerCell && Slices.Capacity <= MaxRetainedSlicesPerCell; public void Reset() { Planes.Clear(); Slices.Clear(); } } /// True when the outside view is a full-screen pass-all (the /// synthetic outdoor root) — every outside-test passes. public bool OutsideIsFullScreen { get; private set; } public static ViewconeCuller Build( ClipFrameAssembly assembly, in Matrix4x4 viewProjection, ViewconeCuller? reuse = null) { ArgumentNullException.ThrowIfNull(assembly); var culler = reuse ?? new ViewconeCuller(); culler.Reset(); foreach (var (cellId, slices) in assembly.CellIdToViewSlices) { PlaneSet lifted = culler.RentPlaneSet(); for (int s = 0; s < slices.Length; s++) AppendLiftedSlice(lifted, slices[s].Planes, viewProjection); culler._cellPlanes[cellId] = lifted; } var outside = assembly.OutsideViewSlices; bool fullScreen = false; for (int s = 0; s < outside.Length; s++) { AppendLiftedSlice(culler._outsidePlanes, outside[s].Planes, viewProjection); if (outside[s].Planes.Length == 0) fullScreen = true; } culler.OutsideIsFullScreen = fullScreen; return culler; } private void Reset() { foreach (PlaneSet set in _cellPlanes.Values) { set.Reset(); if (set.IsRetainable && _planeSetPool.Count < MaxRetainedCellPlaneSets) _planeSetPool.Push(set); } _cellPlanes.Clear(); if (_outsidePlanes.IsRetainable) _outsidePlanes.Reset(); else _outsidePlanes = new PlaneSet(); OutsideIsFullScreen = false; } private PlaneSet RentPlaneSet() { PlaneSet result = _planeSetPool.Count != 0 ? _planeSetPool.Pop() : new PlaneSet(); result.Reset(); return result; } private static void AppendLiftedSlice( PlaneSet destination, Vector4[] clipPlanes, in Matrix4x4 m) { int start = destination.Planes.Count; for (int i = 0; i < clipPlanes.Length; i++) { var p = clipPlanes[i]; var equation = 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); float normalLength = MathF.Sqrt( equation.X * equation.X + equation.Y * equation.Y + equation.Z * equation.Z); destination.Planes.Add(new LiftedPlane(equation, normalLength)); } destination.Slices.Add(new SliceRange(start, clipPlanes.Length)); } private static bool SphereInsidePlanes( PlaneSet set, SliceRange slice, in Vector3 center, float radius) { int end = slice.Start + slice.Count; for (int i = slice.Start; i < end; i++) { LiftedPlane plane = set.Planes[i]; Vector4 l = plane.Equation; float nLen = plane.NormalLength; 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; } /// 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. public bool SphereVisibleInCell(uint cellId, in Vector3 center, float radius) { if (!_cellPlanes.TryGetValue(cellId, out PlaneSet? set)) return false; for (int s = 0; s < set.Slices.Count; s++) if (SphereInsidePlanes(set, set.Slices[s], center, radius)) return true; return false; } /// 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). public bool SphereVisibleOutside(in Vector3 center, float radius) { if (OutsideIsFullScreen) return true; for (int s = 0; s < _outsidePlanes.Slices.Count; s++) if (SphereInsidePlanes(_outsidePlanes, _outsidePlanes.Slices[s], center, radius)) return true; return false; } /// Sphere vs ONE outside slice (the landscape pass draws per /// slice; its statics pre-filter tests against exactly that slice). public bool SphereVisibleInOutsideSlice(int sliceIndex, in Vector3 center, float radius) { if ((uint)sliceIndex >= (uint)_outsidePlanes.Slices.Count) return false; return SphereInsidePlanes( _outsidePlanes, _outsidePlanes.Slices[sliceIndex], center, radius); } }