feat(render) Campaign FW1: port the view machinery (xformStart, polyClipFinish, copy_view)
WalkScreenClip ports PrimD3DRender::xformStart @0x0059b990 (homogeneous viewport coords, y-flip, no divide) and ACRender::polyClipFinish @0x006b6d00 (w>=cdstW plane then last-to-first edge passes, inside = side<=0 homogeneous 2D cross, reverse-scan passes with original-winding restore, <3 early-outs). WalkViews ports the view_type/portal_view_type data model and Render::copy_view @0x0054dfc0 exactly: in-place divide, the keep/last/stl/second pruning bookkeeping with all three closing wrap checks, <3 reject leaving dest untouched, cap 31, pool-base reset at view_count==0, retail fabs on copy, and edge planes N=normalize(cross(ray[k+1],ray[k])), d=-dot(N,eye) behind an IWalkRayCaster seam. Thirteen new tests; Walk namespace 85/85. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
368c480bc2
commit
11ca527fb9
4 changed files with 718 additions and 0 deletions
163
tests/AcDream.App.Tests/Rendering/Walk/WalkCopyViewTests.cs
Normal file
163
tests/AcDream.App.Tests/Rendering/Walk/WalkCopyViewTests.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
public sealed class WalkCopyViewTests
|
||||
{
|
||||
private sealed class LinearRayCaster : IWalkRayCaster
|
||||
{
|
||||
public Vector3 RayThrough(float screenX, float screenY)
|
||||
=> new(screenX, screenY, 100f);
|
||||
}
|
||||
|
||||
private static readonly LinearRayCaster Rays = new();
|
||||
private static readonly Vector3 Eye = new(1f, 2f, 3f);
|
||||
|
||||
private static WalkScreenPoint Pt(float x, float y, float w = 1f)
|
||||
=> new(x * w, y * w, 0f, w);
|
||||
|
||||
[Fact]
|
||||
public void Full_viewport_quad_appends_retails_root_view()
|
||||
{
|
||||
var dest = new WalkPortalView();
|
||||
|
||||
bool ok = WalkCopyView.AppendFullViewportQuad(dest, Rays, Eye, 640f, 480f);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(1, dest.ViewCount);
|
||||
WalkViewPoly poly = dest.View.Polys[0];
|
||||
Assert.Equal(4, poly.VertexCount);
|
||||
Assert.Equal(0, poly.VertexIndex);
|
||||
Assert.Equal((0f, 640f, 0f, 480f), (poly.XMin, poly.XMax, poly.YMin, poly.YMax));
|
||||
// Vertex order (0,H)(W,H)(W,0)(0,0) + the closing duplicate.
|
||||
Assert.Equal(new Vector2(0, 480), dest.View.Vertices[0].Point);
|
||||
Assert.Equal(new Vector2(640, 480), dest.View.Vertices[1].Point);
|
||||
Assert.Equal(new Vector2(640, 0), dest.View.Vertices[2].Point);
|
||||
Assert.Equal(new Vector2(0, 0), dest.View.Vertices[3].Point);
|
||||
Assert.Equal(dest.View.Vertices[0].Point, dest.View.Vertices[4].Point);
|
||||
Assert.Equal(5, dest.View.VertexCountTotal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Collinear_midpoint_on_an_edge_is_pruned()
|
||||
{
|
||||
var dest = new WalkPortalView();
|
||||
Span<WalkScreenPoint> square =
|
||||
[
|
||||
Pt(0, 0), Pt(50, 0), Pt(100, 0), Pt(100, 100), Pt(0, 100),
|
||||
];
|
||||
|
||||
bool ok = WalkCopyView.Append(dest, square, Rays, Eye);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(4, dest.View.Polys[0].VertexCount);
|
||||
Assert.Equal(new Vector2(0, 0), dest.View.Vertices[0].Point);
|
||||
Assert.Equal(new Vector2(100, 0), dest.View.Vertices[1].Point);
|
||||
Assert.Equal(new Vector2(100, 100), dest.View.Vertices[2].Point);
|
||||
Assert.Equal(new Vector2(0, 100), dest.View.Vertices[3].Point);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Near_duplicate_points_within_one_pixel_are_dropped()
|
||||
{
|
||||
var dest = new WalkPortalView();
|
||||
Span<WalkScreenPoint> poly =
|
||||
[
|
||||
Pt(0, 0), Pt(0.5f, 0.5f), Pt(100, 0), Pt(50, 100),
|
||||
];
|
||||
|
||||
bool ok = WalkCopyView.Append(dest, poly, Rays, Eye);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(3, dest.View.Polys[0].VertexCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fewer_than_three_survivors_reject_and_leave_dest_untouched()
|
||||
{
|
||||
var dest = new WalkPortalView();
|
||||
Span<WalkScreenPoint> tiny =
|
||||
[
|
||||
Pt(0, 0), Pt(0.5f, 0f), Pt(0f, 0.5f),
|
||||
];
|
||||
|
||||
bool ok = WalkCopyView.Append(dest, tiny, Rays, Eye);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Equal(0, dest.ViewCount);
|
||||
Assert.Empty(dest.View.Polys);
|
||||
Assert.Empty(dest.View.Vertices);
|
||||
Assert.Equal(0, dest.View.VertexCountTotal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Homogeneous_points_are_perspective_divided_before_storage()
|
||||
{
|
||||
var dest = new WalkPortalView();
|
||||
Span<WalkScreenPoint> tri =
|
||||
[
|
||||
Pt(0, 0, w: 2f), Pt(100, 0, w: 2f), Pt(50, 100, w: 2f),
|
||||
];
|
||||
|
||||
bool ok = WalkCopyView.Append(dest, tri, Rays, Eye);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Equal(new Vector2(0, 0), dest.View.Vertices[0].Point);
|
||||
Assert.Equal(new Vector2(100, 0), dest.View.Vertices[1].Point);
|
||||
Assert.Equal(new Vector2(50, 100), dest.View.Vertices[2].Point);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Edge_planes_are_next_cross_current_normalized_through_the_eye()
|
||||
{
|
||||
var dest = new WalkPortalView();
|
||||
Span<WalkScreenPoint> tri = [Pt(0, 0), Pt(100, 0), Pt(50, 100)];
|
||||
|
||||
Assert.True(WalkCopyView.Append(dest, tri, Rays, Eye));
|
||||
|
||||
// Edge k starts at vertex k: plane N = normalize(cross(ray[k+1], ray[k])).
|
||||
Vector3 ray0 = Rays.RayThrough(0, 0);
|
||||
Vector3 ray1 = Rays.RayThrough(100, 0);
|
||||
Vector3 expected = Vector3.Normalize(Vector3.Cross(ray1, ray0));
|
||||
WalkPlane plane = dest.View.Vertices[0].Plane;
|
||||
Assert.Equal(expected.X, plane.Normal.X, 5);
|
||||
Assert.Equal(expected.Y, plane.Normal.Y, 5);
|
||||
Assert.Equal(expected.Z, plane.Normal.Z, 5);
|
||||
Assert.Equal(-Vector3.Dot(expected, Eye), plane.D, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pool_resets_when_view_count_returns_to_zero()
|
||||
{
|
||||
var dest = new WalkPortalView();
|
||||
Span<WalkScreenPoint> tri = [Pt(0, 0), Pt(100, 0), Pt(50, 100)];
|
||||
Assert.True(WalkCopyView.Append(dest, tri, Rays, Eye));
|
||||
int firstTotal = dest.View.VertexCountTotal;
|
||||
|
||||
dest.ResetForPush(); // curr_view_push: ViewCount back to 0
|
||||
Span<WalkScreenPoint> tri2 = [Pt(0, 0), Pt(200, 0), Pt(100, 200)];
|
||||
Assert.True(WalkCopyView.Append(dest, tri2, Rays, Eye));
|
||||
|
||||
Assert.Equal(1, dest.ViewCount);
|
||||
Assert.Single(dest.View.Polys);
|
||||
Assert.Equal(firstTotal, dest.View.VertexCountTotal); // pool restarted at 0
|
||||
Assert.Equal(0, dest.View.Polys[0].VertexIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Second_append_extends_the_shared_pool()
|
||||
{
|
||||
var dest = new WalkPortalView();
|
||||
Span<WalkScreenPoint> tri = [Pt(0, 0), Pt(100, 0), Pt(50, 100)];
|
||||
Assert.True(WalkCopyView.Append(dest, tri, Rays, Eye));
|
||||
Span<WalkScreenPoint> tri2 = [Pt(0, 0), Pt(200, 0), Pt(100, 200)];
|
||||
|
||||
Assert.True(WalkCopyView.Append(dest, tri2, Rays, Eye));
|
||||
|
||||
Assert.Equal(2, dest.ViewCount);
|
||||
Assert.Equal(2, dest.View.Polys.Count);
|
||||
Assert.Equal(4, dest.View.Polys[1].VertexIndex); // after tri's 3 + dup
|
||||
Assert.Equal(8, dest.View.VertexCountTotal);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
public sealed class WalkScreenClipTests
|
||||
{
|
||||
private const float W = 640f, H = 480f;
|
||||
|
||||
// The root full-viewport quad in retail's vertex order (0,H)(W,H)(W,0)(0,0).
|
||||
private static readonly Vector2[] RootQuad =
|
||||
[
|
||||
new(0, H), new(W, H), new(W, 0), new(0, 0),
|
||||
];
|
||||
|
||||
private static WalkScreenPoint Pt(float x, float y, float w = 1f)
|
||||
=> new(x * w, y * w, 0f, w); // homogeneous: screen * w
|
||||
|
||||
[Fact]
|
||||
public void Transform_maps_clip_center_to_screen_center_with_y_flip()
|
||||
{
|
||||
Matrix4x4 identity = Matrix4x4.Identity;
|
||||
|
||||
WalkScreenPoint center = WalkScreenClip.TransformToScreen(
|
||||
Vector3.Zero, identity, W, H);
|
||||
Assert.Equal(W / 2, center.X);
|
||||
Assert.Equal(H / 2, center.Y);
|
||||
Assert.Equal(1f, center.W);
|
||||
|
||||
// Clip y = +1 (top of clip space) lands at screen y = 0 (top-left origin).
|
||||
WalkScreenPoint top = WalkScreenClip.TransformToScreen(
|
||||
new Vector3(0, 1, 0), identity, W, H);
|
||||
Assert.Equal(0f, top.Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fully_inside_polygon_survives_unchanged_with_original_winding()
|
||||
{
|
||||
Span<WalkScreenPoint> tri = [Pt(100, 100), Pt(300, 120), Pt(200, 300)];
|
||||
Span<WalkScreenPoint> outPts = stackalloc WalkScreenPoint[16];
|
||||
|
||||
int n = WalkScreenClip.ClipAgainstView(tri, RootQuad, outPts);
|
||||
|
||||
Assert.Equal(3, n);
|
||||
Assert.Equal(100f, outPts[0].X);
|
||||
Assert.Equal(300f, outPts[1].X);
|
||||
Assert.Equal(200f, outPts[2].X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Polygon_straddling_the_left_edge_is_clipped_at_x_zero()
|
||||
{
|
||||
Span<WalkScreenPoint> tri = [Pt(-100, 100), Pt(100, 100), Pt(100, 300)];
|
||||
Span<WalkScreenPoint> outPts = stackalloc WalkScreenPoint[16];
|
||||
|
||||
int n = WalkScreenClip.ClipAgainstView(tri, RootQuad, outPts);
|
||||
|
||||
Assert.True(n >= 3);
|
||||
for (int i = 0; i < n; i++)
|
||||
Assert.True(outPts[i].X / outPts[i].W >= -0.001f, $"vertex {i} left of x=0");
|
||||
// Something was actually cut (an intersection vertex exists at x≈0).
|
||||
bool touchesEdge = false;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (MathF.Abs(outPts[i].X / outPts[i].W) < 0.001f) touchesEdge = true;
|
||||
Assert.True(touchesEdge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Polygon_fully_outside_one_edge_returns_zero()
|
||||
{
|
||||
Span<WalkScreenPoint> tri = [Pt(-300, 100), Pt(-100, 100), Pt(-200, 300)];
|
||||
Span<WalkScreenPoint> outPts = stackalloc WalkScreenPoint[16];
|
||||
|
||||
Assert.Equal(0, WalkScreenClip.ClipAgainstView(tri, RootQuad, outPts));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void W_plane_clips_points_behind_the_eye()
|
||||
{
|
||||
// One vertex behind the eye (w < cdstW); survivors get intersections
|
||||
// at w == cdstW rather than dropping the polygon.
|
||||
Span<WalkScreenPoint> tri =
|
||||
[
|
||||
Pt(100, 100), Pt(300, 100), new WalkScreenPoint(200, 200, 0, -0.5f),
|
||||
];
|
||||
Span<WalkScreenPoint> outPts = stackalloc WalkScreenPoint[16];
|
||||
|
||||
int n = WalkScreenClip.ClipAgainstView(tri, RootQuad, outPts);
|
||||
|
||||
Assert.True(n >= 3);
|
||||
for (int i = 0; i < n; i++)
|
||||
Assert.True(outPts[i].W >= WalkScreenClip.MinW - 1e-6f);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue