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
194
src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs
Normal file
194
src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Walk;
|
||||||
|
|
||||||
|
/// <summary>The composition context: the cell/building halves plus the
|
||||||
|
/// frame-level state retail keeps in globals (the CY near plane from
|
||||||
|
/// <c>Render::update_viewpoint</c>, and the active-view installation the
|
||||||
|
/// building clip consumes).</summary>
|
||||||
|
public interface IRetailFrameWalkContext : IWalkBuildingFrameContext
|
||||||
|
{
|
||||||
|
WalkPlane CyPlane { get; }
|
||||||
|
|
||||||
|
/// <summary><c>Render::set_view</c> at the frame level: install view
|
||||||
|
/// <paramref name="index"/> of <paramref name="views"/> as the active
|
||||||
|
/// clip context for subsequent building-polygon clips.</summary>
|
||||||
|
void SetActiveView(WalkPortalView views, int index);
|
||||||
|
|
||||||
|
float ViewportWidth { get; }
|
||||||
|
float ViewportHeight { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign FW1 — the frame walk root, composing the ported machinery into
|
||||||
|
/// retail's frame shapes (model:
|
||||||
|
/// docs/research/2026-08-30-fw-walk-pseudocode.md §1, §3-§5; oracle:
|
||||||
|
/// docs/research/2026-08-30-fw-walk-oracle/README.md):
|
||||||
|
///
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Rooting (<c>SmartBox::RenderNormalMode</c> @0x00453aa0): the
|
||||||
|
/// CAMERA cell's low word < 0x100 → outdoor root (default full-screen
|
||||||
|
/// view + the landscape walk); otherwise → <c>DrawInside</c>.</item>
|
||||||
|
/// <item>Outdoor (<c>LScape::draw</c> @0x00506330): visibility per view,
|
||||||
|
/// blocks far-to-near, per-block cells far-to-near, buildings at their
|
||||||
|
/// cell's turn with the two-pass portal machinery (punches + look-ins on
|
||||||
|
/// the OUTDOOR pview — the traces' <c>pv=009d4b08 ov=0</c>).</item>
|
||||||
|
/// <item>Interior (<c>PView::DrawInside</c> @0x005a5860 +
|
||||||
|
/// <c>DrawCells</c> @0x005a4840): the flood on the INTERIOR pview
|
||||||
|
/// (<c>pv=009d4a80</c>), then the landscape drawn THROUGH the exit views
|
||||||
|
/// when any survive (<c>ov>0</c>).</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// Two PView instances exist exactly as the live traces showed.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RetailFrameWalk
|
||||||
|
{
|
||||||
|
private readonly WalkPView _interiorPView = new();
|
||||||
|
private readonly WalkPView _outdoorPView = new();
|
||||||
|
private readonly WalkPortalView _defaultView = new();
|
||||||
|
|
||||||
|
public WalkPView InteriorPView => _interiorPView;
|
||||||
|
public WalkPView OutdoorPView => _outdoorPView;
|
||||||
|
|
||||||
|
/// <summary>The per-frame root (<c>SmartBox::RenderNormalMode</c>).
|
||||||
|
/// <paramref name="cameraCell"/> may be null only when the camera is
|
||||||
|
/// outdoors.</summary>
|
||||||
|
public void WalkFrame(
|
||||||
|
uint cameraCellId, WalkCell? cameraCell, WalkLandscape landscape,
|
||||||
|
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||||
|
{
|
||||||
|
if ((cameraCellId & 0xFFFF) < 0x100)
|
||||||
|
{
|
||||||
|
// Render::set_default_view @0x0054ef50: the full-screen quad view.
|
||||||
|
_defaultView.ResetForPush();
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
_defaultView, ctx.Rays, ctx.WorldViewpoint,
|
||||||
|
ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
DrawLandscape(landscape, _defaultView, ctx, sink);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DrawInside(
|
||||||
|
cameraCell ?? throw new ArgumentNullException(nameof(cameraCell)),
|
||||||
|
landscape, ctx, sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary><c>PView::DrawInside</c> + the event half of
|
||||||
|
/// <c>DrawCells</c>. The geometry/object passes emit no walk events.</summary>
|
||||||
|
public void DrawInside(
|
||||||
|
WalkCell cell, WalkLandscape landscape,
|
||||||
|
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||||
|
{
|
||||||
|
sink.Emit(WalkEvent.DrawInside(cell.CellId));
|
||||||
|
cell.PushView();
|
||||||
|
AddViews(cell.StabList, ctx);
|
||||||
|
WalkCopyView.AppendFullViewportQuad(
|
||||||
|
cell.TopView, ctx.Rays, ctx.WorldViewpoint,
|
||||||
|
ctx.ViewportWidth, ctx.ViewportHeight);
|
||||||
|
_interiorPView.ConstructView(cell, 0xFFFF, ctx.CellContext);
|
||||||
|
|
||||||
|
EmitDrawCells(_interiorPView, sink);
|
||||||
|
if (_interiorPView.OutsideView.ViewCount > 0)
|
||||||
|
DrawLandscape(landscape, _interiorPView.OutsideView, ctx, sink);
|
||||||
|
|
||||||
|
RemoveViews(cell.StabList, ctx);
|
||||||
|
cell.PopView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary><c>LScape::draw</c>: visibility per active view, then blocks
|
||||||
|
/// far-to-near, cells far-to-near, buildings at their cell's turn.</summary>
|
||||||
|
public void DrawLandscape(
|
||||||
|
WalkLandscape landscape, WalkPortalView activeViews,
|
||||||
|
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||||
|
{
|
||||||
|
sink.Emit(WalkEvent.Landscape());
|
||||||
|
landscape.CalcDrawOrder();
|
||||||
|
landscape.CheckBlocks(ctx.CyPlane, activeViews);
|
||||||
|
|
||||||
|
for (int i = landscape.BlockDrawCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
WalkLandBlock? block = landscape.Blocks[landscape.BlockDrawList[i]];
|
||||||
|
if (block is null || block.InView == WalkBoundingType.Outside)
|
||||||
|
continue;
|
||||||
|
int cellCount = block.SideCellCount * block.SideCellCount;
|
||||||
|
for (int k = 0; k < cellCount; k++)
|
||||||
|
{
|
||||||
|
int cellIndex = block.DrawArray[k];
|
||||||
|
// DrawSortCell runs for in-view cells (alwaysDrawObjects
|
||||||
|
// default 0); terrain (DrawLandCell) emits no walk event.
|
||||||
|
if (block.CellInView[cellIndex] == WalkBoundingType.Outside)
|
||||||
|
continue;
|
||||||
|
if (block.CellBuildings[cellIndex] is WalkBuilding building)
|
||||||
|
DrawBuilding(building, activeViews, ctx, sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary><c>RenderDeviceD3D::DrawBuilding</c> @0x0059f2a0 at the walk
|
||||||
|
/// level: the BLD event fires at ENTRY (before the degrade check); the
|
||||||
|
/// portal pass walks the drawing BSP twice per active view (punch, then
|
||||||
|
/// look-in + DrawCells on the outdoor pview); the shell pass emits no
|
||||||
|
/// walk event.</summary>
|
||||||
|
public void DrawBuilding(
|
||||||
|
WalkBuilding building, WalkPortalView activeViews,
|
||||||
|
IRetailFrameWalkContext ctx, IWalkEventSink sink)
|
||||||
|
{
|
||||||
|
sink.Emit(WalkEvent.Building(building.PositionCellId));
|
||||||
|
if (!building.HasGeometry) return;
|
||||||
|
|
||||||
|
int viewCount = Math.Max(activeViews.ViewCount, 0);
|
||||||
|
var passSink = new PortalPassSink(sink);
|
||||||
|
Vector3 viewpoint = ctx.ViewpointInBuilding(building);
|
||||||
|
for (int v = 0; v < viewCount; v++)
|
||||||
|
{
|
||||||
|
ctx.SetActiveView(activeViews, v);
|
||||||
|
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||||
|
building.DrawingBsp, 1, viewpoint,
|
||||||
|
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||||
|
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||||
|
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||||
|
building.DrawingBsp, 2, viewpoint,
|
||||||
|
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||||
|
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitDrawCells(WalkPView pview, IWalkEventSink sink)
|
||||||
|
{
|
||||||
|
uint[] cells = new uint[pview.CellDrawList.Count];
|
||||||
|
for (int i = 0; i < cells.Length; i++)
|
||||||
|
cells[i] = pview.CellDrawList[i].CellId;
|
||||||
|
sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddViews(uint[] stabList, IRetailFrameWalkContext ctx)
|
||||||
|
{
|
||||||
|
foreach (uint id in stabList)
|
||||||
|
ctx.GetVisible(id)?.PushView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveViews(uint[] stabList, IRetailFrameWalkContext ctx)
|
||||||
|
{
|
||||||
|
foreach (uint id in stabList)
|
||||||
|
ctx.GetVisible(id)?.PopView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PortalPassSink(IWalkEventSink sink)
|
||||||
|
: WalkBuildingPortals.IWalkPortalPassSink
|
||||||
|
{
|
||||||
|
public void OnPunch(WalkPolygon polygon)
|
||||||
|
{
|
||||||
|
// The far-Z punch is a depth-only GPU submission (FW2's ordered
|
||||||
|
// stream); the oracle traces do not log it.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDrawCells(WalkPView pview)
|
||||||
|
{
|
||||||
|
uint[] cells = new uint[pview.CellDrawList.Count];
|
||||||
|
for (int i = 0; i < cells.Length; i++)
|
||||||
|
cells[i] = pview.CellDrawList[i].CellId;
|
||||||
|
sink.Emit(WalkEvent.DrawCells(pview.OutsideView.ViewCount, cells));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
src/AcDream.App/Rendering/Walk/WalkLandscape.cs
Normal file
220
src/AcDream.App/Rendering/Walk/WalkLandscape.cs
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
namespace AcDream.App.Rendering.Walk;
|
||||||
|
|
||||||
|
/// <summary>One landblock as the walk sees it (retail <c>CLandBlock</c>):
|
||||||
|
/// static shape plus the per-frame visibility the landscape pass writes.</summary>
|
||||||
|
public sealed class WalkLandBlock
|
||||||
|
{
|
||||||
|
public int SideCellCount = 8;
|
||||||
|
public float MaxZ;
|
||||||
|
public float MinZ;
|
||||||
|
|
||||||
|
/// <summary>Per cell index (x * side + y): the building the cell's
|
||||||
|
/// CSortCell owns, if any.</summary>
|
||||||
|
public WalkBuilding?[] CellBuildings = [];
|
||||||
|
|
||||||
|
// ---- per-frame visibility (draw_check_blocks / landcell_check) ----
|
||||||
|
public WalkBoundingType InView;
|
||||||
|
public WalkBoundingType[] CellInView = [];
|
||||||
|
|
||||||
|
// ---- per-block cell draw order cache (calc_sq_draw_order) ----
|
||||||
|
public int[] DrawArray = [];
|
||||||
|
public int ClosestX = -1;
|
||||||
|
public int ClosestY = -1;
|
||||||
|
|
||||||
|
public void EnsureCellArrays()
|
||||||
|
{
|
||||||
|
int n = SideCellCount * SideCellCount;
|
||||||
|
if (CellInView.Length < n) CellInView = new WalkBoundingType[n];
|
||||||
|
if (DrawArray.Length < n) DrawArray = new int[n];
|
||||||
|
if (CellBuildings.Length < n) CellBuildings = new WalkBuilding?[n];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The viewer-centered block grid (retail <c>LScape</c>): mid_width
|
||||||
|
/// × mid_width blocks (retail 11×11), the near-to-far block draw list, and
|
||||||
|
/// the viewer's grid offsets (<c>calc_draw_order</c>).</summary>
|
||||||
|
public sealed class WalkLandscape
|
||||||
|
{
|
||||||
|
public const float BlockLength = 192f; // 24 m cell × 8 (BN elided this to '0f')
|
||||||
|
public const float CellLength = 24f;
|
||||||
|
|
||||||
|
public int MidWidth = 11;
|
||||||
|
public WalkLandBlock?[] Blocks = []; // [x * MidWidth + y]
|
||||||
|
public int ViewerBlockX;
|
||||||
|
public int ViewerBlockY;
|
||||||
|
public int ViewerCellX; // viewer cell & 7 per axis (SqCoord)
|
||||||
|
public int ViewerCellY;
|
||||||
|
|
||||||
|
public int[] BlockDrawList = [];
|
||||||
|
public int BlockDrawCount;
|
||||||
|
|
||||||
|
public WalkLandBlock? BlockAt(int gridX, int gridY)
|
||||||
|
=> gridX >= 0 && gridX < MidWidth && gridY >= 0 && gridY < MidWidth
|
||||||
|
? Blocks[gridX * MidWidth + gridY]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
/// <summary><c>LScape::get_block_order</c> + per-block
|
||||||
|
/// <c>calc_draw_order</c>: rebuild the near-to-far block list and each
|
||||||
|
/// block's far-to-near cell order for the current viewer placement.</summary>
|
||||||
|
public void CalcDrawOrder()
|
||||||
|
{
|
||||||
|
if (BlockDrawList.Length < MidWidth * MidWidth)
|
||||||
|
BlockDrawList = new int[MidWidth * MidWidth];
|
||||||
|
BlockDrawCount = LandWalkOrder.GetBlockOrder(
|
||||||
|
ViewerBlockX, ViewerBlockY, MidWidth, BlockDrawList);
|
||||||
|
|
||||||
|
for (int x = 0; x < MidWidth; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < MidWidth; y++)
|
||||||
|
{
|
||||||
|
WalkLandBlock? block = Blocks[x * MidWidth + y];
|
||||||
|
if (block is null) continue;
|
||||||
|
block.EnsureCellArrays();
|
||||||
|
LandDirection dir = LandWalkOrder.GetDirection(
|
||||||
|
x - ViewerBlockX, y - ViewerBlockY);
|
||||||
|
(int cx, int cy) = LandWalkOrder.ClosestCell(
|
||||||
|
dir, ViewerCellX, ViewerCellY, block.SideCellCount);
|
||||||
|
// Retail's early-out: same closest cell keeps the stale order
|
||||||
|
// (the dir field is NOT compared — keep the quirk).
|
||||||
|
if (cx == block.ClosestX && cy == block.ClosestY) continue;
|
||||||
|
block.ClosestX = cx;
|
||||||
|
block.ClosestY = cy;
|
||||||
|
LandWalkOrder.FillCellOrderFarToNear(
|
||||||
|
cx, cy, block.SideCellCount,
|
||||||
|
block.DrawArray.AsSpan(0, block.SideCellCount * block.SideCellCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>LScape::draw_check_blocks</c> @0x00505f80 + <c>landcell_check</c>
|
||||||
|
/// @0x005050a0: clear all visibility, then for EACH view of the active
|
||||||
|
/// portal view union block/cell visibility from clip-height interval
|
||||||
|
/// grids at 192 m block / 24 m cell pitch in viewer-block-relative
|
||||||
|
/// coordinates. A block/cell marked by an earlier view is never
|
||||||
|
/// downgraded (blocks are only written non-Outside; cells are skipped
|
||||||
|
/// once in view).
|
||||||
|
/// </summary>
|
||||||
|
public void CheckBlocks(in WalkPlane cyPlane, WalkPortalView activeViews)
|
||||||
|
{
|
||||||
|
foreach (WalkLandBlock? block in Blocks)
|
||||||
|
{
|
||||||
|
if (block is null) continue;
|
||||||
|
block.EnsureCellArrays();
|
||||||
|
block.InView = WalkBoundingType.Outside;
|
||||||
|
Array.Clear(block.CellInView, 0, block.SideCellCount * block.SideCellCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
int viewCount = activeViews.ViewCount;
|
||||||
|
Span<float> boundsScratch = stackalloc float[32];
|
||||||
|
// Corner interval grids: 2 rolling rows of (MidWidth+1) corners,
|
||||||
|
// each corner a plane-classification vector.
|
||||||
|
int cornerRow = MidWidth + 1;
|
||||||
|
var intervals = new float[2 * cornerRow][];
|
||||||
|
for (int i = 0; i < intervals.Length; i++) intervals[i] = new float[32];
|
||||||
|
|
||||||
|
int v = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
WalkPlane[] edgePlanes;
|
||||||
|
int edgeCount;
|
||||||
|
bool last;
|
||||||
|
if (viewCount == 0)
|
||||||
|
{
|
||||||
|
edgePlanes = [];
|
||||||
|
edgeCount = 0;
|
||||||
|
last = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WalkViewPoly poly = activeViews.View.Polys[v];
|
||||||
|
edgePlanes = new WalkPlane[poly.VertexCount];
|
||||||
|
for (int k = 0; k < poly.VertexCount; k++)
|
||||||
|
edgePlanes[k] = activeViews.View.Vertices[poly.VertexIndex + k].Plane;
|
||||||
|
edgeCount = poly.VertexCount;
|
||||||
|
v++;
|
||||||
|
last = v == viewCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed the west column (grid x = 0) into parity row 0.
|
||||||
|
for (int j = 0; j <= MidWidth; j++)
|
||||||
|
WalkVisibilityMath.FillClipHeights(
|
||||||
|
(0 - ViewerBlockX) * BlockLength,
|
||||||
|
(j - ViewerBlockY) * BlockLength,
|
||||||
|
cyPlane, edgePlanes, intervals[j]);
|
||||||
|
for (int bx = 0; bx < MidWidth; bx++)
|
||||||
|
{
|
||||||
|
int westRow = (bx & 1) * cornerRow;
|
||||||
|
int eastRow = ((bx - 1) & 1) * cornerRow;
|
||||||
|
for (int j = 0; j <= MidWidth; j++)
|
||||||
|
WalkVisibilityMath.FillClipHeights(
|
||||||
|
(bx + 1 - ViewerBlockX) * BlockLength,
|
||||||
|
(j - ViewerBlockY) * BlockLength,
|
||||||
|
cyPlane, edgePlanes, intervals[eastRow + j]);
|
||||||
|
for (int by = 0; by < MidWidth; by++)
|
||||||
|
{
|
||||||
|
WalkLandBlock? block = Blocks[bx * MidWidth + by];
|
||||||
|
if (block is null) continue;
|
||||||
|
WalkBoundingType bt = WalkVisibilityMath.BlockCheck(
|
||||||
|
intervals[westRow + by], intervals[westRow + by + 1],
|
||||||
|
intervals[eastRow + by], intervals[eastRow + by + 1],
|
||||||
|
edgeCount, block.MaxZ, block.MinZ);
|
||||||
|
if (bt != WalkBoundingType.Outside)
|
||||||
|
{
|
||||||
|
block.InView = bt;
|
||||||
|
LandCellCheck(block, bx, by, cyPlane, edgePlanes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (last) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// landcell_check @0x005050a0: non-8×8 far blocks mark all cells PARTIAL;
|
||||||
|
// ENTIRELY_INSIDE blocks mark all cells 2; else per-cell corner grids at
|
||||||
|
// 24 m pitch, skipping cells already in view (union across views).
|
||||||
|
private void LandCellCheck(
|
||||||
|
WalkLandBlock block, int bx, int by,
|
||||||
|
in WalkPlane cyPlane, WalkPlane[] edgePlanes)
|
||||||
|
{
|
||||||
|
int n = block.SideCellCount;
|
||||||
|
if (n != 8)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < n * n; i++)
|
||||||
|
block.CellInView[i] = WalkBoundingType.PartiallyInside;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (block.InView == WalkBoundingType.EntirelyInside)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < n * n; i++)
|
||||||
|
block.CellInView[i] = WalkBoundingType.EntirelyInside;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float x0 = (bx - ViewerBlockX) * BlockLength;
|
||||||
|
float y0 = (by - ViewerBlockY) * BlockLength;
|
||||||
|
int cornerRow = n + 1;
|
||||||
|
var grid = new float[2 * cornerRow][];
|
||||||
|
for (int i = 0; i < grid.Length; i++) grid[i] = new float[32];
|
||||||
|
for (int j = 0; j <= n; j++)
|
||||||
|
WalkVisibilityMath.FillClipHeights(
|
||||||
|
x0, j * CellLength + y0, cyPlane, edgePlanes, grid[j]);
|
||||||
|
for (int cx = 0; cx < n; cx++)
|
||||||
|
{
|
||||||
|
int westRow = (cx & 1) * cornerRow;
|
||||||
|
int eastRow = ((cx - 1) & 1) * cornerRow;
|
||||||
|
for (int j = 0; j <= n; j++)
|
||||||
|
WalkVisibilityMath.FillClipHeights(
|
||||||
|
(cx + 1) * CellLength + x0, j * CellLength + y0,
|
||||||
|
cyPlane, edgePlanes, grid[eastRow + j]);
|
||||||
|
for (int cy = 0; cy < n; cy++)
|
||||||
|
{
|
||||||
|
if (block.CellInView[n * cx + cy] != WalkBoundingType.Outside)
|
||||||
|
continue; // union across views: never downgrade
|
||||||
|
block.CellInView[n * cx + cy] = WalkVisibilityMath.BlockCheck(
|
||||||
|
grid[westRow + cy], grid[westRow + cy + 1],
|
||||||
|
grid[eastRow + cy], grid[eastRow + cy + 1],
|
||||||
|
edgePlanes.Length, block.MaxZ, block.MinZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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