acdream/tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs
Erik 0ee328a824 fix(render): Phase U.4c — root indoor visibility at the player's cell (the flap)
The visibility root + portal-side test now use the PLAYER position (visRootPos) in
player mode instead of the camera EYE; the eye still drives the per-frame projection
(envCellViewProj). Live ACDREAM_PROBE_FLAP evidence: the flap was the 3rd-person eye
drifting out of the player's cell -> FindCameraCell returning the STALE cell for its
grace frames -> the doorway portal culled as behind-the-eye -> exit cell + terrain +
shells dropped (res=Grace eyeInRoot=n terrain=Skip on every flap frame). Retail's
CellManager::ChangePosition (0x004559B0) tracks curr_cell by the player; acdream
already roots lighting at the player (GameWindow:7152) for the same chase-cam reason
— visibility was the lone holdout on the eye. Removed the earlier synthetic builder
flap test, which modeled a disproven (side-test) hypothesis; the fix is integration-
level, validated by the visual gate + [flap] probe. App tests 151/151.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:35:21 +02:00

419 lines
22 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class PortalVisibilityBuilderTests
{
private static Matrix4x4 ViewProj()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
return view * proj;
}
private static Vector3[] Quad(float cx, float cy, float halfW, float halfH, float z) => new[]
{
new Vector3(cx - halfW, cy - halfH, z), new Vector3(cx + halfW, cy - halfH, z),
new Vector3(cx + halfW, cy + halfH, z), new Vector3(cx - halfW, cy + halfH, z),
};
// Full-height (y in [-0.9, 0.9]) quad spanning [minX, maxX] in X at plane z.
// Used by the multi-back-portal fixture where the X span is the only thing
// distinguishing the LEFT and RIGHT apertures.
private static Vector3[] QuadX(float minX, float maxX, float z) => new[]
{
new Vector3(minX, -0.9f, z), new Vector3(maxX, -0.9f, z),
new Vector3(maxX, 0.9f, z), new Vector3(minX, 0.9f, z),
};
private static LoadedCell Cell(uint id, params CellPortalInfo[] portals) => new LoadedCell
{
CellId = id, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo>(portals),
};
private static PortalVisibilityFrame Build(LoadedCell cam, Dictionary<uint, LoadedCell> all)
=> PortalVisibilityBuilder.Build(cam, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, ViewProj());
[Fact]
public void Builder_Cellar_WindowClippedToStairwell_NotFullWindow()
{
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.1f, 1.0f, -3f)); // narrow stairwell
var ground = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
ground.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f)); // wide window
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = ground };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
float outsideWidth = frame.OutsideView.MaxX - frame.OutsideView.MinX;
float windowOnlyWidth = PortalFrameTestHelper.ProjectedWidth(
new[] { new Vector3(-1, 0, -6), new Vector3(1, 0, -6) }, ViewProj());
Assert.True(outsideWidth < windowOnlyWidth * 0.5f,
$"OutsideView width {outsideWidth} should be a sliver, far less than full window {windowOnlyWidth}");
}
[Fact]
public void Builder_SealedCellar_NoExitPortal_OutsideViewEmpty()
{
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 0.1f, 1f, -3f));
var inner = Cell(0x0002); // no portals at all
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = inner };
Assert.True(Build(cam, all).OutsideView.IsEmpty);
}
[Fact]
public void Builder_CameraCellWithDirectExit_OutsideViewIsFullWindow()
{
var cam = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 1f, 1f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
Assert.True(frame.OutsideView.MaxX - frame.OutsideView.MinX > 0.3f);
}
[Fact]
public void Builder_BackFacingPortal_NotTraversed()
{
// Portal to 0x0002, but its clip plane puts the camera (origin) on the OUTSIDE.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 0.5f, 0.5f, -3f));
cam.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = -1f, InsideSide = 0 });
// dot = (0,0,1)·origin + (-1) = -1 < 0; InsideSide==0 requires dot >= -eps → camera OUTSIDE → skip.
var ground = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
ground.PortalPolygons.Add(Quad(0, 0, 1f, 1f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = ground };
var frame = Build(cam, all);
Assert.False(frame.CellViews.ContainsKey(0x0002)); // neighbour never reached
Assert.True(frame.OutsideView.IsEmpty); // its window never marked
}
[Fact]
public void Builder_CwWoundExitPortal_OutsideRegionIsCcw()
{
// Exit portal authored CLOCKWISE — the builder must normalize to CCW so downstream stays valid.
var cwQuad = new[]
{
new Vector3(-1, -1, -6), new Vector3(-1, 1, -6), new Vector3(1, 1, -6), new Vector3(1, -1, -6),
};
var cam = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
cam.PortalPolygons.Add(cwQuad);
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
var p = frame.OutsideView.Polygons[0].Vertices;
float area2 = 0f;
for (int i = 0; i < p.Length; i++) { var a = p[i]; var b = p[(i + 1) % p.Length]; area2 += a.X * b.Y - b.X * a.Y; }
Assert.True(area2 > 0f, "clipped OutsideView region should be CCW after winding normalization");
}
[Fact]
public void Builder_CyclicGraph_TerminatesWithBoundedPolys()
{
// A <-> B cycle; B also has an exit window. Must terminate and not blow up.
var a = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -3f));
var b = Cell(0x0002, new CellPortalInfo(0x0001, 0, 0, 0), new CellPortalInfo(0xFFFF, 1, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -2f)); // back to A
b.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f)); // exit window
var all = new Dictionary<uint, LoadedCell> { [0x0001] = a, [0x0002] = b };
var frame = Build(a, all); // must return (no infinite loop)
Assert.False(frame.OutsideView.IsEmpty);
Assert.True(frame.OutsideView.Polygons.Count < 256,
$"OutsideView poly count {frame.OutsideView.Polygons.Count} — termination/dedup regression guard");
}
// -----------------------------------------------------------------------
// Phase U.2a: ordered visible-cell list (closest-first) + grow-watermark
// fixpoint termination (replaces MaxReprocessPerCell hard cap).
// -----------------------------------------------------------------------
// Straight chain A -> B -> C, camera in A looking down -Z. Each onward portal
// is progressively farther in -Z so the camera-to-portal distance is monotonic,
// forcing the priority queue to dequeue A, then B, then C in that order.
private static (LoadedCell[] cells, Dictionary<uint, LoadedCell> lookup) SyntheticChain()
{
const uint A = 0x0001, B = 0x0002, C = 0x0003;
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f)); // portal A->B at z=-2 (nearer)
var b = Cell(B, new CellPortalInfo((ushort)C, 0, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -5f)); // portal B->C at z=-5 (farther)
var c = Cell(C, new CellPortalInfo(0xFFFF, 0, 0, 0));
c.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -8f)); // exit window
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b, [C] = c };
return (new[] { a, b, c }, all);
}
[Fact] // closest-first ordering
public void Build_OrdersVisibleCells_ClosestFirst()
{
var (cells, lookup) = SyntheticChain();
var f = PortalVisibilityBuilder.Build(
cells[0], Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.Equal(new uint[] { 0x0001, 0x0002, 0x0003 }, f.OrderedVisibleCells.ToArray());
}
// Hub cell with 4 rooms, each room portal-linked BACK to the hub (a cycle on
// every spoke). A naive FIFO with no real fixpoint re-enqueues the hub once per
// returning spoke and the rooms once per hub re-process — the watermark must
// converge instead, bounding the visible set to {hub + 4 rooms} with no dupes.
private static (LoadedCell hub, Dictionary<uint, LoadedCell> lookup) SyntheticCyclicHub()
{
const uint HUB = 0x0010;
uint[] rooms = { 0x0011, 0x0012, 0x0013, 0x0014 };
// Hub has one portal to each room; rooms sit at distinct depths so ordering is deterministic.
var hub = Cell(HUB,
new CellPortalInfo((ushort)rooms[0], 0, 0, 0), new CellPortalInfo((ushort)rooms[1], 1, 0, 0),
new CellPortalInfo((ushort)rooms[2], 2, 0, 0), new CellPortalInfo((ushort)rooms[3], 3, 0, 0));
for (int i = 0; i < 4; i++)
hub.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f - i)); // -2,-3,-4,-5
var all = new Dictionary<uint, LoadedCell> { [HUB] = hub };
for (int i = 0; i < 4; i++)
{
var room = Cell(rooms[i], new CellPortalInfo((ushort)HUB, 0, 0, 0)); // links back to hub → cycle
room.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f - i));
all[rooms[i]] = room;
}
return (hub, all);
}
[Fact] // cyclic graph terminates and bounds the visible set
public void Build_CyclicHub_TerminatesAndBounds()
{
var (hub, lookup) = SyntheticCyclicHub();
var f = PortalVisibilityBuilder.Build(
hub, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.True(f.OrderedVisibleCells.Count <= 5,
$"hub + 4 rooms expected, got {f.OrderedVisibleCells.Count} — fixpoint failed to converge");
Assert.Equal(f.OrderedVisibleCells.Count, f.OrderedVisibleCells.Distinct().Count()); // no dup cells
}
// -----------------------------------------------------------------------
// Phase U.2b: reciprocal OtherPortalClip (retail PView::OtherPortalClip
// decomp:433524). When a portal leads to a loaded neighbour, the
// propagated region must ALSO be clipped against the neighbour's matching
// (reciprocal) back-portal polygon — the result is the intersection of the
// opening seen from BOTH sides. This can only tighten, never widen.
// -----------------------------------------------------------------------
// Camera in A looking down -Z through a WIDE near-side portal (A->B). B's
// matching back-portal (B->A) is a NARROW opening at the same plane, so the
// reciprocal opening projects to a strictly smaller NDC region. Without the
// reciprocal clip, B's CellView equals the wide near-side projection; with
// it, B's CellView is bounded by the narrow reciprocal opening.
private static (LoadedCell camCell, LoadedCell neighbour, Dictionary<uint, LoadedCell> lookup)
SyntheticReciprocalPair()
{
const uint A = 0x0001, B = 0x0002;
// A's portal into B: wide opening (half-width 0.9) at z = -3. Its
// reciprocal back-portal lives at index 0 in B (OtherPortalId = 0).
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.9f, 0.9f, -3f));
// B's reciprocal portal back to A: NARROW opening (half-width 0.3),
// same height and plane, so it projects fully inside the near-side rect
// but covers only ~1/3 of its width.
var b = Cell(B, new CellPortalInfo((ushort)A, 0, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.3f, 0.9f, -3f));
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
return (a, b, all);
}
[Fact]
public void Build_AppliesReciprocalOtherPortalClip()
{
var (camCell, neighbour, lookup) = SyntheticReciprocalPair();
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
camCell, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(0x0002), "neighbour cell view should be populated");
// The reciprocal opening's area in NDC: project B's narrow back-portal
// polygon and take its (CCW-magnitude) shoelace area. This is the
// tightest the neighbour region can be — the propagated region must not
// exceed it once both-sides clipping is applied.
float reciprocalArea = ProjectedPolygonArea(neighbour.PortalPolygons[0], vp);
float area = CellViewArea(f.CellViews[0x0002]);
const float eps = 1e-4f;
Assert.True(area <= reciprocalArea + eps,
$"neighbour CellView area {area} must be clipped to the narrower reciprocal opening " +
$"{reciprocalArea} (without OtherPortalClip it equals the WIDE near-side projection)");
// Falsifiability guard: the near-side projection is genuinely WIDER than
// the reciprocal, so a no-op clip would have failed the assertion above.
float nearSideArea = ProjectedPolygonArea(camCell.PortalPolygons[0], vp);
Assert.True(nearSideArea > reciprocalArea * 1.5f,
$"fixture sanity: near-side area {nearSideArea} must dominate reciprocal {reciprocalArea}");
}
[Fact]
public void Build_ReciprocalClip_DegradesGracefully_WhenNoBackPortal()
{
// Same wide A->B opening, but B has NO portal pointing back to A (data gap).
// The reciprocal clip must no-op, so B's CellView equals the WIDE near-side
// projection — proving degradation never under-includes (and never throws).
const uint A = 0x0001, B = 0x0002;
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.9f, 0.9f, -3f));
var b = Cell(B); // no portals at all → no back-portal to match
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
a, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(B), "neighbour must still be reached when no back-portal exists");
float nearSideArea = ProjectedPolygonArea(a.PortalPolygons[0], vp);
float area = CellViewArea(f.CellViews[B]);
Assert.True(System.MathF.Abs(area - nearSideArea) < 1e-3f,
$"with no reciprocal portal the region must equal the near-side projection " +
$"(got {area}, near-side {nearSideArea}) — degrade must not tighten or expand");
}
// -----------------------------------------------------------------------
// Phase U.2b CRITICAL — multiple portals to the SAME neighbour must each
// resolve their OWN reciprocal back-portal (retail arg2->other_portal_id,
// decomp:433557), NOT the first OtherCellId match. This is the real
// Holtburg-cellar shape: cell 0x148 has two portals to 0x149 (poly 40, 41)
// and 0x149 has two reciprocals back to 0x148. A scan-by-first-match clips
// BOTH near-side openings against the FIRST reciprocal — if the apertures
// are disjoint, the second opening's intersection underflows to empty and
// its geometry is HIDDEN (under-inclusion). Direct-index resolution clips
// each opening against the matching reciprocal, so both survive.
// -----------------------------------------------------------------------
// Camera in A looking down -Z. A has TWO portals to the SAME neighbour B,
// with DISJOINT openings: a LEFT aperture (x in [-0.9,-0.3]) and a RIGHT
// aperture (x in [0.3,0.9]). B has two reciprocals back to A, one matching
// each side. A.Portals[0].OtherPortalId = 0 (B's LEFT reciprocal),
// A.Portals[1].OtherPortalId = 1 (B's RIGHT reciprocal).
//
// Scan-by-first-match resolves BOTH A-portals to B.Portals[0] (LEFT),
// so the RIGHT near-side region (x>0) intersects an x<0 reciprocal → empty
// → B's CellView never reaches the RIGHT aperture. Direct-index keeps it.
private static (LoadedCell camCell, LoadedCell neighbour, Dictionary<uint, LoadedCell> lookup)
SyntheticMultiBackPortalPair()
{
const uint A = 0x0001, B = 0x0002;
// A: portal[0] → B through the LEFT opening, portal[1] → B through the RIGHT opening.
// The OtherPortalId back-links pick the matching reciprocal in B (0 = LEFT, 1 = RIGHT).
var a = Cell(A,
new CellPortalInfo((ushort)B, PolygonId: 0, Flags: 0, OtherPortalId: 0), // LEFT → B recip[0]
new CellPortalInfo((ushort)B, PolygonId: 1, Flags: 0, OtherPortalId: 1)); // RIGHT → B recip[1]
a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -3f)); // LEFT near-side aperture
a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f)); // RIGHT near-side aperture
// B: two reciprocals back to A. Index 0 covers the LEFT opening, index 1
// the RIGHT opening — disjoint, same plane (z=-3) as the near side so each
// reciprocal exactly overlaps its matching A aperture.
var b = Cell(B,
new CellPortalInfo((ushort)A, PolygonId: 0, Flags: 0, OtherPortalId: 0),
new CellPortalInfo((ushort)A, PolygonId: 1, Flags: 0, OtherPortalId: 1));
b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -3f)); // reciprocal[0] = LEFT
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f)); // reciprocal[1] = RIGHT
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
return (a, b, all);
}
[Fact]
public void Build_MultiplePortalsToSameNeighbour_EachResolvesOwnReciprocal()
{
var (camCell, neighbour, lookup) = SyntheticMultiBackPortalPair();
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
camCell, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(0x0002), "neighbour cell view should be populated");
// The RIGHT reciprocal opening (B.PortalPolygons[1]) projects to a region
// with strictly positive NDC X. Geometry visible through the SECOND opening
// survives ONLY if A.Portals[1] was clipped against B's RIGHT reciprocal
// (index 1) rather than the LEFT one (index 0).
var rightNdc = PortalProjection.ProjectToNdc(neighbour.PortalPolygons[1], Matrix4x4.Identity, vp);
float rightMinX = float.MaxValue, rightMaxX = float.MinValue;
foreach (var v in rightNdc) { if (v.X < rightMinX) rightMinX = v.X; if (v.X > rightMaxX) rightMaxX = v.X; }
Assert.True(rightMinX > 0f, $"fixture sanity: RIGHT reciprocal must project to positive NDC X (got minX {rightMinX})");
// The neighbour CellView must extend into the RIGHT aperture. With the
// scan-by-first-match bug both near-side openings clip against the LEFT
// reciprocal (x<0), so the CellView's MaxX stays well left of rightMinX
// and this assertion FAILS (the RIGHT opening's geometry is hidden).
var bView = f.CellViews[0x0002];
Assert.True(bView.MaxX >= rightMinX - 1e-4f,
$"neighbour CellView MaxX {bView.MaxX} must reach the RIGHT reciprocal opening " +
$"[{rightMinX}, {rightMaxX}] — under scan-by-first-match the second opening is clipped " +
$"against the LEFT reciprocal and HIDDEN (under-inclusion bug #102 M-4).");
// And the LEFT aperture must still be present (the first opening was never
// in question) — guards against a fix that accidentally drops the LEFT side.
var leftNdc = PortalProjection.ProjectToNdc(neighbour.PortalPolygons[0], Matrix4x4.Identity, vp);
float leftMinX = float.MaxValue;
foreach (var v in leftNdc) if (v.X < leftMinX) leftMinX = v.X;
Assert.True(bView.MinX <= leftMinX + 1e-4f,
$"neighbour CellView MinX {bView.MinX} must still cover the LEFT reciprocal opening (minX {leftMinX})");
}
// -----------------------------------------------------------------------
// Phase U.4c: an earlier synthetic flap test (Build_NearBoundaryIntermediatePortal)
// lived here. It modeled the doorway flap as a BUILDER side-test cull dropping the
// exit cell. The live ACDREAM_PROBE_FLAP capture (2026-05-31) DISPROVED that model:
// the real cause is the visibility ROOT being driven by the 3rd-person camera EYE —
// the eye drifts out of the player's cell, FindCameraCell returns a STALE cell for
// its grace frames, and the doorway is then culled as "behind" the eye. The fix is
// at the GameWindow integration level (root visibility at the PLAYER's cell:
// visRootPos), NOT in the builder — the builder's side test is correct and unchanged,
// so the old test asserted a non-bug and was removed rather than left red. The fix is
// validated by the visual gate + the [flap] probe (RenderingDiagnostics.ProbeFlapEnabled);
// see docs/research/2026-05-31-u4c-flap-characterization.md.
// -----------------------------------------------------------------------
// Signed-area-magnitude (shoelace) sum over a CellView's polygons in NDC.
private static float CellViewArea(CellView view)
{
float total = 0f;
foreach (var poly in view.Polygons) total += ShoelaceAbs(poly.Vertices);
return total;
}
// Project a cell-local polygon (identity world transform in these fixtures)
// to NDC via the same path the builder uses, then take its area magnitude.
private static float ProjectedPolygonArea(Vector3[] localPoly, Matrix4x4 vp)
{
var ndc = PortalProjection.ProjectToNdc(localPoly, Matrix4x4.Identity, vp);
return ShoelaceAbs(ndc);
}
private static float ShoelaceAbs(Vector2[] poly)
{
if (poly == null || poly.Length < 3) return 0f;
float area2 = 0f;
for (int i = 0; i < poly.Length; i++)
{
var p = poly[i];
var q = poly[(i + 1) % poly.Length];
area2 += p.X * q.Y - q.X * p.Y;
}
return System.MathF.Abs(area2) * 0.5f;
}
}
internal static class PortalFrameTestHelper
{
public static float ProjectedWidth(Vector3[] worldSeg, Matrix4x4 vp)
{
var a = Vector4.Transform(new Vector4(worldSeg[0], 1f), vp);
var b = Vector4.Transform(new Vector4(worldSeg[1], 1f), vp);
return System.MathF.Abs(a.X / a.W - b.X / b.W);
}
}