WorldPicker.Pick previously had no occlusion test — any entity along the click ray within maxDistance was a candidate, including ones behind walls. Adds the CellBspRayOccluder static helper that Möller-Trumbore-tests the click ray against every polygon in every currently-cached EnvCell BSP, returning the nearest wall-hit `t`. Both Pick overloads gate candidate selection by that wall-t (legacy ray-sphere via world-space `t`, screen-rect via camera-space clip.W depth — matching ScreenProjection.TryProjectSphereToScreenRect's convention). PhysicsDataCache exposes a new CellStructIds snapshot accessor so the caller can iterate without needing the private cache dictionary. CellPhysics.BSP/PhysicsPolygons/Vertices relaxed from required to nullable so test fixtures can construct a CellPhysics from Resolved alone without a real DAT BSP object. GameWindow snapshots the loaded cell physics on each Pick call and passes the occluder callback. Closes #86. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
86 lines
3 KiB
C#
86 lines
3 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using DatReaderWriter.Enums;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Selection;
|
|
|
|
public class CellBspRayOccluderTests
|
|
{
|
|
// Build a CellPhysics with a single triangular poly at world-Y=10.
|
|
// Triangle vertices in local space, world transform = identity.
|
|
// Uses the Resolved-only constructor path (BSP = null is allowed after Phase 1 relaxation).
|
|
private static CellPhysics MakeWallCell()
|
|
{
|
|
var verts = new[]
|
|
{
|
|
new Vector3(-5, 10, 0),
|
|
new Vector3( 5, 10, 0),
|
|
new Vector3( 0, 10, 5),
|
|
};
|
|
var poly = new ResolvedPolygon
|
|
{
|
|
Vertices = verts,
|
|
Plane = new System.Numerics.Plane(new Vector3(0, -1, 0), 10f),
|
|
NumPoints = 3,
|
|
SidesType = CullMode.None,
|
|
};
|
|
return new CellPhysics
|
|
{
|
|
BSP = null, // Occluder doesn't use BSP — direct poly iteration.
|
|
Resolved = new() { [0] = poly },
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
};
|
|
}
|
|
|
|
[Fact]
|
|
public void NearestWallT_RayHitsTriangle_ReturnsHitDistance()
|
|
{
|
|
var cell = MakeWallCell();
|
|
var origin = new Vector3(0, 0, 1);
|
|
var direction = Vector3.UnitY; // travels +Y toward the wall at Y=10
|
|
float t = CellBspRayOccluder.NearestWallT(origin, direction, new[] { cell });
|
|
Assert.True(t > 9.9f && t < 10.1f, $"expected ~10, got {t}");
|
|
}
|
|
|
|
[Fact]
|
|
public void NearestWallT_RayMisses_ReturnsPositiveInfinity()
|
|
{
|
|
var cell = MakeWallCell();
|
|
var origin = new Vector3(0, 0, 1);
|
|
var direction = -Vector3.UnitY; // travels AWAY from the wall
|
|
float t = CellBspRayOccluder.NearestWallT(origin, direction, new[] { cell });
|
|
Assert.True(float.IsPositiveInfinity(t), $"expected +inf, got {t}");
|
|
}
|
|
|
|
[Fact]
|
|
public void NearestWallT_EmptyCellList_ReturnsPositiveInfinity()
|
|
{
|
|
var origin = Vector3.Zero;
|
|
var direction = Vector3.UnitY;
|
|
float t = CellBspRayOccluder.NearestWallT(origin, direction, System.Array.Empty<CellPhysics>());
|
|
Assert.True(float.IsPositiveInfinity(t));
|
|
}
|
|
|
|
[Fact]
|
|
public void NearestWallT_TwoCells_ReturnsNearer()
|
|
{
|
|
var nearCell = MakeWallCell(); // wall at Y=10
|
|
var farCell = MakeWallCell();
|
|
// Move farCell's transform to push it to Y=20.
|
|
farCell = new CellPhysics
|
|
{
|
|
BSP = null,
|
|
Resolved = nearCell.Resolved,
|
|
WorldTransform = Matrix4x4.CreateTranslation(0, 10, 0),
|
|
InverseWorldTransform = Matrix4x4.CreateTranslation(0, -10, 0),
|
|
};
|
|
|
|
var origin = new Vector3(0, 0, 1);
|
|
var direction = Vector3.UnitY;
|
|
float t = CellBspRayOccluder.NearestWallT(origin, direction, new[] { farCell, nearCell });
|
|
Assert.True(t < 11f, $"expected near-cell hit ~10, got {t}");
|
|
}
|
|
}
|