feat(render) Campaign FW1: the frame-walk composition root
RetailFrameWalk composes the ported machinery into retail frame shapes: SmartBox::RenderNormalMode rooting (camera-cell low word < 0x100), PView::DrawInside + the DrawCells event half (interior pview, the traces pv=009d4a80), LScape::draw (per-view visibility, blocks far-to-near, per-block cells far-to-near, buildings at their cell turn) and RenderDeviceD3D::DrawBuilding (BLD at entry before the degrade check; two-pass BSP portal walk per active view on the outdoor pview, the traces pv=009d4b08). WalkLandscape ports the block grid + draw_check_blocks/landcell_check visibility (192 m/24 m pitch, viewer-relative, union across views, never downgrade). Six composition tests: outdoor far-to-near emission, degrade-entry event, interior ov=0/ov=1 shapes, rooting, complete view unwind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f05b93b5b5
commit
4b401a08ed
3 changed files with 638 additions and 0 deletions
224
tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs
Normal file
224
tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
public sealed class RetailFrameWalkTests
|
||||
{
|
||||
private sealed class Recorder : IWalkEventSink
|
||||
{
|
||||
public readonly List<WalkEvent> Events = new();
|
||||
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
|
||||
|
||||
public string Signature()
|
||||
=> string.Join("|", Events.Select(e => e.Kind switch
|
||||
{
|
||||
WalkEventKind.Landscape => "LS",
|
||||
WalkEventKind.Building => $"BLD:{e.CellId:x8}",
|
||||
WalkEventKind.DrawInside => $"DI:{e.CellId:x8}",
|
||||
WalkEventKind.DrawCells =>
|
||||
$"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}",
|
||||
_ => "?",
|
||||
}));
|
||||
}
|
||||
|
||||
private sealed class Caster : IWalkRayCaster
|
||||
{
|
||||
public Vector3 RayThrough(float screenX, float screenY)
|
||||
=> new(screenX, screenY, 100f);
|
||||
}
|
||||
|
||||
private sealed class TestContext : IWalkFrameContext, IRetailFrameWalkContext
|
||||
{
|
||||
public readonly Dictionary<uint, WalkCell> Cells = new();
|
||||
private readonly Matrix4x4 _viewProj;
|
||||
|
||||
public TestContext()
|
||||
{
|
||||
Matrix4x4 view = Matrix4x4.CreateLookAt(
|
||||
Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
|
||||
Matrix4x4 proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
|
||||
_viewProj = view * proj;
|
||||
}
|
||||
|
||||
public Vector3 ViewpointIn(WalkCell cell) => Vector3.Zero;
|
||||
public Matrix4x4 ObjectToClip(WalkCell cell) => _viewProj;
|
||||
public WalkCell? GetVisible(uint cellId) => Cells.GetValueOrDefault(cellId);
|
||||
public IWalkRayCaster Rays { get; } = new Caster();
|
||||
public Vector3 WorldViewpoint => Vector3.Zero;
|
||||
public float ViewportWidth => 640f;
|
||||
public float ViewportHeight => 480f;
|
||||
|
||||
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
|
||||
public IWalkFrameContext CellContext => this;
|
||||
// Permissive near plane: every column wholly inside.
|
||||
public WalkPlane CyPlane => new(new Vector3(0, 0, 1), 0f);
|
||||
public void SetActiveView(WalkPortalView views, int index) { }
|
||||
|
||||
public int ClipBuildingPolygon(
|
||||
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
|
||||
=> 0; // no BSP-driven look-ins in these composition fixtures
|
||||
}
|
||||
|
||||
private static WalkPolygon Quad(float z, bool facingViewer = true) => new()
|
||||
{
|
||||
Vertices =
|
||||
[
|
||||
new Vector3(-0.5f, -0.5f, z), new Vector3(0.5f, -0.5f, z),
|
||||
new Vector3(0.5f, 0.5f, z), new Vector3(-0.5f, 0.5f, z),
|
||||
],
|
||||
Plane = new WalkPlane(new Vector3(0, 0, facingViewer ? 1f : -1f), facingViewer ? -z : z),
|
||||
};
|
||||
|
||||
private static WalkLandscape Landscape3x3()
|
||||
{
|
||||
var landscape = new WalkLandscape
|
||||
{
|
||||
MidWidth = 3,
|
||||
Blocks = new WalkLandBlock?[9],
|
||||
ViewerBlockX = 1,
|
||||
ViewerBlockY = 1,
|
||||
ViewerCellX = 0,
|
||||
ViewerCellY = 0,
|
||||
};
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
landscape.Blocks[i] = new WalkLandBlock { MaxZ = 10f, MinZ = 0f };
|
||||
landscape.Blocks[i]!.EnsureCellArrays();
|
||||
}
|
||||
return landscape;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outdoor_frame_emits_landscape_then_buildings_far_to_near()
|
||||
{
|
||||
var ctx = new TestContext();
|
||||
WalkLandscape landscape = Landscape3x3();
|
||||
// A building in ring-1 block (0,0), and two in the viewer block:
|
||||
// one at the block's far corner, one at the viewer's closest cell.
|
||||
var ringBuilding = new WalkBuilding { PositionCellId = 0xAAAA0001 };
|
||||
var farBuilding = new WalkBuilding { PositionCellId = 0xBBBB0002 };
|
||||
var nearBuilding = new WalkBuilding { PositionCellId = 0xCCCC0003 };
|
||||
landscape.Blocks[0]!.CellBuildings[0] = ringBuilding; // block (0,0)
|
||||
WalkLandBlock viewerBlock = landscape.Blocks[1 * 3 + 1]!;
|
||||
viewerBlock.CellBuildings[7 * 8 + 7] = farBuilding; // far corner cell
|
||||
viewerBlock.CellBuildings[0] = nearBuilding; // the closest cell (viewer at 0,0)
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
|
||||
// ViewCount == 0 exercises the CY-only visibility arm deterministically.
|
||||
walk.DrawLandscape(landscape, new WalkPortalView(), ctx, recorder);
|
||||
|
||||
Assert.Equal(
|
||||
"LS|BLD:aaaa0001|BLD:bbbb0002|BLD:cccc0003",
|
||||
recorder.Signature());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Degraded_building_still_emits_its_entry_event()
|
||||
{
|
||||
var ctx = new TestContext();
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
var building = new WalkBuilding { PositionCellId = 0xF518002E, HasGeometry = false };
|
||||
|
||||
walk.DrawBuilding(building, new WalkPortalView(), ctx, recorder);
|
||||
|
||||
Assert.Equal("BLD:f518002e", recorder.Signature());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Interior_frame_without_exit_views_skips_the_landscape()
|
||||
{
|
||||
var ctx = new TestContext();
|
||||
// One facing portal only: the flood stays in the cell, no exit view.
|
||||
var cell = new WalkCell
|
||||
{
|
||||
CellId = 0xA9B40178,
|
||||
Portals = [new WalkCellPortal
|
||||
{
|
||||
OtherCellId = 0xA9B40179, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
|
||||
}],
|
||||
PortalPolygons = [Quad(-2f)],
|
||||
};
|
||||
ctx.Cells[cell.CellId] = cell;
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
|
||||
walk.WalkFrame(cell.CellId, cell, Landscape3x3(), ctx, recorder);
|
||||
|
||||
Assert.Equal("DI:a9b40178|DC:ov=0:a9b40178", recorder.Signature());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Interior_frame_with_an_exit_view_draws_the_landscape_through_it()
|
||||
{
|
||||
var ctx = new TestContext();
|
||||
var cell = new WalkCell
|
||||
{
|
||||
CellId = 0xA9B40150,
|
||||
Portals = [new WalkCellPortal
|
||||
{
|
||||
OtherCellId = 0xFFFFFFFF, PolygonIndex = 0, PortalSide = 0, OtherPortalId = -1,
|
||||
}],
|
||||
PortalPolygons = [Quad(-2f)],
|
||||
};
|
||||
ctx.Cells[cell.CellId] = cell;
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
var landscape = new WalkLandscape
|
||||
{
|
||||
MidWidth = 1,
|
||||
Blocks = new WalkLandBlock?[1],
|
||||
ViewerBlockX = 0,
|
||||
ViewerBlockY = 0,
|
||||
};
|
||||
|
||||
walk.WalkFrame(cell.CellId, cell, landscape, ctx, recorder);
|
||||
|
||||
Assert.Equal("DI:a9b40150|DC:ov=1:a9b40150|LS", recorder.Signature());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Outdoor_camera_cell_roots_the_landscape_walk()
|
||||
{
|
||||
var ctx = new TestContext();
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
|
||||
// Low word < 0x100 = an outdoor landcell id.
|
||||
walk.WalkFrame(0xA9B40015, null, Landscape3x3(), ctx, recorder);
|
||||
|
||||
Assert.StartsWith("LS", recorder.Signature());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void View_state_unwinds_completely_after_an_interior_frame()
|
||||
{
|
||||
var ctx = new TestContext();
|
||||
var stabCell = new WalkCell { CellId = 0xA9B40151 };
|
||||
var cell = new WalkCell
|
||||
{
|
||||
CellId = 0xA9B40150,
|
||||
StabList = [0xA9B40151u],
|
||||
Portals = [new WalkCellPortal
|
||||
{
|
||||
OtherCellId = 0xA9B40151, PolygonIndex = 0, PortalSide = 0, OtherPortalId = 0,
|
||||
}],
|
||||
PortalPolygons = [Quad(-2f)],
|
||||
};
|
||||
stabCell.Portals = [new WalkCellPortal
|
||||
{
|
||||
OtherCellId = 0xA9B40150, PolygonIndex = 0, PortalSide = 1, OtherPortalId = 0,
|
||||
}];
|
||||
stabCell.PortalPolygons = [Quad(-2f)];
|
||||
ctx.Cells[cell.CellId] = cell;
|
||||
ctx.Cells[stabCell.CellId] = stabCell;
|
||||
var walk = new RetailFrameWalk();
|
||||
|
||||
walk.WalkFrame(cell.CellId, cell, Landscape3x3(), ctx, new Recorder());
|
||||
|
||||
Assert.Equal(0, cell.NumView);
|
||||
Assert.Equal(0, stabCell.NumView);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue