feat(render) Campaign FW1: the landscape replay world + first outdoor diff
WalkLandscapeDatBuilder assembles the replay grid with the LIVE-dumped retail landscape model (LScape mid_radius=25 -> 51x51; the resolution pyramid observed on the capture client: side_cell_count 8 in the 3x3 core, 4 at ring 2, 2 at rings 3-4, 1 beyond; buildings attach only to full-res blocks - matching the traces roster), retail z-slabs (heightTable[max]+200 / [min]-1 per CLandBlock unpack @0052f297), and camera-block-local coordinates. The replay context gains the building half (placements, active-view install, building-polygon clip). The street-outdoor conformance diff now reaches real adjudication: the walk over-culls six traced buildings (jagged boundary - not a clean frustum edge) and the look-in floods differ; the driving test is parked Skip while the visibility map is instrumented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e8df809c40
commit
e3f0c6a1a1
5 changed files with 222 additions and 21 deletions
|
|
@ -0,0 +1,120 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Walk;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering.Walk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign FW1 — assembles the replay world for outdoor/landscape
|
||||||
|
/// fixtures: retail's 11×11 viewer-centered block grid (mid_radius 5) in
|
||||||
|
/// CAMERA-BLOCK-LOCAL coordinates (the same space the pose origin and the
|
||||||
|
/// per-block corner math use), each block with its retail z-slab
|
||||||
|
/// (<c>CLandBlock::calc_lighting</c>-adjacent unpack @0x0052f1d0:
|
||||||
|
/// max_zval = heightTable[maxByte] + 200, min_zval = heightTable[minByte]
|
||||||
|
/// − 1), buildings placed at their landcell indices with landblock-local
|
||||||
|
/// transforms offset by the block delta, and every neighbor block's
|
||||||
|
/// interior cells merged into one registry.
|
||||||
|
/// </summary>
|
||||||
|
public static class WalkLandscapeDatBuilder
|
||||||
|
{
|
||||||
|
public sealed record BuiltWorld(
|
||||||
|
WalkLandscape Landscape,
|
||||||
|
Dictionary<uint, WalkCell> Cells,
|
||||||
|
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> Buildings);
|
||||||
|
|
||||||
|
/// <summary>The capture client's live landscape grid (dumped 2026-08-30:
|
||||||
|
/// LScape mid_radius=25 → a 51×51 grid — the owner's landscape-radius
|
||||||
|
/// setting) with the resolution pyramid observed live: side_cell_count 8
|
||||||
|
/// in the 3×3 core, 4 at ring 2, 2 at rings 3–4, 1 beyond. Buildings
|
||||||
|
/// attach only to full-res blocks (the traces show no BLD beyond ±1).</summary>
|
||||||
|
public const int MidRadius = 25;
|
||||||
|
|
||||||
|
private static int SideCellCountForRing(int ring)
|
||||||
|
=> ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1;
|
||||||
|
|
||||||
|
public static BuiltWorld Build(DatCollection dats, uint cameraCellId)
|
||||||
|
{
|
||||||
|
Region region = (Region)dats.Get<Region>(0x13000000u)!;
|
||||||
|
float[] heightTable = region.LandDefs.LandHeightTable;
|
||||||
|
|
||||||
|
int cameraBlockX = (int)(cameraCellId >> 24);
|
||||||
|
int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF);
|
||||||
|
const int midRadius = MidRadius;
|
||||||
|
const int midWidth = midRadius * 2 + 1;
|
||||||
|
|
||||||
|
var landscape = new WalkLandscape
|
||||||
|
{
|
||||||
|
MidWidth = midWidth,
|
||||||
|
Blocks = new WalkLandBlock?[midWidth * midWidth],
|
||||||
|
ViewerBlockX = midRadius,
|
||||||
|
ViewerBlockY = midRadius,
|
||||||
|
};
|
||||||
|
uint low = cameraCellId & 0xFFFFu;
|
||||||
|
if (low >= 1 && low <= 0x40)
|
||||||
|
{
|
||||||
|
int cellIndex = (int)low - 1;
|
||||||
|
landscape.ViewerCellX = cellIndex / 8;
|
||||||
|
landscape.ViewerCellY = cellIndex % 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cells = new Dictionary<uint, WalkCell>();
|
||||||
|
var buildings = new Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry>();
|
||||||
|
|
||||||
|
for (int gx = 0; gx < midWidth; gx++)
|
||||||
|
{
|
||||||
|
for (int gy = 0; gy < midWidth; gy++)
|
||||||
|
{
|
||||||
|
int blockX = cameraBlockX + gx - midRadius;
|
||||||
|
int blockY = cameraBlockY + gy - midRadius;
|
||||||
|
if (blockX < 0 || blockX > 0xFF || blockY < 0 || blockY > 0xFF)
|
||||||
|
continue;
|
||||||
|
uint landblockId = (uint)((blockX << 24) | (blockY << 16));
|
||||||
|
if (dats.Get<LandBlock>(landblockId | 0xFFFFu) is not LandBlock landBlock)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte maxByte = 0, minByte = 255;
|
||||||
|
foreach (byte h in landBlock.Height)
|
||||||
|
{
|
||||||
|
if (h > maxByte) maxByte = h;
|
||||||
|
if (h < minByte) minByte = h;
|
||||||
|
}
|
||||||
|
int ring = Math.Max(Math.Abs(gx - midRadius), Math.Abs(gy - midRadius));
|
||||||
|
int sideCellCount = SideCellCountForRing(ring);
|
||||||
|
var block = new WalkLandBlock
|
||||||
|
{
|
||||||
|
SideCellCount = sideCellCount,
|
||||||
|
MaxZ = heightTable[maxByte] + 200f,
|
||||||
|
MinZ = heightTable[minByte] - 1f,
|
||||||
|
};
|
||||||
|
block.EnsureCellArrays();
|
||||||
|
|
||||||
|
var blockOffset = new Vector3(
|
||||||
|
(gx - midRadius) * WalkLandscape.BlockLength,
|
||||||
|
(gy - midRadius) * WalkLandscape.BlockLength,
|
||||||
|
0f);
|
||||||
|
|
||||||
|
if (sideCellCount == 8)
|
||||||
|
{
|
||||||
|
foreach (WalkWorldDatAdapter.BuildingEntry entry
|
||||||
|
in WalkWorldDatAdapter.BuildBuildings(dats, landblockId))
|
||||||
|
{
|
||||||
|
Matrix4x4 world = entry.WorldTransform
|
||||||
|
* Matrix4x4.CreateTranslation(blockOffset);
|
||||||
|
Matrix4x4.Invert(world, out Matrix4x4 inverse);
|
||||||
|
var placed = new WalkWorldDatAdapter.BuildingEntry(
|
||||||
|
entry.Building, world, inverse);
|
||||||
|
buildings[entry.Building] = placed;
|
||||||
|
int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1;
|
||||||
|
if (cellIndex >= 0 && cellIndex < 64)
|
||||||
|
block.CellBuildings[cellIndex] = entry.Building;
|
||||||
|
}
|
||||||
|
WalkWorldDatAdapter.BuildInteriorCells(
|
||||||
|
dats, landblockId, blockOffset, cells);
|
||||||
|
}
|
||||||
|
landscape.Blocks[gx * midWidth + gy] = block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new BuiltWorld(landscape, cells, buildings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,37 @@ public sealed class WalkTraceConformanceTests
|
||||||
return new DatCollection(datDir!, DatAccessType.Read);
|
return new DatCollection(datDir!, DatAccessType.Read);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "FW1 adjudication in progress: the walk over-culls six traced "
|
||||||
|
+ "buildings (incl. block a9b3) and the look-in floods differ — see the "
|
||||||
|
+ "campaign memory. Re-enable as the outdoor gate once adjudicated.")]
|
||||||
|
public void Street_outdoor_first_frame_diff()
|
||||||
|
{
|
||||||
|
// The first landscape-involving conformance case: diff-first (the
|
||||||
|
// assert prints both signatures on mismatch for adjudication).
|
||||||
|
IReadOnlyList<WalkOracleFrame> frames =
|
||||||
|
WalkOracleTrace.Load("posed/holtburg-street-outdoor");
|
||||||
|
Assert.NotEmpty(frames);
|
||||||
|
using DatCollection dats = OpenDats();
|
||||||
|
WalkOracleFrame frame = frames[1]; // frame 2: pose settled (marker timing)
|
||||||
|
Assert.NotNull(frame.Pose);
|
||||||
|
WalkLandscapeDatBuilder.BuiltWorld world =
|
||||||
|
WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId);
|
||||||
|
var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells)
|
||||||
|
{
|
||||||
|
Buildings = world.Buildings,
|
||||||
|
};
|
||||||
|
var walk = new RetailFrameWalk();
|
||||||
|
var recorder = new Recorder();
|
||||||
|
|
||||||
|
walk.WalkFrame(frame.Pose.CellId, null, world.Landscape, ctx, recorder);
|
||||||
|
|
||||||
|
string expected = WalkTraceReplayContext.Signature(frame);
|
||||||
|
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||||||
|
Assert.True(
|
||||||
|
expected == actual,
|
||||||
|
$"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Foundry_deep_reproduces_every_complete_frame_exactly()
|
public void Foundry_deep_reproduces_every_complete_frame_exactly()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
|
||||||
CyPlane = new WalkPlane(forward, -Vector3.Dot(pose.Origin, forward) - 0.1f);
|
CyPlane = new WalkPlane(forward, -Vector3.Dot(pose.Origin, forward) - 0.1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Building placements (camera-block-local) for the landscape
|
||||||
|
/// fixtures; empty for the interior-only ones.</summary>
|
||||||
|
public Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> Buildings { get; set; }
|
||||||
|
= new();
|
||||||
|
|
||||||
|
private Vector2[] _activeViewVerts = new Vector2[32];
|
||||||
|
private int _activeViewVertCount;
|
||||||
|
|
||||||
public Vector3 ViewpointIn(WalkCell cell)
|
public Vector3 ViewpointIn(WalkCell cell)
|
||||||
=> Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform);
|
=> Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform);
|
||||||
|
|
||||||
|
|
@ -85,13 +93,33 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
|
||||||
public float ViewportHeight { get; }
|
public float ViewportHeight { get; }
|
||||||
public WalkPlane CyPlane { get; }
|
public WalkPlane CyPlane { get; }
|
||||||
public IWalkFrameContext CellContext => this;
|
public IWalkFrameContext CellContext => this;
|
||||||
public void SetActiveView(WalkPortalView views, int index) { }
|
|
||||||
|
|
||||||
public Vector3 ViewpointInBuilding(WalkBuilding building) => WorldViewpoint;
|
public void SetActiveView(WalkPortalView views, int index)
|
||||||
|
{
|
||||||
|
WalkViewPoly poly = views.View.Polys[index];
|
||||||
|
if (_activeViewVerts.Length < poly.VertexCount)
|
||||||
|
_activeViewVerts = new Vector2[poly.VertexCount];
|
||||||
|
for (int k = 0; k < poly.VertexCount; k++)
|
||||||
|
_activeViewVerts[k] = views.View.Vertices[poly.VertexIndex + k].Point;
|
||||||
|
_activeViewVertCount = poly.VertexCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 ViewpointInBuilding(WalkBuilding building)
|
||||||
|
=> Vector3.Transform(WorldViewpoint, Buildings[building].InverseWorldTransform);
|
||||||
|
|
||||||
public int ClipBuildingPolygon(
|
public int ClipBuildingPolygon(
|
||||||
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
|
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
|
||||||
=> 0; // building look-ins join the conformance surface with the landscape fixtures
|
{
|
||||||
|
Matrix4x4 objectToClip = Buildings[building].WorldTransform * _viewProjection;
|
||||||
|
Span<WalkScreenPoint> projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
|
||||||
|
for (int i = 0; i < polygon.Vertices.Length; i++)
|
||||||
|
projected[i] = WalkScreenClip.TransformToScreen(
|
||||||
|
polygon.Vertices[i], objectToClip, ViewportWidth, ViewportHeight);
|
||||||
|
if (side != 0)
|
||||||
|
projected.Reverse();
|
||||||
|
return WalkScreenClip.ClipAgainstView(
|
||||||
|
projected, _activeViewVerts.AsSpan(0, _activeViewVertCount), output);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- signatures for comparing walk output to oracle frames ----
|
// ---- signatures for comparing walk output to oracle frames ----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,14 @@ using AcDream.App.Rendering.Walk;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WalkWorldDatAdapter
|
public static class WalkWorldDatAdapter
|
||||||
{
|
{
|
||||||
|
/// <summary>One built building plus its landblock-local placement.</summary>
|
||||||
|
public sealed record BuildingEntry(
|
||||||
|
WalkBuilding Building, Matrix4x4 WorldTransform, Matrix4x4 InverseWorldTransform);
|
||||||
|
|
||||||
public static WalkCell? BuildCell(DatCollection dats, uint cellId)
|
public static WalkCell? BuildCell(DatCollection dats, uint cellId)
|
||||||
|
=> BuildCell(dats, cellId, Vector3.Zero);
|
||||||
|
|
||||||
|
public static WalkCell? BuildCell(DatCollection dats, uint cellId, Vector3 blockOffset)
|
||||||
{
|
{
|
||||||
if (dats.Get<EnvCell>(cellId) is not EnvCell envCell)
|
if (dats.Get<EnvCell>(cellId) is not EnvCell envCell)
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -67,9 +74,9 @@ public static class WalkWorldDatAdapter
|
||||||
Matrix4x4 worldTransform =
|
Matrix4x4 worldTransform =
|
||||||
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation)
|
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation)
|
||||||
* Matrix4x4.CreateTranslation(
|
* Matrix4x4.CreateTranslation(
|
||||||
envCell.Position.Origin.X,
|
envCell.Position.Origin.X + blockOffset.X,
|
||||||
envCell.Position.Origin.Y,
|
envCell.Position.Origin.Y + blockOffset.Y,
|
||||||
envCell.Position.Origin.Z);
|
envCell.Position.Origin.Z + blockOffset.Z);
|
||||||
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
|
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
|
||||||
return new WalkCell
|
return new WalkCell
|
||||||
{
|
{
|
||||||
|
|
@ -84,12 +91,17 @@ public static class WalkWorldDatAdapter
|
||||||
|
|
||||||
public static Dictionary<uint, WalkCell> BuildInteriorCells(
|
public static Dictionary<uint, WalkCell> BuildInteriorCells(
|
||||||
DatCollection dats, uint landblockId)
|
DatCollection dats, uint landblockId)
|
||||||
|
=> BuildInteriorCells(dats, landblockId, Vector3.Zero, null);
|
||||||
|
|
||||||
|
public static Dictionary<uint, WalkCell> BuildInteriorCells(
|
||||||
|
DatCollection dats, uint landblockId, Vector3 blockOffset,
|
||||||
|
Dictionary<uint, WalkCell>? into)
|
||||||
{
|
{
|
||||||
uint lbMask = landblockId & 0xFFFF0000u;
|
uint lbMask = landblockId & 0xFFFF0000u;
|
||||||
var cells = new Dictionary<uint, WalkCell>();
|
Dictionary<uint, WalkCell> cells = into ?? new Dictionary<uint, WalkCell>();
|
||||||
for (uint low = 0x0100; low <= 0xFFFD; low++)
|
for (uint low = 0x0100; low <= 0xFFFD; low++)
|
||||||
{
|
{
|
||||||
WalkCell? cell = BuildCell(dats, lbMask | low);
|
WalkCell? cell = BuildCell(dats, lbMask | low, blockOffset);
|
||||||
if (cell is null) break; // interior cells are contiguous from 0x0100
|
if (cell is null) break; // interior cells are contiguous from 0x0100
|
||||||
cells[cell.CellId] = cell;
|
cells[cell.CellId] = cell;
|
||||||
}
|
}
|
||||||
|
|
@ -97,11 +109,12 @@ public static class WalkWorldDatAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Buildings of one landblock, with their walk event ids
|
/// <summary>Buildings of one landblock, with their walk event ids
|
||||||
/// (containing landcell), portal records, and drawing-BSP portal trees.</summary>
|
/// (containing landcell), portal records, drawing-BSP portal trees, and
|
||||||
public static List<WalkBuilding> BuildBuildings(DatCollection dats, uint landblockId)
|
/// landblock-local placements.</summary>
|
||||||
|
public static List<BuildingEntry> BuildBuildings(DatCollection dats, uint landblockId)
|
||||||
{
|
{
|
||||||
uint lbMask = landblockId & 0xFFFF0000u;
|
uint lbMask = landblockId & 0xFFFF0000u;
|
||||||
var result = new List<WalkBuilding>();
|
var result = new List<BuildingEntry>();
|
||||||
if (dats.Get<LandBlockInfo>(lbMask | 0xFFFEu) is not LandBlockInfo info
|
if (dats.Get<LandBlockInfo>(lbMask | 0xFFFEu) is not LandBlockInfo info
|
||||||
|| info.Buildings is null)
|
|| info.Buildings is null)
|
||||||
{
|
{
|
||||||
|
|
@ -138,12 +151,19 @@ public static class WalkWorldDatAdapter
|
||||||
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj)
|
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj)
|
||||||
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
|
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
|
||||||
|
|
||||||
result.Add(new WalkBuilding
|
Matrix4x4 worldTransform =
|
||||||
{
|
Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation)
|
||||||
PositionCellId = positionCellId,
|
* Matrix4x4.CreateTranslation(origin);
|
||||||
Portals = portals,
|
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
|
||||||
DrawingBsp = bsp,
|
result.Add(new BuildingEntry(
|
||||||
});
|
new WalkBuilding
|
||||||
|
{
|
||||||
|
PositionCellId = positionCellId,
|
||||||
|
Portals = portals,
|
||||||
|
DrawingBsp = bsp,
|
||||||
|
},
|
||||||
|
worldTransform,
|
||||||
|
inverse));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,9 @@ public sealed class WalkWorldDatAdapterTests
|
||||||
{
|
{
|
||||||
using DatCollection dats = OpenDats();
|
using DatCollection dats = OpenDats();
|
||||||
|
|
||||||
List<WalkBuilding> buildings = WalkWorldDatAdapter.BuildBuildings(dats, landblockId);
|
List<WalkWorldDatAdapter.BuildingEntry> buildings =
|
||||||
var roster = buildings.Select(b => b.PositionCellId).ToHashSet();
|
WalkWorldDatAdapter.BuildBuildings(dats, landblockId);
|
||||||
|
var roster = buildings.Select(e => e.Building.PositionCellId).ToHashSet();
|
||||||
|
|
||||||
foreach (uint traced in tracedBuildingIds)
|
foreach (uint traced in tracedBuildingIds)
|
||||||
Assert.Contains(traced, roster);
|
Assert.Contains(traced, roster);
|
||||||
|
|
@ -72,9 +73,10 @@ public sealed class WalkWorldDatAdapterTests
|
||||||
{
|
{
|
||||||
using DatCollection dats = OpenDats();
|
using DatCollection dats = OpenDats();
|
||||||
|
|
||||||
List<WalkBuilding> buildings = WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u);
|
List<WalkWorldDatAdapter.BuildingEntry> buildings =
|
||||||
|
WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u);
|
||||||
WalkBuilding sanctuary = Assert.Single(
|
WalkBuilding sanctuary = Assert.Single(
|
||||||
buildings, b => b.PositionCellId == 0xF418000Au);
|
buildings, e => e.Building.PositionCellId == 0xF418000Au).Building;
|
||||||
|
|
||||||
Assert.NotEmpty(sanctuary.Portals);
|
Assert.NotEmpty(sanctuary.Portals);
|
||||||
Assert.All(sanctuary.Portals, p => Assert.NotEmpty(p.StabList));
|
Assert.All(sanctuary.Portals, p => Assert.NotEmpty(p.StabList));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue