acdream/src/AcDream.App/Rendering/Walk/WalkVisibilityMath.cs
Erik 368c480bc2 feat(render) Campaign FW1: flood decomp appendix + the visibility math port
The second decomp round (flood bookkeeping/propagation, view-clip
support, landscape visibility) is archived Ghidra-arbitrated - it caught
a load-bearing BN inversion (InsCellTodoList pops NEAREST-first, which
is what makes the draw list far-to-near) and three more traps (the 192m
elided constant, the min/max double positional swap, the copy_view
cross order - the walk doc section 6 is corrected). WalkVisibilityMath
ports get_pt_limit @0x0054b840, get_clip_height @0x0054cff0,
corner/block_plane_check @0x0054b930/@0x0054d060, block_check
@0x0054dc50, and viewconeCheck @0x0054c250 with retail boundary
semantics (strict cull, inclusive partial, touch-out=Outside /
touch-in=EntirelyInside) under 23 focused tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 09:44:49 +02:00

211 lines
8.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW1 — retail's CPU visibility primitives, ported from the named
/// 2013 decomp (extractions:
/// docs/research/2026-08-30-fw-flood-pseudocode-appendix.md, report 4; all
/// x87-ambiguous branches Ghidra-arbitrated). Two families:
///
/// 1. The landscape interval test — a grid corner's vertical column is
/// classified against each active clip plane as a signed height
/// (<c>Render::get_pt_limit</c> @0x0054b840), and a landblock's z-slab
/// is tested against its four corner columns plane by plane
/// (<c>corner_plane_check</c> @0x0054b930, <c>block_plane_check</c>
/// @0x0054d060, <c>block_check</c> @0x0054dc50).
/// 2. The sphere-vs-view test (<c>Render::viewconeCheck</c> @0x0054c250).
///
/// Both consume the same installed state: the CY near plane (built by
/// <c>Render::update_viewpoint</c>) plus the active view's edge planes
/// (installed by <c>Render::set_view</c>).
/// </summary>
public static class WalkVisibilityMath
{
/// <summary>Retail F_EPSILON (raw float 0x3951B717).</summary>
public const float Epsilon = 0.000199999995f;
/// <summary>get_pt_limit sky cap: a clip height at or above this means
/// the column holds nothing (up-normal arm) / everything (down-normal
/// arm).</summary>
public const float SkyHeight = 1000f;
/// <summary>Sentinel: the column is wholly inside the plane's positive
/// half-space.</summary>
public const float InsideColumn = 0f;
/// <summary>Sentinel: the column is wholly outside (static init 1000+1).</summary>
public const float OutsideColumn = 1001f;
/// <summary>
/// <c>Render::get_pt_limit</c> @0x0054b840: classify the vertical column
/// at (x, y) against one plane. Returns <see cref="InsideColumn"/> /
/// <see cref="OutsideColumn"/>, a positive h (inside only BELOW h —
/// down-pointing normal), or a negative h (inside only ABOVE h —
/// up-pointing normal). Vertical planes (|N.z| ≤ ε) collapse to
/// all-in/all-out via the plane side of (x, y, 0).
/// </summary>
public static float GetPointLimit(float x, float y, in WalkPlane plane)
{
Vector3 n = plane.Normal;
if (n.Z > Epsilon)
{
float h = -((x * n.X + y * n.Y + plane.D) / n.Z);
if (h >= SkyHeight) return OutsideColumn;
return h > 0f ? -h : InsideColumn;
}
if (n.Z < -Epsilon)
{
float h = -((x * n.X + y * n.Y + plane.D) / n.Z);
if (h <= 0f) return OutsideColumn;
return h >= SkyHeight ? InsideColumn : h;
}
// Vertical plane: no z dependence; test point (x, y, 0), retail
// Plane::which_side semantics (NEGATIVE = strictly below -epsilon).
float d = x * n.X + y * n.Y + plane.D;
return d < -Epsilon ? OutsideColumn : InsideColumn;
}
/// <summary>
/// <c>Render::get_clip_height</c> @0x0054cff0: fill one corner's
/// interval vector — bounds[0] vs the CY plane, bounds[1..edgeCount] vs
/// each active edge plane (the loop is INCLUSIVE of edgeCount entries:
/// edgeCount + 1 floats are written).
/// </summary>
public static void FillClipHeights(
float x, float y, in WalkPlane cyPlane, ReadOnlySpan<WalkPlane> edgePlanes,
Span<float> bounds)
{
bounds[0] = GetPointLimit(x, y, cyPlane);
for (int i = 0; i < edgePlanes.Length; i++)
bounds[i + 1] = GetPointLimit(x, y, edgePlanes[i]);
}
/// <summary>
/// <c>Render::corner_plane_check</c> @0x0054b930: one corner's encoding
/// vs the block z-slab [minZ, maxZ]. Boundary semantics are retail's:
/// touching the clip height on the OUT side is Outside; on the IN side
/// is EntirelyInside.
/// </summary>
public static WalkBoundingType CornerPlaneCheck(float bound, float minZ, float maxZ)
{
if (bound == OutsideColumn) return WalkBoundingType.Outside;
if (bound != InsideColumn)
{
if (bound <= 0f)
{
// inside is z >= h, h = -bound
float h = -bound;
if (h > minZ)
{
if (maxZ <= h) return WalkBoundingType.Outside;
return WalkBoundingType.PartiallyInside;
}
}
else if (bound < maxZ)
{
// inside is z <= h, h = bound; the block top pokes above h
if (bound <= minZ) return WalkBoundingType.Outside;
return WalkBoundingType.PartiallyInside;
}
}
return WalkBoundingType.EntirelyInside;
}
/// <summary>
/// <c>Render::block_plane_check</c> @0x0054d060: four corners vs one
/// plane. Outside/EntirelyInside require unanimity; anything mixed is
/// PartiallyInside.
/// </summary>
public static WalkBoundingType BlockPlaneCheck(
float b1, float b2, float b3, float b4, float minZ, float maxZ)
{
WalkBoundingType c1 = CornerPlaneCheck(b1, minZ, maxZ);
WalkBoundingType c2 = CornerPlaneCheck(b2, minZ, maxZ);
WalkBoundingType c3 = CornerPlaneCheck(b3, minZ, maxZ);
WalkBoundingType c4 = CornerPlaneCheck(b4, minZ, maxZ);
if (c1 == WalkBoundingType.Outside)
{
if (c2 == WalkBoundingType.Outside
&& c3 == WalkBoundingType.Outside
&& c4 == WalkBoundingType.Outside)
{
return WalkBoundingType.Outside;
}
}
else if (c1 == WalkBoundingType.EntirelyInside
&& c2 == WalkBoundingType.EntirelyInside
&& c3 == WalkBoundingType.EntirelyInside
&& c4 == WalkBoundingType.EntirelyInside)
{
return WalkBoundingType.EntirelyInside;
}
return WalkBoundingType.PartiallyInside;
}
/// <summary>
/// <c>Render::block_check</c> @0x0054dc50: the block-vs-view test over
/// four corner interval vectors. Retail's call site passes
/// (max_zval, min_zval) and an internal double positional swap hands
/// corner_plane_check (min, max) — this port takes (maxZ, minZ) to
/// mirror the caller's argument order and performs the same swap, so
/// call sites read like the decomp. Any plane with all four corners
/// outside culls; any partial plane demotes stickily; EntirelyInside
/// requires unanimity on every plane. planeCount = the active view's
/// edge count (bounds[0] is the CY plane, tested first with early-out).
/// </summary>
public static WalkBoundingType BlockCheck(
ReadOnlySpan<float> corner00, ReadOnlySpan<float> corner01,
ReadOnlySpan<float> corner10, ReadOnlySpan<float> corner11,
int planeCount, float maxZ, float minZ)
{
WalkBoundingType result = BlockPlaneCheck(
corner00[0], corner01[0], corner10[0], corner11[0], minZ, maxZ);
if (result == WalkBoundingType.Outside) return WalkBoundingType.Outside;
for (int k = 1; k <= planeCount; k++)
{
WalkBoundingType r = BlockPlaneCheck(
corner00[k], corner01[k], corner10[k], corner11[k], minZ, maxZ);
if (r == WalkBoundingType.Outside) return WalkBoundingType.Outside;
if (r == WalkBoundingType.PartiallyInside)
result = WalkBoundingType.PartiallyInside;
}
return result;
}
/// <summary>
/// <c>Render::viewconeCheck</c> @0x0054c250 (plane-test half): a sphere
/// (center already in viewer-block space, radius already scaled) vs the
/// CY plane plus the active view's edge planes. Cull is STRICT
/// (d &lt; r); the partial flag is INCLUSIVE (d ≤ r) — a sphere exactly
/// tangent from inside counts Partial, not EntirelyInside. The retail
/// body's side effects (publishing local_object_center/radius) belong
/// to the walk context, not this math.
/// </summary>
public static WalkBoundingType ViewconeCheck(
Vector3 center, float radius, in WalkPlane cyPlane,
ReadOnlySpan<WalkPlane> edgePlanes)
{
float d = Vector3.Dot(cyPlane.Normal, center) + cyPlane.D;
if (d < -radius) return WalkBoundingType.Outside;
bool partial = d <= radius;
foreach (ref readonly WalkPlane plane in edgePlanes)
{
d = Vector3.Dot(plane.Normal, center) + plane.D;
if (d < -radius) return WalkBoundingType.Outside;
if (d <= radius) partial = true;
}
return partial ? WalkBoundingType.PartiallyInside : WalkBoundingType.EntirelyInside;
}
}
/// <summary>Retail <c>Plane</c>: dot(N, p) + d, positive side = inside.</summary>
public readonly record struct WalkPlane(Vector3 Normal, float D);
/// <summary>Retail <c>BoundingType</c>, values used raw by the walk.</summary>
public enum WalkBoundingType
{
Outside = 0,
PartiallyInside = 1,
EntirelyInside = 2,
}