fix(render): clip walk content to authored portal views
This commit is contained in:
parent
90eba0ec3c
commit
11e68aad82
10 changed files with 408 additions and 78 deletions
|
|
@ -209,6 +209,7 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
private readonly Action _drawExitSeals;
|
private readonly Action _drawExitSeals;
|
||||||
private readonly HashSet<uint> _singleCellScratch = new();
|
private readonly HashSet<uint> _singleCellScratch = new();
|
||||||
private readonly List<uint> _singleCellListScratch = new();
|
private readonly List<uint> _singleCellListScratch = new();
|
||||||
|
private readonly Dictionary<uint, int> _singleCellClipScratch = new(1);
|
||||||
|
|
||||||
internal WalkProductionLeafRenderer(
|
internal WalkProductionLeafRenderer(
|
||||||
RetailPViewPassExecutor passes,
|
RetailPViewPassExecutor passes,
|
||||||
|
|
@ -231,16 +232,26 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
||||||
public void DrawTerrainSlice(int sliceIndex) =>
|
public void DrawTerrainSlice(int sliceIndex) =>
|
||||||
_passes.DrawWalkTerrainSlice(_frame, _clipAssembly, sliceIndex);
|
_passes.DrawWalkTerrainSlice(_frame, _clipAssembly, sliceIndex);
|
||||||
|
|
||||||
public void DrawCellShell(uint cellId)
|
public void DrawCellShell(uint cellId, uint clipSlot)
|
||||||
{
|
{
|
||||||
_singleCellScratch.Clear();
|
_singleCellClipScratch.Clear();
|
||||||
_singleCellScratch.Add(cellId);
|
_singleCellClipScratch.Add(cellId, checked((int)clipSlot));
|
||||||
_passes.DrawOpaqueCellShells(_singleCellScratch);
|
_passes.SetCellShellClipRouting(_singleCellClipScratch);
|
||||||
if (_passes.CellHasTransparentShell(cellId))
|
try
|
||||||
{
|
{
|
||||||
_singleCellListScratch.Clear();
|
_singleCellScratch.Clear();
|
||||||
_singleCellListScratch.Add(cellId);
|
_singleCellScratch.Add(cellId);
|
||||||
_passes.DrawTransparentCellShellsOrdered(_singleCellListScratch);
|
_passes.DrawOpaqueCellShells(_singleCellScratch);
|
||||||
|
if (_passes.CellHasTransparentShell(cellId))
|
||||||
|
{
|
||||||
|
_singleCellListScratch.Clear();
|
||||||
|
_singleCellListScratch.Add(cellId);
|
||||||
|
_passes.DrawTransparentCellShellsOrdered(_singleCellListScratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_passes.SetCellShellClipRouting(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,9 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
public void DrawOpaqueCellShells(HashSet<uint> cellIds) =>
|
public void DrawOpaqueCellShells(HashSet<uint> cellIds) =>
|
||||||
_envCells.Render(WbRenderPass.Opaque, cellIds);
|
_envCells.Render(WbRenderPass.Opaque, cellIds);
|
||||||
|
|
||||||
|
public void SetCellShellClipRouting(IReadOnlyDictionary<uint, int>? routing) =>
|
||||||
|
_envCells.SetClipRouting(routing);
|
||||||
|
|
||||||
public bool CellHasTransparentShell(uint cellId) =>
|
public bool CellHasTransparentShell(uint cellId) =>
|
||||||
_envCells.CellHasTransparent(cellId);
|
_envCells.CellHasTransparent(cellId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,8 @@ internal sealed class RetailPViewRenderer
|
||||||
walkDriver = new Walk.WalkFrameDriver(
|
walkDriver = new Walk.WalkFrameDriver(
|
||||||
walkExecutor!.Dispatcher,
|
walkExecutor!.Dispatcher,
|
||||||
walkLeafRenderer,
|
walkLeafRenderer,
|
||||||
_walkWorldData);
|
_walkWorldData,
|
||||||
|
clipFrame: clipAssembly.Frame);
|
||||||
|
|
||||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ internal interface IWalkFrameLeafRenderer
|
||||||
/// second reverse loop for <c>DrawObjCellForDummies</c> @0x005a4b0d.
|
/// second reverse loop for <c>DrawObjCellForDummies</c> @0x005a4b0d.
|
||||||
/// The ordinary interior-root flood and every building look-in flood use
|
/// The ordinary interior-root flood and every building look-in flood use
|
||||||
/// this same two-pass discipline.</summary>
|
/// this same two-pass discipline.</summary>
|
||||||
void DrawCellShell(uint cellId);
|
void DrawCellShell(uint cellId, uint clipSlot);
|
||||||
|
|
||||||
/// <summary>One landscape cell's or building shell's static-owner
|
/// <summary>One landscape cell's or building shell's static-owner
|
||||||
/// particle submission, at its own walk turn — see
|
/// particle submission, at its own walk turn — see
|
||||||
|
|
@ -231,7 +231,9 @@ internal enum WalkFrameEventKind : byte
|
||||||
TerrainSlice,
|
TerrainSlice,
|
||||||
|
|
||||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
|
/// <summary><see cref="IWalkFrameLeafRenderer.DrawCellShell"/> —
|
||||||
/// <see cref="WalkFrameEvent.CellId"/> is the cell.</summary>
|
/// <see cref="WalkFrameEvent.CellId"/> is the cell and
|
||||||
|
/// <see cref="WalkFrameEvent.IntArg"/> is the exact portal-view GPU clip
|
||||||
|
/// slot for this retail <c>DrawEnvCell</c> turn.</summary>
|
||||||
CellShell,
|
CellShell,
|
||||||
|
|
||||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> —
|
/// <summary><see cref="IWalkFrameLeafRenderer.DrawPunchFan"/> —
|
||||||
|
|
@ -288,9 +290,23 @@ internal interface IWalkLookInViewSource
|
||||||
int routeIndex,
|
int routeIndex,
|
||||||
in Vector3 center,
|
in Vector3 center,
|
||||||
float radius);
|
float radius);
|
||||||
|
|
||||||
|
/// <summary>Returns the GPU clip slots for every view slice in this
|
||||||
|
/// DrawObjCell turn which admits the part's retail drawing sphere. Retail
|
||||||
|
/// runs DrawMeshInternal once per surviving portal_view slice; a boolean
|
||||||
|
/// sphere gate alone is insufficient because the mesh polygons must remain
|
||||||
|
/// clipped to that aperture.</summary>
|
||||||
|
IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
|
||||||
|
int routeIndex,
|
||||||
|
in Vector3 center,
|
||||||
|
float radius,
|
||||||
|
bool testSphere);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal readonly record struct WalkLookInSlice(int PlaneStart, int PlaneCount);
|
internal readonly record struct WalkLookInSlice(
|
||||||
|
int PlaneStart,
|
||||||
|
int PlaneCount,
|
||||||
|
uint ClipSlot);
|
||||||
|
|
||||||
internal readonly record struct WalkLookInTurn(
|
internal readonly record struct WalkLookInTurn(
|
||||||
uint CellId,
|
uint CellId,
|
||||||
|
|
@ -341,8 +357,8 @@ internal readonly struct WalkFrameEvent
|
||||||
internal static WalkFrameEvent TerrainSlice(int sliceIndex) =>
|
internal static WalkFrameEvent TerrainSlice(int sliceIndex) =>
|
||||||
new(WalkFrameEventKind.TerrainSlice, sliceIndex, 0, 0f, null);
|
new(WalkFrameEventKind.TerrainSlice, sliceIndex, 0, 0f, null);
|
||||||
|
|
||||||
internal static WalkFrameEvent CellShell(uint cellId) =>
|
internal static WalkFrameEvent CellShell(uint cellId, uint clipSlot) =>
|
||||||
new(WalkFrameEventKind.CellShell, 0, cellId, 0f, null);
|
new(WalkFrameEventKind.CellShell, checked((int)clipSlot), cellId, 0f, null);
|
||||||
|
|
||||||
internal static WalkFrameEvent PunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
internal static WalkFrameEvent PunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
||||||
new(WalkFrameEventKind.PunchFan, activeViewIndex, 0, 0f, worldPolygon);
|
new(WalkFrameEventKind.PunchFan, activeViewIndex, 0, 0f, worldPolygon);
|
||||||
|
|
@ -452,6 +468,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
private readonly IWalkFrameLeafRenderer _leafRenderer;
|
private readonly IWalkFrameLeafRenderer _leafRenderer;
|
||||||
private readonly IWalkFrameWorldData _worldData;
|
private readonly IWalkFrameWorldData _worldData;
|
||||||
private readonly IWalkFrameDriverTrace? _trace;
|
private readonly IWalkFrameDriverTrace? _trace;
|
||||||
|
private readonly ClipFrame? _clipFrame;
|
||||||
private readonly OrderedDrawStream _stream = new();
|
private readonly OrderedDrawStream _stream = new();
|
||||||
private readonly List<WalkFrameEvent> _events = new();
|
private readonly List<WalkFrameEvent> _events = new();
|
||||||
private readonly List<int> _markPositions = new();
|
private readonly List<int> _markPositions = new();
|
||||||
|
|
@ -472,6 +489,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
private readonly List<WalkLookInTurn> _lookInTurns = new();
|
private readonly List<WalkLookInTurn> _lookInTurns = new();
|
||||||
private readonly List<WalkLookInSlice> _lookInSlices = new();
|
private readonly List<WalkLookInSlice> _lookInSlices = new();
|
||||||
private readonly List<WalkPlane> _lookInPlanes = new();
|
private readonly List<WalkPlane> _lookInPlanes = new();
|
||||||
|
private readonly List<uint> _visibleClipSlotScratch = new();
|
||||||
|
private readonly List<int> _floodViewRouteScratch = new();
|
||||||
private WalkPlane _lookInCyPlane;
|
private WalkPlane _lookInCyPlane;
|
||||||
|
|
||||||
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
|
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
|
||||||
|
|
@ -524,12 +543,14 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
WbDrawDispatcher dispatcher,
|
WbDrawDispatcher dispatcher,
|
||||||
IWalkFrameLeafRenderer leafRenderer,
|
IWalkFrameLeafRenderer leafRenderer,
|
||||||
IWalkFrameWorldData worldData,
|
IWalkFrameWorldData worldData,
|
||||||
IWalkFrameDriverTrace? trace = null)
|
IWalkFrameDriverTrace? trace = null,
|
||||||
|
ClipFrame? clipFrame = null)
|
||||||
{
|
{
|
||||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||||
_leafRenderer = leafRenderer ?? throw new ArgumentNullException(nameof(leafRenderer));
|
_leafRenderer = leafRenderer ?? throw new ArgumentNullException(nameof(leafRenderer));
|
||||||
_worldData = worldData ?? throw new ArgumentNullException(nameof(worldData));
|
_worldData = worldData ?? throw new ArgumentNullException(nameof(worldData));
|
||||||
_trace = trace;
|
_trace = trace;
|
||||||
|
_clipFrame = clipFrame;
|
||||||
_populator = new WalkStaticStreamPopulator(dispatcher);
|
_populator = new WalkStaticStreamPopulator(dispatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -621,6 +642,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
_lookInTurns.Clear();
|
_lookInTurns.Clear();
|
||||||
_lookInSlices.Clear();
|
_lookInSlices.Clear();
|
||||||
_lookInPlanes.Clear();
|
_lookInPlanes.Clear();
|
||||||
|
_visibleClipSlotScratch.Clear();
|
||||||
_lookInCyPlane = ctx.CyPlane;
|
_lookInCyPlane = ctx.CyPlane;
|
||||||
LookInCells.Clear();
|
LookInCells.Clear();
|
||||||
VisitedBuildings.Clear();
|
VisitedBuildings.Clear();
|
||||||
|
|
@ -699,7 +721,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
_leafRenderer.DrawTerrainSlice(e.IntArg);
|
_leafRenderer.DrawTerrainSlice(e.IntArg);
|
||||||
break;
|
break;
|
||||||
case WalkFrameEventKind.CellShell:
|
case WalkFrameEventKind.CellShell:
|
||||||
_leafRenderer.DrawCellShell(e.CellId);
|
_leafRenderer.DrawCellShell(e.CellId, checked((uint)e.IntArg));
|
||||||
break;
|
break;
|
||||||
case WalkFrameEventKind.PunchFan:
|
case WalkFrameEventKind.PunchFan:
|
||||||
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
|
_leafRenderer.DrawPunchFan(e.Polygon!, e.IntArg);
|
||||||
|
|
@ -985,12 +1007,23 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
|
|
||||||
private void EmitFloodTurns(WalkDrawStage stage, IReadOnlyList<uint> cells)
|
private void EmitFloodTurns(WalkDrawStage stage, IReadOnlyList<uint> cells)
|
||||||
{
|
{
|
||||||
|
_floodViewRouteScratch.Clear();
|
||||||
|
for (int i = 0; i < cells.Count; i++)
|
||||||
|
_floodViewRouteScratch.Add(-1);
|
||||||
|
|
||||||
// PView::DrawCells @0x005A4840, loop 2 (005A4A00–005A4ADE):
|
// PView::DrawCells @0x005A4840, loop 2 (005A4A00–005A4ADE):
|
||||||
// cell_draw_list[count - 1] down to zero, DrawEnvCell only.
|
// cell_draw_list[count - 1] down to zero. DrawEnvCell loops the
|
||||||
|
// cell's live portal_view slices, so capture those slices here and
|
||||||
|
// replay the shell once per exact GPU clip slot.
|
||||||
for (int i = cells.Count - 1; i >= 0; i--)
|
for (int i = cells.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
|
int viewRouteIndex = CaptureCellViewRoute(cells[i]);
|
||||||
|
_floodViewRouteScratch[i] = viewRouteIndex;
|
||||||
|
IReadOnlyList<uint> clipSlots = VisibleClipSlotsInLookInTurn(
|
||||||
|
viewRouteIndex, default, 0f, testSphere: false);
|
||||||
MarkIfGrown();
|
MarkIfGrown();
|
||||||
_events.Add(WalkFrameEvent.CellShell(cells[i]));
|
for (int slotIndex = 0; slotIndex < clipSlots.Count; slotIndex++)
|
||||||
|
_events.Add(WalkFrameEvent.CellShell(cells[i], clipSlots[slotIndex]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop 3 (005A4ADE–005A4B2D): restart at count - 1 and draw each
|
// Loop 3 (005A4ADE–005A4B2D): restart at count - 1 and draw each
|
||||||
|
|
@ -998,23 +1031,31 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
// static records and its dynamic/particle tail stay at that exact
|
// static records and its dynamic/particle tail stay at that exact
|
||||||
// retail turn.
|
// retail turn.
|
||||||
for (int i = cells.Count - 1; i >= 0; i--)
|
for (int i = cells.Count - 1; i >= 0; i--)
|
||||||
EmitCellContentsTurn(stage, cells[i]);
|
EmitCellContentsTurn(stage, cells[i], _floodViewRouteScratch[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EmitCellContentsTurn(WalkDrawStage stage, uint cellId)
|
private int CaptureCellViewRoute(uint cellId)
|
||||||
{
|
{
|
||||||
|
int viewRouteIndex = _cellViewRouteIndex++;
|
||||||
|
CaptureCellViews(cellId);
|
||||||
|
return viewRouteIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EmitCellContentsTurn(
|
||||||
|
WalkDrawStage stage,
|
||||||
|
uint cellId,
|
||||||
|
int viewRouteIndex)
|
||||||
|
{
|
||||||
|
// The shell loop captured the exact live portal_view for this cell.
|
||||||
|
// Static and dynamic object lists reuse that same route: retail's
|
||||||
|
// DrawEnvCell and DrawObjCell consume the same PortalList.
|
||||||
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
|
WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId);
|
||||||
_populator.PopulateCell(
|
_populator.PopulateCell(
|
||||||
_stream, stage, cellId, records.Records, records.TupleLandblockId,
|
_stream, stage, cellId, records.Records, records.TupleLandblockId,
|
||||||
_cameraWorldPosition, _viewProjection);
|
_cameraWorldPosition, _viewProjection,
|
||||||
|
this, viewRouteIndex);
|
||||||
MarkIfGrown();
|
MarkIfGrown();
|
||||||
|
|
||||||
// PView::DrawObjCellForDummies draws this cell's live objects at the
|
|
||||||
// same turn as its statics. Capture the currently installed portal
|
|
||||||
// views before the walk pops them, then apply retail's per-GfxObj
|
|
||||||
// drawing-sphere viewcone check in the walk classifier.
|
|
||||||
int viewRouteIndex = _cellViewRouteIndex++;
|
|
||||||
CaptureCellViews(cellId);
|
|
||||||
if (stage == WalkDrawStage.LookInStatic)
|
if (stage == WalkDrawStage.LookInStatic)
|
||||||
{
|
{
|
||||||
LookInCellTurns.Add(cellId);
|
LookInCellTurns.Add(cellId);
|
||||||
|
|
@ -1062,8 +1103,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
_lookInPlanes.Add(
|
_lookInPlanes.Add(
|
||||||
portalView.View.Vertices[poly.VertexIndex + edge].Plane);
|
portalView.View.Vertices[poly.VertexIndex + edge].Plane);
|
||||||
}
|
}
|
||||||
|
uint clipSlot = AppendClipSlot(portalView, poly);
|
||||||
_lookInSlices.Add(new WalkLookInSlice(
|
_lookInSlices.Add(new WalkLookInSlice(
|
||||||
planeStart, poly.VertexCount));
|
planeStart, poly.VertexCount, clipSlot));
|
||||||
}
|
}
|
||||||
_lookInTurns.Add(new WalkLookInTurn(
|
_lookInTurns.Add(new WalkLookInTurn(
|
||||||
cellId, sliceStart, _lookInSlices.Count - sliceStart));
|
cellId, sliceStart, _lookInSlices.Count - sliceStart));
|
||||||
|
|
@ -1095,6 +1137,100 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
|
||||||
|
int routeIndex,
|
||||||
|
in Vector3 center,
|
||||||
|
float radius,
|
||||||
|
bool testSphere)
|
||||||
|
{
|
||||||
|
_visibleClipSlotScratch.Clear();
|
||||||
|
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
|
||||||
|
return _visibleClipSlotScratch;
|
||||||
|
|
||||||
|
WalkLookInTurn turn = _lookInTurns[routeIndex];
|
||||||
|
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
|
||||||
|
{
|
||||||
|
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
|
||||||
|
if (!testSphere
|
||||||
|
|| WalkVisibilityMath.ViewconeCheck(
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
_lookInCyPlane,
|
||||||
|
CollectionsMarshal.AsSpan(_lookInPlanes).Slice(
|
||||||
|
slice.PlaneStart,
|
||||||
|
slice.PlaneCount)) != WalkBoundingType.Outside)
|
||||||
|
{
|
||||||
|
_visibleClipSlotScratch.Add(slice.ClipSlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _visibleClipSlotScratch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Converts one retail pixel-space portal_view polygon into the
|
||||||
|
/// clip-space half-planes consumed by mesh_modern.vert. copy_view already
|
||||||
|
/// deduplicates and removes collinear vertices, so 3..8 points map
|
||||||
|
/// directly. The rare >8-edge case uses its convex AABB as a conservative
|
||||||
|
/// four-plane gate rather than falling back to slot 0 (which would erase
|
||||||
|
/// the aperture entirely).</summary>
|
||||||
|
private uint AppendClipSlot(WalkPortalView portalView, WalkViewPoly poly)
|
||||||
|
{
|
||||||
|
if (_clipFrame is null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
IRetailFrameWalkContext ctx = (IRetailFrameWalkContext)RequireOpenFrame();
|
||||||
|
int count = poly.VertexCount;
|
||||||
|
if (count < 3)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
Span<Vector2> ndc = stackalloc Vector2[Math.Min(count, WalkCopyView.MaxVertices)];
|
||||||
|
float minX = float.MaxValue, minY = float.MaxValue;
|
||||||
|
float maxX = float.MinValue, maxY = float.MinValue;
|
||||||
|
for (int i = 0; i < ndc.Length; i++)
|
||||||
|
{
|
||||||
|
Vector2 px = portalView.View.Vertices[poly.VertexIndex + i].Point;
|
||||||
|
Vector2 point = new(
|
||||||
|
px.X / ctx.ViewportWidth * 2f - 1f,
|
||||||
|
1f - px.Y / ctx.ViewportHeight * 2f);
|
||||||
|
ndc[i] = point;
|
||||||
|
minX = MathF.Min(minX, point.X);
|
||||||
|
minY = MathF.Min(minY, point.Y);
|
||||||
|
maxX = MathF.Max(maxX, point.X);
|
||||||
|
maxY = MathF.Max(maxY, point.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ndc.Length > ClipFrame.MaxPlanes)
|
||||||
|
{
|
||||||
|
Span<Vector4> aabbPlanes = stackalloc Vector4[4]
|
||||||
|
{
|
||||||
|
new(1f, 0f, 0f, -minX),
|
||||||
|
new(-1f, 0f, 0f, maxX),
|
||||||
|
new(0f, 1f, 0f, -minY),
|
||||||
|
new(0f, -1f, 0f, maxY),
|
||||||
|
};
|
||||||
|
return checked((uint)_clipFrame.AppendSlot(aabbPlanes));
|
||||||
|
}
|
||||||
|
|
||||||
|
float area2 = 0f;
|
||||||
|
for (int i = 0; i < ndc.Length; i++)
|
||||||
|
area2 += ndc[i].X * ndc[(i + 1) % ndc.Length].Y
|
||||||
|
- ndc[(i + 1) % ndc.Length].X * ndc[i].Y;
|
||||||
|
bool ccw = area2 >= 0f;
|
||||||
|
Span<Vector4> planes = stackalloc Vector4[ClipFrame.MaxPlanes];
|
||||||
|
for (int i = 0; i < ndc.Length; i++)
|
||||||
|
{
|
||||||
|
int current = ccw ? i : ndc.Length - 1 - i;
|
||||||
|
int next = ccw
|
||||||
|
? (i + 1) % ndc.Length
|
||||||
|
: (ndc.Length - 2 - i + ndc.Length) % ndc.Length;
|
||||||
|
Vector2 p = ndc[current];
|
||||||
|
Vector2 q = ndc[next];
|
||||||
|
Vector2 dir = q - p;
|
||||||
|
Vector2 normal = Vector2.Normalize(new Vector2(-dir.Y, dir.X));
|
||||||
|
planes[i] = new Vector4(normal.X, normal.Y, 0f, -Vector2.Dot(normal, p));
|
||||||
|
}
|
||||||
|
return checked((uint)_clipFrame.AppendSlot(planes[..ndc.Length]));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Campaign FW3.4a: the collect-time analogue of the old
|
/// <summary>Campaign FW3.4a: the collect-time analogue of the old
|
||||||
/// immediate driver's <c>FlushIfNonEmpty</c> — records a
|
/// immediate driver's <c>FlushIfNonEmpty</c> — records a
|
||||||
/// <see cref="WalkFrameEventKind.StreamMark"/> at the stream's current
|
/// <see cref="WalkFrameEventKind.StreamMark"/> at the stream's current
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,17 @@ internal sealed class WalkStaticStreamPopulator
|
||||||
ReadOnlySpan<RenderProjectionRecord> records,
|
ReadOnlySpan<RenderProjectionRecord> records,
|
||||||
uint tupleLandblockId,
|
uint tupleLandblockId,
|
||||||
Vector3 cameraWorldPosition,
|
Vector3 cameraWorldPosition,
|
||||||
Matrix4x4 viewProjection)
|
Matrix4x4 viewProjection,
|
||||||
|
IWalkLookInViewSource? views = null,
|
||||||
|
int viewRouteIndex = -1)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(stream);
|
ArgumentNullException.ThrowIfNull(stream);
|
||||||
for (int i = 0; i < records.Length; i++)
|
for (int i = 0; i < records.Length; i++)
|
||||||
{
|
{
|
||||||
ClassifyAndAppend(
|
ClassifyAndAppend(
|
||||||
stream, stage, cellId, in records[i], tupleLandblockId,
|
stream, stage, cellId, in records[i], tupleLandblockId,
|
||||||
cameraWorldPosition, viewProjection);
|
cameraWorldPosition, viewProjection,
|
||||||
|
liveDynamic: false, views, viewRouteIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -274,23 +274,15 @@ public sealed partial class WbDrawDispatcher
|
||||||
Matrix4x4 model = restPose * entity.RootWorld;
|
Matrix4x4 model = restPose * entity.RootWorld;
|
||||||
int selectionPartIndex = unchecked((partIndex << 16) | (setupPartIndex & 0xFFFF));
|
int selectionPartIndex = unchecked((partIndex << 16) | (setupPartIndex & 0xFFFF));
|
||||||
|
|
||||||
if (!PartVisibleInLookInTurn(
|
IReadOnlyList<uint>? partClipSlots = ResolvePartClipSlots(
|
||||||
lookInViews,
|
lookInViews, lookInRouteIndex, partData, model);
|
||||||
lookInRouteIndex,
|
if (partClipSlots is { Count: 0 })
|
||||||
lookInCellId,
|
|
||||||
in entity,
|
|
||||||
selectionPartIndex,
|
|
||||||
(uint)gfxObjId,
|
|
||||||
partData,
|
|
||||||
model))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
EmitClassifiedBatches(
|
EmitClassifiedBatches(
|
||||||
partData, model, in entity, meshRef, paletteIdentity,
|
partData, model, in entity, meshRef, paletteIdentity,
|
||||||
entityHasCutoutSubset, slot, lights, indoor, selectionLighting,
|
entityHasCutoutSubset, slot, lights, indoor, selectionLighting,
|
||||||
detailCategory, opacity, batches);
|
detailCategory, opacity, partClipSlots, batches);
|
||||||
selectionParts.Add(new WalkClassifiedSelectionPart(
|
selectionParts.Add(new WalkClassifiedSelectionPart(
|
||||||
entity.ServerGuid, entity.LocalEntityId, selectionPartIndex,
|
entity.ServerGuid, entity.LocalEntityId, selectionPartIndex,
|
||||||
(uint)gfxObjId, model));
|
(uint)gfxObjId, model));
|
||||||
|
|
@ -308,22 +300,14 @@ public sealed partial class WbDrawDispatcher
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Matrix4x4 model = meshRef.PartTransform * entity.RootWorld;
|
Matrix4x4 model = meshRef.PartTransform * entity.RootWorld;
|
||||||
if (!PartVisibleInLookInTurn(
|
IReadOnlyList<uint>? partClipSlots = ResolvePartClipSlots(
|
||||||
lookInViews,
|
lookInViews, lookInRouteIndex, renderData, model);
|
||||||
lookInRouteIndex,
|
if (partClipSlots is { Count: 0 })
|
||||||
lookInCellId,
|
|
||||||
in entity,
|
|
||||||
partIndex,
|
|
||||||
(uint)meshRef.GfxObjId,
|
|
||||||
renderData,
|
|
||||||
model))
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
EmitClassifiedBatches(
|
EmitClassifiedBatches(
|
||||||
renderData, model, in entity, meshRef, paletteIdentity,
|
renderData, model, in entity, meshRef, paletteIdentity,
|
||||||
entityHasCutoutSubsetOverride: null, slot, lights, indoor,
|
entityHasCutoutSubsetOverride: null, slot, lights, indoor,
|
||||||
selectionLighting, detailCategory, opacity, batches);
|
selectionLighting, detailCategory, opacity, partClipSlots, batches);
|
||||||
selectionParts.Add(new WalkClassifiedSelectionPart(
|
selectionParts.Add(new WalkClassifiedSelectionPart(
|
||||||
entity.ServerGuid, entity.LocalEntityId, partIndex,
|
entity.ServerGuid, entity.LocalEntityId, partIndex,
|
||||||
(uint)meshRef.GfxObjId, model));
|
(uint)meshRef.GfxObjId, model));
|
||||||
|
|
@ -379,28 +363,29 @@ public sealed partial class WbDrawDispatcher
|
||||||
lights = InstanceLightSet.From(selected);
|
lights = InstanceLightSet.From(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool PartVisibleInLookInTurn(
|
private static IReadOnlyList<uint>? ResolvePartClipSlots(
|
||||||
IWalkLookInViewSource? lookInViews,
|
IWalkLookInViewSource? lookInViews,
|
||||||
int routeIndex,
|
int routeIndex,
|
||||||
uint cellId,
|
|
||||||
in RenderInstanceCandidate entity,
|
|
||||||
int partIndex,
|
|
||||||
uint gfxObjId,
|
|
||||||
ObjectRenderData renderData,
|
ObjectRenderData renderData,
|
||||||
Matrix4x4 localToWorld)
|
Matrix4x4 localToWorld)
|
||||||
{
|
{
|
||||||
if (lookInViews is null)
|
if (lookInViews is null)
|
||||||
return true;
|
return null;
|
||||||
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
|
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
|
||||||
return true;
|
{
|
||||||
|
return lookInViews.VisibleClipSlotsInLookInTurn(
|
||||||
|
routeIndex,
|
||||||
|
Vector3.Zero,
|
||||||
|
radius: 0f,
|
||||||
|
testSphere: false);
|
||||||
|
}
|
||||||
|
|
||||||
return LookInDrawingSphereVisible(
|
TransformDrawingSphere(sphere, localToWorld, out Vector3 center, out float radius);
|
||||||
lookInViews,
|
return lookInViews.VisibleClipSlotsInLookInTurn(
|
||||||
routeIndex,
|
routeIndex,
|
||||||
sphere,
|
in center,
|
||||||
localToWorld,
|
radius,
|
||||||
out _,
|
testSphere: true);
|
||||||
out _);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool LookInDrawingSphereVisible(
|
internal static bool LookInDrawingSphereVisible(
|
||||||
|
|
@ -414,6 +399,19 @@ public sealed partial class WbDrawDispatcher
|
||||||
ArgumentNullException.ThrowIfNull(lookInViews);
|
ArgumentNullException.ThrowIfNull(lookInViews);
|
||||||
ArgumentNullException.ThrowIfNull(sphere);
|
ArgumentNullException.ThrowIfNull(sphere);
|
||||||
|
|
||||||
|
TransformDrawingSphere(sphere, localToWorld, out center, out radius);
|
||||||
|
return lookInViews.SphereVisibleInLookInTurn(
|
||||||
|
routeIndex,
|
||||||
|
in center,
|
||||||
|
radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TransformDrawingSphere(
|
||||||
|
DatReaderWriter.Types.Sphere sphere,
|
||||||
|
Matrix4x4 localToWorld,
|
||||||
|
out Vector3 center,
|
||||||
|
out float radius)
|
||||||
|
{
|
||||||
center = Vector3.Transform(sphere.Origin, localToWorld);
|
center = Vector3.Transform(sphere.Origin, localToWorld);
|
||||||
float scaleX = new Vector3(
|
float scaleX = new Vector3(
|
||||||
localToWorld.M11,
|
localToWorld.M11,
|
||||||
|
|
@ -429,10 +427,6 @@ public sealed partial class WbDrawDispatcher
|
||||||
localToWorld.M33).Length();
|
localToWorld.M33).Length();
|
||||||
radius = sphere.Radius
|
radius = sphere.Radius
|
||||||
* MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
|
* MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
|
||||||
return lookInViews.SphereVisibleInLookInTurn(
|
|
||||||
routeIndex,
|
|
||||||
in center,
|
|
||||||
radius);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -454,6 +448,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
Vector2 selectionLighting,
|
Vector2 selectionLighting,
|
||||||
uint detailCategory,
|
uint detailCategory,
|
||||||
float opacity,
|
float opacity,
|
||||||
|
IReadOnlyList<uint>? clipSlots,
|
||||||
List<WalkClassifiedBatch> sink)
|
List<WalkClassifiedBatch> sink)
|
||||||
{
|
{
|
||||||
bool entityHasCutoutSubset = entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset;
|
bool entityHasCutoutSubset = entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset;
|
||||||
|
|
@ -466,10 +461,23 @@ public sealed partial class WbDrawDispatcher
|
||||||
if (!survives)
|
if (!survives)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
sink.Add(new WalkClassifiedBatch(
|
if (clipSlots is null)
|
||||||
key, model, slot, lights, indoor ? 1u : 0u, Alpha: opacity,
|
{
|
||||||
selectionLighting, detailCategory, IsOpaque: IsOpaque(key.Translucency),
|
sink.Add(new WalkClassifiedBatch(
|
||||||
LocalSortCenter: renderData.SortCenter));
|
key, model, slot, lights, indoor ? 1u : 0u, Alpha: opacity,
|
||||||
|
selectionLighting, detailCategory, IsOpaque: IsOpaque(key.Translucency),
|
||||||
|
LocalSortCenter: renderData.SortCenter));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int clipIndex = 0; clipIndex < clipSlots.Count; clipIndex++)
|
||||||
|
{
|
||||||
|
sink.Add(new WalkClassifiedBatch(
|
||||||
|
key, model, clipSlots[clipIndex], lights, indoor ? 1u : 0u,
|
||||||
|
Alpha: opacity, selectionLighting, detailCategory,
|
||||||
|
IsOpaque: IsOpaque(key.Translucency),
|
||||||
|
LocalSortCenter: renderData.SortCenter));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,17 @@ public sealed class WalkFrameDriverTests
|
||||||
private sealed class RecordingLeafRenderer(List<string> log) : IWalkFrameLeafRenderer
|
private sealed class RecordingLeafRenderer(List<string> log) : IWalkFrameLeafRenderer
|
||||||
{
|
{
|
||||||
public readonly List<WalkPolygon> Punches = new();
|
public readonly List<WalkPolygon> Punches = new();
|
||||||
|
public readonly List<(uint CellId, uint ClipSlot)> Shells = new();
|
||||||
|
|
||||||
public void DrawSky() => log.Add("SKY");
|
public void DrawSky() => log.Add("SKY");
|
||||||
|
|
||||||
public void DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
|
public void DrawTerrainSlice(int sliceIndex) => log.Add($"TERRAIN:{sliceIndex}");
|
||||||
|
|
||||||
public void DrawCellShell(uint cellId) => log.Add($"SHELL:{cellId:x8}");
|
public void DrawCellShell(uint cellId, uint clipSlot)
|
||||||
|
{
|
||||||
|
Shells.Add((cellId, clipSlot));
|
||||||
|
log.Add($"SHELL:{cellId:x8}");
|
||||||
|
}
|
||||||
|
|
||||||
public void ClearInteriorDepth() => log.Add("CLEAR");
|
public void ClearInteriorDepth() => log.Add("CLEAR");
|
||||||
|
|
||||||
|
|
@ -427,7 +432,9 @@ public sealed class WalkFrameDriverTests
|
||||||
|
|
||||||
var leaf = new RecordingLeafRenderer(log);
|
var leaf = new RecordingLeafRenderer(log);
|
||||||
var trace = new RecordingTrace(log);
|
var trace = new RecordingTrace(log);
|
||||||
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData, trace);
|
using ClipFrame clipFrame = ClipFrame.NoClip();
|
||||||
|
var driver = new WalkFrameDriver(
|
||||||
|
fx.Dispatcher, leaf, worldData, trace, clipFrame);
|
||||||
var walk = new RetailFrameWalk();
|
var walk = new RetailFrameWalk();
|
||||||
|
|
||||||
var activeView = new WalkPortalView();
|
var activeView = new WalkPortalView();
|
||||||
|
|
@ -456,6 +463,13 @@ public sealed class WalkFrameDriverTests
|
||||||
|
|
||||||
Assert.Equal([0x104u], driver.LookInCellTurns);
|
Assert.Equal([0x104u], driver.LookInCellTurns);
|
||||||
Assert.Equal([0x104u], driver.LookInCells);
|
Assert.Equal([0x104u], driver.LookInCells);
|
||||||
|
Assert.Collection(
|
||||||
|
leaf.Shells,
|
||||||
|
shell =>
|
||||||
|
{
|
||||||
|
Assert.Equal(0x104u, shell.CellId);
|
||||||
|
Assert.NotEqual(0u, shell.ClipSlot);
|
||||||
|
});
|
||||||
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
|
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
|
||||||
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
|
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
|
||||||
Vector2 capturedCenter = Vector2.Zero;
|
Vector2 capturedCenter = Vector2.Zero;
|
||||||
|
|
@ -470,6 +484,10 @@ public sealed class WalkFrameDriverTests
|
||||||
0, in insideCone, 0.1f));
|
0, in insideCone, 0.1f));
|
||||||
Assert.False(driver.SphereVisibleInLookInTurn(
|
Assert.False(driver.SphereVisibleInLookInTurn(
|
||||||
0, new Vector3(10_000f, 0f, 10f), 0.1f));
|
0, new Vector3(10_000f, 0f, 10f), 0.1f));
|
||||||
|
uint clipSlot = Assert.Single(driver.VisibleClipSlotsInLookInTurn(
|
||||||
|
0, in insideCone, 0.1f, testSphere: true));
|
||||||
|
Assert.NotEqual(0u, clipSlot);
|
||||||
|
Assert.Equal(2, clipFrame.SlotCount);
|
||||||
|
|
||||||
// The punch polygon reached the leaf renderer in WORLD space: the
|
// The punch polygon reached the leaf renderer in WORLD space: the
|
||||||
// building-local Quad(-2f) vertex (-0.5,-0.5,-2) translates by
|
// building-local Quad(-2f) vertex (-0.5,-0.5,-2) translates by
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,13 @@ public sealed class WalkProductionWorldConformanceTests
|
||||||
private sealed class Recorder : IWalkEventSink
|
private sealed class Recorder : IWalkEventSink
|
||||||
{
|
{
|
||||||
public readonly List<WalkEvent> Events = new();
|
public readonly List<WalkEvent> Events = new();
|
||||||
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
|
public Action<WalkEvent>? OnEmit { get; init; }
|
||||||
|
|
||||||
|
public void Emit(in WalkEvent walkEvent)
|
||||||
|
{
|
||||||
|
Events.Add(walkEvent);
|
||||||
|
OnEmit?.Invoke(walkEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DatCollection OpenDats()
|
private static DatCollection OpenDats()
|
||||||
|
|
@ -227,6 +233,93 @@ public sealed class WalkProductionWorldConformanceTests
|
||||||
$"production walk diverged from the FW1 test adapter ({fixture})\nEXPECTED: {expected}\nACTUAL: {actual}");
|
$"production walk diverged from the FW1 test adapter ({fixture})\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0xF4180100u, 36.166267f, 79.828407f)]
|
||||||
|
[InlineData(0xF4180101u, 36.391270f, 72.167931f)]
|
||||||
|
public void Cathedral_transition_keeps_south_hall_draws_inside_the_authored_aperture(
|
||||||
|
uint cameraCellId,
|
||||||
|
float x,
|
||||||
|
float y)
|
||||||
|
{
|
||||||
|
// Owner's exact 2026-08-31 repro: the remote player and special NPC
|
||||||
|
// are parented in 0xF4180112, behind opaque cathedral walls. Retail
|
||||||
|
// hides them on BOTH sides of the 0x100 <-> 0x101 transition. The walk
|
||||||
|
// legitimately reaches 0x112 through one authored building aperture;
|
||||||
|
// the regression was submitting each admitted mesh with slot 0, so the
|
||||||
|
// whole player/NPC escaped that aperture. Preserve the installed-DAT
|
||||||
|
// fact this fix depends on: every admitted route is a real, bounded
|
||||||
|
// portal polygon, never a pass-all zero-plane route.
|
||||||
|
var pose = new WalkOraclePose(
|
||||||
|
cameraCellId,
|
||||||
|
new Vector3(x, y, 169.804993f),
|
||||||
|
Q0: -0.004591f,
|
||||||
|
Q1: 0f,
|
||||||
|
Q2: 0f,
|
||||||
|
Q3: 0.999989f);
|
||||||
|
using DatCollection dats = OpenDats();
|
||||||
|
using var adapter = new DatCollectionAdapter(dats);
|
||||||
|
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||||||
|
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||||||
|
BuildProductionWorld(adapter, pose.CellId, pose.Origin);
|
||||||
|
var ctx = new WalkTraceReplayContext(pose, cells) { Buildings = buildings };
|
||||||
|
WalkCell camera = Assert.Contains(cameraCellId, cells);
|
||||||
|
var remotePlayer = new Vector3(36.299465f, 18.594580f, 169.804993f);
|
||||||
|
var southHallAdmission = new List<string>();
|
||||||
|
var recorder = new Recorder
|
||||||
|
{
|
||||||
|
OnEmit = e =>
|
||||||
|
{
|
||||||
|
if (e.Kind != WalkEventKind.DrawCells
|
||||||
|
|| !e.Cells.Contains(0xF4180112u))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WalkPortalView views = cells[0xF4180112u].TopView;
|
||||||
|
for (int viewIndex = 0; viewIndex < views.ViewCount; viewIndex++)
|
||||||
|
{
|
||||||
|
WalkViewPoly poly = views.View.Polys[viewIndex];
|
||||||
|
var planes = new WalkPlane[poly.VertexCount];
|
||||||
|
for (int edge = 0; edge < poly.VertexCount; edge++)
|
||||||
|
{
|
||||||
|
planes[edge] = views.View.Vertices[
|
||||||
|
poly.VertexIndex + edge].Plane;
|
||||||
|
}
|
||||||
|
|
||||||
|
WalkBoundingType verdict = WalkVisibilityMath.ViewconeCheck(
|
||||||
|
remotePlayer,
|
||||||
|
radius: 1.5f,
|
||||||
|
ctx.CyPlane,
|
||||||
|
planes);
|
||||||
|
float minDistance = planes
|
||||||
|
.Select(plane => Vector3.Dot(plane.Normal, remotePlayer) + plane.D)
|
||||||
|
.Append(Vector3.Dot(ctx.CyPlane.Normal, remotePlayer) + ctx.CyPlane.D)
|
||||||
|
.Min();
|
||||||
|
southHallAdmission.Add(
|
||||||
|
$"view={viewIndex} planes={poly.VertexCount} verdict={verdict} min={minDistance:F4}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new RetailFrameWalk().WalkFrame(
|
||||||
|
cameraCellId,
|
||||||
|
camera,
|
||||||
|
assembler.Landscape,
|
||||||
|
ctx,
|
||||||
|
recorder);
|
||||||
|
|
||||||
|
WalkEvent[] drawCells = recorder.Events
|
||||||
|
.Where(static e => e.Kind == WalkEventKind.DrawCells)
|
||||||
|
.ToArray();
|
||||||
|
Assert.NotEmpty(drawCells);
|
||||||
|
Assert.Contains(cameraCellId, drawCells[0].Cells);
|
||||||
|
Assert.NotEmpty(southHallAdmission);
|
||||||
|
Assert.Contains(
|
||||||
|
southHallAdmission,
|
||||||
|
static verdict => verdict.Contains("verdict=PartiallyInside")
|
||||||
|
&& !verdict.Contains("planes=0"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Foundry_entry_reproduces_every_frame_before_the_f67_order_segment()
|
public void Foundry_entry_reproduces_every_frame_before_the_f67_order_segment()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,22 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Calls.Add((serverGuid, localEntityId, partIndex, gfxObjId, partWorld));
|
Calls.Add((serverGuid, localEntityId, partIndex, gfxObjId, partWorld));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class FixedWalkViews(params uint[] slots) : IWalkLookInViewSource
|
||||||
|
{
|
||||||
|
public IReadOnlyList<uint> LookInCellTurns { get; } = [0x8C040112u];
|
||||||
|
|
||||||
|
public bool SphereVisibleInLookInTurn(
|
||||||
|
int routeIndex,
|
||||||
|
in Vector3 center,
|
||||||
|
float radius) => slots.Length != 0;
|
||||||
|
|
||||||
|
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
|
||||||
|
int routeIndex,
|
||||||
|
in Vector3 center,
|
||||||
|
float radius,
|
||||||
|
bool testSphere) => slots;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Synthetic RenderProjectionRecord construction ──────────────────────
|
// ── Synthetic RenderProjectionRecord construction ──────────────────────
|
||||||
|
|
||||||
private static RenderProjectionRecord MakeRecord(
|
private static RenderProjectionRecord MakeRecord(
|
||||||
|
|
@ -212,6 +228,35 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId);
|
Assert.Equal((uint)leavesGfxObj, selectionParts[1].GfxObjId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClassifyEntityForWalk_EmitsOneGpuClippedInstancePerPortalViewSlice()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture();
|
||||||
|
const ulong gfxObj = 0x0100_0013UL;
|
||||||
|
InjectRenderData(fx.Manager, gfxObj, MakeFlatMesh(
|
||||||
|
MakeBatch(0x08000013u, TranslucencyKind.Opaque, 0, 0, 3, 1)));
|
||||||
|
RenderProjectionRecord record = MakeRecord(
|
||||||
|
201, 0, Vector3.Zero,
|
||||||
|
[new MeshRef((uint)gfxObj, Matrix4x4.Identity)],
|
||||||
|
parentCellId: 0x8C040112u);
|
||||||
|
var batches = new List<WbDrawDispatcher.WalkClassifiedBatch>();
|
||||||
|
var selectionParts = new List<WbDrawDispatcher.WalkClassifiedSelectionPart>();
|
||||||
|
var views = new FixedWalkViews(7u, 9u);
|
||||||
|
|
||||||
|
fx.Dispatcher.ClassifyEntityForWalk(
|
||||||
|
in record,
|
||||||
|
0x8C04u,
|
||||||
|
batches,
|
||||||
|
selectionParts,
|
||||||
|
liveDynamic: true,
|
||||||
|
views,
|
||||||
|
lookInRouteIndex: 0,
|
||||||
|
lookInCellId: 0x8C040112u);
|
||||||
|
|
||||||
|
Assert.Equal([7u, 9u], batches.Select(static batch => batch.ClipSlot));
|
||||||
|
Assert.Single(selectionParts);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Deliverable 2: WalkStaticStreamPopulator routing ───────────────────
|
// ── Deliverable 2: WalkStaticStreamPopulator routing ───────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -56,5 +56,17 @@ public sealed class WbDrawDispatcherLookInConeTests
|
||||||
Radius = radius;
|
Radius = radius;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<uint> VisibleClipSlotsInLookInTurn(
|
||||||
|
int routeIndex,
|
||||||
|
in Vector3 center,
|
||||||
|
float radius,
|
||||||
|
bool testSphere)
|
||||||
|
{
|
||||||
|
RouteIndex = routeIndex;
|
||||||
|
Center = center;
|
||||||
|
Radius = radius;
|
||||||
|
return [7u];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue