acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
Erik b3ac5872e9 feat(render) Campaign FW1: the degrade-level BSP gate kills the wrong punches
The offline degrade probe proved the mechanism: every Holtburg building
carries PORT nodes ONLY in its level-0 GfxObj (out to ~24-48 m); every
degraded level has zero. Retail walks the CURRENT degrade level BSP
(part->gfxobj[deg_level]) - that is what limits look-in punches to the
nearest buildings. WalkBuilding gains the degrade ladder +
SelectDrawingBsp (band pick; UpdateViewerDistance hysteresis is a port
TODO), the walk selects per viewer distance, the adapter builds
per-level BSPs, and the stab-list load rule (CLandBlock::init_buildings
@0052fd80: a full-res block loads exactly its buildings portal stab
cells) replaces load-everything in the landscape builder. The sweep now
shows clean rosters with all far-building punches gone; remaining
deltas: the near buildings 001a/0022 (50 m/28 m center distance vs the
48 m band edge - sphere-adjusted distance/hysteresis to port) and the
one ring-1 frustum boundary pair (aab50002/a9b3003c).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 10:51:05 +02:00

156 lines
6.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// The FW1 conformance replay harness: reconstructs the camera state from a
/// pose-stamped oracle frame and drives the ported walk over
/// adapter-built world data. Convention notes (adjudicate against the
/// fixtures, loudly, on any mismatch):
/// <list type="bullet">
/// <item>The dumped quaternion is retail Frame storage order w,x,y,z
/// (q0=w) — unit-norm verified on the captures.</item>
/// <item>Retail's frame axes: +Y forward, +Z up (the camera looks along
/// the rotated +Y).</item>
/// <item>Pose origin is landblock-local, the same space the adapter's
/// cell transforms produce.</item>
/// </list>
/// </summary>
public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalkContext
{
// Retail projection globals, dumped live from the capture client
// (recon 2026-08-30 evening: Render::bw/bh/xinvscale/yinvscale/tx/ty/vdst).
public const float RetailViewportWidth = 1024f;
public const float RetailViewportHeight = 720f;
public const float RetailXInvScale = 0.00025f;
public const float RetailYInvScale = 0.00025f;
public const float RetailTx = 0.127875f;
public const float RetailTy = 0.089875f;
public const float RetailVdst = 0.1330766976f;
private sealed class RetailRayCaster(
Vector3 right, Vector3 forward, Vector3 up) : IWalkRayCaster
{
// Retail's unproject (copy_view's ray path; equal to
// ScreenToViewTransform for these globals):
// u = sx·xinvscale tx; w = sy·yinvscale ty
// ray = Xaxis·u + Yaxis·vdst Zaxis·w
public Vector3 RayThrough(float screenX, float screenY)
{
float u = screenX * RetailXInvScale - RetailTx;
float w = screenY * RetailYInvScale - RetailTy;
return right * u + forward * RetailVdst - up * w;
}
}
private readonly Dictionary<uint, WalkCell> _cells;
private readonly Matrix4x4 _viewProjection;
private readonly IWalkRayCaster _rays;
public WalkTraceReplayContext(WalkOraclePose pose, Dictionary<uint, WalkCell> cells)
{
_cells = cells;
WorldViewpoint = pose.Origin;
var rotation = new Quaternion(pose.Q1, pose.Q2, pose.Q3, pose.Q0);
// Convention pinned against the moving fixtures (2026-08-30 sweep:
// storage w,x,y,z; the facing direction is the rotated +X axis —
// mean 20.8° vs motion headings, every alternative ≥52°). The
// consistent right-handed triple is then right = rot(+Y),
// forward = rot(+X), up = rot(+Z).
Vector3 forward = Vector3.Transform(Vector3.UnitX, rotation);
Vector3 up = Vector3.Transform(Vector3.UnitZ, rotation);
Vector3 right = -Vector3.Transform(Vector3.UnitY, rotation);
// The exact retail frustum: tan(halfFovY) = ty/vdst, aspect = tx/ty.
float fovY = 2f * MathF.Atan(RetailTy / RetailVdst);
Matrix4x4 view = Matrix4x4.CreateLookAt(pose.Origin, pose.Origin + forward, up);
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
fovY, RetailTx / RetailTy, 0.1f, 5000f);
_viewProjection = view * projection;
ViewportWidth = RetailViewportWidth;
ViewportHeight = RetailViewportHeight;
_rays = new RetailRayCaster(right, forward, up);
// The retail CY near plane: N = forward, d = dot(eye, forward) znear.
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)
=> Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform);
public Matrix4x4 ObjectToClip(WalkCell cell)
=> cell.WorldTransform * _viewProjection;
public WalkCell? GetVisible(uint cellId) => _cells.GetValueOrDefault(cellId);
public IWalkRayCaster Rays => _rays;
public Vector3 WorldViewpoint { get; }
public float ViewportWidth { get; }
public float ViewportHeight { get; }
public WalkPlane CyPlane { get; }
public IWalkFrameContext CellContext => this;
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 float ViewerDistanceTo(WalkBuilding building)
=> Vector3.Distance(
WorldViewpoint, Buildings[building].WorldTransform.Translation);
public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
{
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 ----
public static string Signature(IEnumerable<WalkEvent> events)
=> 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")))}",
_ => "?",
}));
public static string Signature(WalkOracleFrame frame)
=> string.Join("|", frame.Events.Select(e => e.Kind switch
{
WalkOracleEventKind.Landscape => "LS",
WalkOracleEventKind.Building => $"BLD:{e.CellId!.Value:x8}",
WalkOracleEventKind.DrawInside => $"DI:{e.CellId!.Value:x8}",
WalkOracleEventKind.DrawCells =>
$"DC:ov={e.OutsideViewCount}:{string.Join(',', e.Cells.Select(c => c.ToString("x8")))}",
_ => "?",
}));
}