acdream/src/AcDream.App/Rendering/Walk/WalkProductionFrameContext.cs

204 lines
9.3 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;
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;
}
}