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>
81 lines
2.4 KiB
C#
81 lines
2.4 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.Enums;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Selection;
|
|
|
|
public class WorldPickerCellOcclusionTests
|
|
{
|
|
private static CellPhysics MakeWallAtY10()
|
|
{
|
|
// A quad wall at Y=10 spanning X=-5..5, Z=-5..5 (local space = world space
|
|
// because WorldTransform = Identity). The occluder triangulates it as a fan:
|
|
// tri0 = [0,1,2], tri1 = [0,2,3]. A ray travelling +Y from Y=0 hits it at t≈10.
|
|
var verts = new[]
|
|
{
|
|
new Vector3(-5, 10, -5),
|
|
new Vector3( 5, 10, -5),
|
|
new Vector3( 5, 10, 5),
|
|
new Vector3(-5, 10, 5),
|
|
};
|
|
var poly = new ResolvedPolygon
|
|
{
|
|
Vertices = verts,
|
|
Plane = new System.Numerics.Plane(new Vector3(0, -1, 0), 10f),
|
|
NumPoints = 4,
|
|
SidesType = CullMode.None,
|
|
};
|
|
return new CellPhysics
|
|
{
|
|
BSP = null,
|
|
Resolved = new() { [0] = poly },
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
};
|
|
}
|
|
|
|
private static WorldEntity MakeEntity(uint guid, Vector3 pos) => new()
|
|
{
|
|
Id = guid,
|
|
ServerGuid = guid,
|
|
SourceGfxObjOrSetupId = 0,
|
|
Position = pos,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = System.Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
[Fact]
|
|
public void Pick_RaySphere_EntityBehindWall_OccludedByCellBsp()
|
|
{
|
|
var wall = MakeWallAtY10();
|
|
var entity = MakeEntity(0xABCDu, new Vector3(0, 20, 0)); // entity at Y=20, wall at Y=10
|
|
|
|
var result = WorldPicker.Pick(
|
|
origin: Vector3.Zero,
|
|
direction: Vector3.UnitY,
|
|
candidates: new[] { entity },
|
|
skipServerGuid: 0u,
|
|
cellOccluder: (origin, direction) =>
|
|
CellBspRayOccluder.NearestWallT(origin, direction, new[] { wall }));
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pick_RaySphere_NoWall_HitsEntity()
|
|
{
|
|
var entity = MakeEntity(0xABCDu, new Vector3(0, 20, 0));
|
|
|
|
var result = WorldPicker.Pick(
|
|
origin: Vector3.Zero,
|
|
direction: Vector3.UnitY,
|
|
candidates: new[] { entity },
|
|
skipServerGuid: 0u,
|
|
cellOccluder: null); // null occluder = no occlusion
|
|
|
|
Assert.Equal(0xABCDu, result);
|
|
}
|
|
}
|