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
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