204 lines
9.3 KiB
C#
204 lines
9.3 KiB
C#
using System.Numerics;
|
||
|
||
namespace AcDream.App.Rendering.Walk;
|
||
|
||
/// <summary>
|
||
/// Campaign FW3.1 — the production <see cref="IWalkFrameContext"/> /
|
||
/// <see cref="IRetailFrameWalkContext"/> / <see cref="IWalkBuildingFrameContext"/>
|
||
/// implementation: cells resolve through the committed
|
||
/// <see cref="CellVisibility"/> registry (<c>LoadedCell.Walk</c>, per the FW
|
||
/// binding rule that the walk consumes ONLY the committed registry —
|
||
/// <c>CellVisibility.TryGetCell</c>, never a synthetic/dead-code path);
|
||
/// buildings resolve through <see cref="WalkBuildingRegistry"/>; the camera
|
||
/// pose, projection, and viewport are supplied by the caller.
|
||
///
|
||
/// Deliberately NOT coupled to any concrete camera type — FW3.2 will supply
|
||
/// live values from <c>WorldCameraFrame</c>; wiring that in is additive.
|
||
/// The ray caster is a GENERIC inverse-view-projection unprojection, not the
|
||
/// capture client's exact <c>Render::xinvscale</c>/<c>tx</c>/<c>vdst</c>
|
||
/// constants the FW1 conformance harness's <c>WalkTraceReplayContext</c>
|
||
/// uses — those are FIXTURE PINS specific to the 1024×720 capture client,
|
||
/// not production values. This is safe because
|
||
/// <see cref="WalkCopyView.Append"/> only ever CROSS-PRODUCTS these rays to
|
||
/// build view-edge planes: a uniform scale or additive offset along a ray
|
||
/// cancels out of every cross product it feeds, so any two points along the
|
||
/// true eye ray (near/far unprojection) are observably equivalent to
|
||
/// retail's exact construction for this contract.
|
||
///
|
||
/// Production retains one instance per renderer and rebinds its frame-local
|
||
/// camera values through <see cref="Reset"/>. The registries and grow-only
|
||
/// active-view scratch remain renderer-lifetime owners.
|
||
/// </summary>
|
||
public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrameWalkContext
|
||
{
|
||
/// <summary><c>Render::znear</c> @0x0081ec84 / <c>set_vdst</c> @0x0054b240.</summary>
|
||
public const float ZNear = 0.1f;
|
||
|
||
private sealed class InverseViewProjectionRayCaster : IWalkRayCaster
|
||
{
|
||
private Matrix4x4 _inverseViewProjection;
|
||
private float _viewportWidth;
|
||
private float _viewportHeight;
|
||
|
||
public InverseViewProjectionRayCaster(
|
||
Matrix4x4 viewProjection, float viewportWidth, float viewportHeight)
|
||
=> Reset(viewProjection, viewportWidth, viewportHeight);
|
||
|
||
internal void Reset(
|
||
Matrix4x4 viewProjection, float viewportWidth, float viewportHeight)
|
||
{
|
||
if (!Matrix4x4.Invert(viewProjection, out Matrix4x4 inverseViewProjection))
|
||
{
|
||
throw new ArgumentException(
|
||
"The walk's view-projection matrix must be invertible.",
|
||
nameof(viewProjection));
|
||
}
|
||
// Publish only after validation succeeds. Matrix4x4.Invert writes
|
||
// its out value even on failure; assigning the field directly
|
||
// would silently corrupt the retained ray caster for the next
|
||
// report-and-continue frame.
|
||
_inverseViewProjection = inverseViewProjection;
|
||
_viewportWidth = viewportWidth;
|
||
_viewportHeight = viewportHeight;
|
||
}
|
||
|
||
/// <summary>Screen space: origin top-left, +Y down — matching
|
||
/// <see cref="WalkScreenClip.TransformToScreen"/> and
|
||
/// <see cref="WalkCopyView.AppendFullViewportQuad"/>'s quad
|
||
/// winding.</summary>
|
||
public Vector3 RayThrough(float screenX, float screenY)
|
||
{
|
||
float ndcX = screenX / _viewportWidth * 2f - 1f;
|
||
float ndcY = 1f - screenY / _viewportHeight * 2f;
|
||
Vector4 near = Vector4.Transform(
|
||
new Vector4(ndcX, ndcY, 0f, 1f), _inverseViewProjection);
|
||
Vector4 far = Vector4.Transform(
|
||
new Vector4(ndcX, ndcY, 1f, 1f), _inverseViewProjection);
|
||
Vector3 nearWorld = new Vector3(near.X, near.Y, near.Z) / near.W;
|
||
Vector3 farWorld = new Vector3(far.X, far.Y, far.Z) / far.W;
|
||
return farWorld - nearWorld;
|
||
}
|
||
}
|
||
|
||
private readonly CellVisibility _cells;
|
||
private readonly WalkBuildingRegistry _buildings;
|
||
private Matrix4x4 _viewProjection;
|
||
private readonly InverseViewProjectionRayCaster _rays;
|
||
|
||
private Vector2[] _activeViewVerts = new Vector2[32];
|
||
private int _activeViewVertCount;
|
||
|
||
public WalkProductionFrameContext(
|
||
CellVisibility cells,
|
||
WalkBuildingRegistry buildings,
|
||
Vector3 worldViewpoint,
|
||
Vector3 forward,
|
||
Matrix4x4 viewProjection,
|
||
float viewportWidth,
|
||
float viewportHeight)
|
||
{
|
||
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
|
||
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
||
_rays = new InverseViewProjectionRayCaster(
|
||
viewProjection, viewportWidth, viewportHeight);
|
||
Reset(worldViewpoint, forward, viewProjection, viewportWidth, viewportHeight);
|
||
}
|
||
|
||
/// <summary>
|
||
/// FW6 allocation closeout: rebind this retained context to one frame's
|
||
/// camera values while preserving its active-view scratch. The cell and
|
||
/// building registries are lifetime owners and therefore never change.
|
||
/// </summary>
|
||
internal void Reset(
|
||
Vector3 worldViewpoint,
|
||
Vector3 forward,
|
||
Matrix4x4 viewProjection,
|
||
float viewportWidth,
|
||
float viewportHeight)
|
||
{
|
||
// Validate/invert before publishing any new frame value so a bad
|
||
// camera matrix leaves the previous usable binding intact.
|
||
_rays.Reset(viewProjection, viewportWidth, viewportHeight);
|
||
WorldViewpoint = worldViewpoint;
|
||
_viewProjection = viewProjection;
|
||
ViewportWidth = viewportWidth;
|
||
ViewportHeight = viewportHeight;
|
||
_activeViewVertCount = 0;
|
||
// The retail CY near plane: N = forward, d = -dot(eye, forward) - znear.
|
||
CyPlane = new WalkPlane(forward, -Vector3.Dot(worldViewpoint, forward) - ZNear);
|
||
}
|
||
|
||
public Vector3 WorldViewpoint { get; private set; }
|
||
public float ViewportWidth { get; private set; }
|
||
public float ViewportHeight { get; private set; }
|
||
public WalkPlane CyPlane { get; private set; }
|
||
public IWalkRayCaster Rays => _rays;
|
||
public IWalkFrameContext CellContext => this;
|
||
|
||
public Vector3 ViewpointIn(WalkCell cell)
|
||
=> Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform);
|
||
|
||
public Matrix4x4 ObjectToClip(WalkCell cell) => cell.WorldTransform * _viewProjection;
|
||
|
||
/// <summary>Resolves through the committed <see cref="CellVisibility"/>
|
||
/// registry only — a missing/uncommitted cell returns null (retail's
|
||
/// portal-skip behavior; the FW binding rule requires any such miss to
|
||
/// be diagnostically counted under a flag rather than silently swallowed
|
||
/// once a caller wires one up — this seam does not itself log, matching
|
||
/// <see cref="IWalkFrameContext.GetVisible"/>'s documented contract).</summary>
|
||
public WalkCell? GetVisible(uint cellId)
|
||
=> _cells.TryGetCell(cellId, out LoadedCell? cell) ? cell?.Walk : null;
|
||
|
||
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, GetEntry(building).InverseWorldTransform);
|
||
|
||
/// <summary><c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030: the
|
||
/// distance to the part's SCALED sort center, not the position origin.</summary>
|
||
public float ViewerDistanceTo(WalkBuilding building)
|
||
{
|
||
WalkBuildingFactory.Entry entry = GetEntry(building);
|
||
return Vector3.Distance(
|
||
WorldViewpoint, Vector3.Transform(building.SortCenter, entry.WorldTransform));
|
||
}
|
||
|
||
public int ClipBuildingPolygon(
|
||
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
|
||
{
|
||
Matrix4x4 objectToClip = GetEntry(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);
|
||
}
|
||
|
||
private WalkBuildingFactory.Entry GetEntry(WalkBuilding building)
|
||
{
|
||
if (!_buildings.TryGetEntry(building, out var entry))
|
||
{
|
||
// Fail loud (the PV3 post-mortem rule): a building the walk is
|
||
// actively placing MUST be committed in the same registry the
|
||
// walk was handed — a miss here is a walk/registry desync, never
|
||
// a silently-skipped building.
|
||
throw new InvalidOperationException(
|
||
"WalkProductionFrameContext was asked to place a WalkBuilding " +
|
||
"that is not committed in its WalkBuildingRegistry.");
|
||
}
|
||
return entry;
|
||
}
|
||
}
|