test(picker): Cluster A #86 — screen-rect cell-occlusion tests
Phase B's WorldPicker change added cellOccluder to both Pick overloads, but the integration test suite only covered the legacy ray-sphere overload. The production code path (GameWindow.PickAndStoreSelection) uses the screen-rect overload, and its clip.W depth-conversion math had no direct test. Adds two integration tests mirroring the existing ray-sphere variants: - Pick_ScreenRect_EntityBehindWall_OccludedByCellBsp — entity dead- ahead, wall between, with cellOccluder → null. - Pick_ScreenRect_NoWall_HitsEntity — same scene, null occluder → hit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3764867566
commit
4e308d567a
1 changed files with 108 additions and 0 deletions
|
|
@ -47,6 +47,114 @@ public class WorldPickerCellOcclusionTests
|
||||||
MeshRefs = System.Array.Empty<MeshRef>(),
|
MeshRefs = System.Array.Empty<MeshRef>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a quad wall at Z=-10 in front of the camera (identity view,
|
||||||
|
/// camera looking down -Z). The wall spans X=-5..5, Y=-5..5 at Z=-10 —
|
||||||
|
/// large enough to cover the center-pixel ray. An entity at Z=-20 sits
|
||||||
|
/// behind it.
|
||||||
|
///
|
||||||
|
/// Wall normal direction doesn't affect Möller-Trumbore (the occluder
|
||||||
|
/// is two-sided), but the Plane is stored for completeness. For a plane
|
||||||
|
/// at z=-10 with outward normal (0,0,+1): (0,0,1)·(x,y,-10) + D = 0
|
||||||
|
/// → D = 10.
|
||||||
|
/// </summary>
|
||||||
|
private static CellPhysics MakeWallAtZNeg10()
|
||||||
|
{
|
||||||
|
var verts = new[]
|
||||||
|
{
|
||||||
|
new Vector3(-5, -5, -10),
|
||||||
|
new Vector3( 5, -5, -10),
|
||||||
|
new Vector3( 5, 5, -10),
|
||||||
|
new Vector3(-5, 5, -10),
|
||||||
|
};
|
||||||
|
var poly = new ResolvedPolygon
|
||||||
|
{
|
||||||
|
Vertices = verts,
|
||||||
|
Plane = new System.Numerics.Plane(new Vector3(0, 0, 1), 10f),
|
||||||
|
NumPoints = 4,
|
||||||
|
SidesType = CullMode.None,
|
||||||
|
};
|
||||||
|
return new CellPhysics
|
||||||
|
{
|
||||||
|
BSP = null,
|
||||||
|
Resolved = new() { [0] = poly },
|
||||||
|
WorldTransform = Matrix4x4.Identity,
|
||||||
|
InverseWorldTransform = Matrix4x4.Identity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// Screen-rect overload + cell-BSP occlusion
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Production path exercised by GameWindow.PickAndStoreSelection.
|
||||||
|
/// Camera at origin looking down -Z (identity view). Entity at Z=-20
|
||||||
|
/// projects to the center of the viewport. A wall at Z=-10 sits between
|
||||||
|
/// camera and entity; with cellOccluder wired up the entity must be
|
||||||
|
/// occluded → null result.
|
||||||
|
///
|
||||||
|
/// This test specifically covers the clip.W depth-conversion math in
|
||||||
|
/// WorldPicker.Pick's screen-rect overload (issue #86).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Pick_ScreenRect_EntityBehindWall_OccludedByCellBsp()
|
||||||
|
{
|
||||||
|
// Use the same camera convention as WorldPickerRectOverloadTests.StdCam():
|
||||||
|
// identity view, 90-degree FoV, 800×600 viewport. Center pixel = (400,300).
|
||||||
|
var view = Matrix4x4.Identity;
|
||||||
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
||||||
|
MathF.PI * 0.5f, 800f / 600f, 0.1f, 100f);
|
||||||
|
var viewport = new Vector2(800f, 600f);
|
||||||
|
|
||||||
|
var wall = MakeWallAtZNeg10();
|
||||||
|
var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -20));
|
||||||
|
|
||||||
|
// Entity is dead-ahead: center of viewport.
|
||||||
|
var result = WorldPicker.Pick(
|
||||||
|
mouseX: 400f, mouseY: 300f,
|
||||||
|
view, proj, viewport,
|
||||||
|
candidates: new[] { entity },
|
||||||
|
skipServerGuid: 0u,
|
||||||
|
sphereForEntity: e => ((Vector3, float)?)(e.Position, 1.0f),
|
||||||
|
inflatePixels: 8f,
|
||||||
|
cellOccluder: (origin, direction) =>
|
||||||
|
CellBspRayOccluder.NearestWallT(origin, direction, new[] { wall }));
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Same camera and entity as Pick_ScreenRect_EntityBehindWall_OccludedByCellBsp,
|
||||||
|
/// but with a null cellOccluder. Verifies that the no-occluder path still
|
||||||
|
/// resolves the entity to a hit (the new parameter is a pure no-op when null).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Pick_ScreenRect_NoWall_HitsEntity()
|
||||||
|
{
|
||||||
|
var view = Matrix4x4.Identity;
|
||||||
|
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
||||||
|
MathF.PI * 0.5f, 800f / 600f, 0.1f, 100f);
|
||||||
|
var viewport = new Vector2(800f, 600f);
|
||||||
|
|
||||||
|
var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -20));
|
||||||
|
|
||||||
|
var result = WorldPicker.Pick(
|
||||||
|
mouseX: 400f, mouseY: 300f,
|
||||||
|
view, proj, viewport,
|
||||||
|
candidates: new[] { entity },
|
||||||
|
skipServerGuid: 0u,
|
||||||
|
sphereForEntity: e => ((Vector3, float)?)(e.Position, 1.0f),
|
||||||
|
inflatePixels: 8f,
|
||||||
|
cellOccluder: null);
|
||||||
|
|
||||||
|
Assert.Equal(0xABCDu, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
// Ray-sphere overload (legacy path)
|
||||||
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Pick_RaySphere_EntityBehindWall_OccludedByCellBsp()
|
public void Pick_RaySphere_EntityBehindWall_OccludedByCellBsp()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue