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
|
|
@ -43,12 +43,17 @@ public sealed class WalkFrameDriverTests
|
|||
private sealed class RecordingLeafRenderer(List<string> log) : IWalkFrameLeafRenderer
|
||||
{
|
||||
public readonly List<WalkPolygon> Punches = new();
|
||||
public readonly List<(uint CellId, uint ClipSlot)> Shells = new();
|
||||
|
||||
public void DrawSky() => log.Add("SKY");
|
||||
|
||||
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");
|
||||
|
||||
|
|
@ -427,7 +432,9 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
var leaf = new RecordingLeafRenderer(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 activeView = new WalkPortalView();
|
||||
|
|
@ -456,6 +463,13 @@ public sealed class WalkFrameDriverTests
|
|||
|
||||
Assert.Equal([0x104u], driver.LookInCellTurns);
|
||||
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];
|
||||
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
|
||||
Vector2 capturedCenter = Vector2.Zero;
|
||||
|
|
@ -470,6 +484,10 @@ public sealed class WalkFrameDriverTests
|
|||
0, in insideCone, 0.1f));
|
||||
Assert.False(driver.SphereVisibleInLookInTurn(
|
||||
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
|
||||
// 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
|
||||
{
|
||||
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()
|
||||
|
|
@ -227,6 +233,93 @@ public sealed class WalkProductionWorldConformanceTests
|
|||
$"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]
|
||||
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));
|
||||
}
|
||||
|
||||
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 ──────────────────────
|
||||
|
||||
private static RenderProjectionRecord MakeRecord(
|
||||
|
|
@ -212,6 +228,35 @@ public sealed class WalkStaticStreamPopulatorTests
|
|||
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 ───────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -56,5 +56,17 @@ public sealed class WbDrawDispatcherLookInConeTests
|
|||
Radius = radius;
|
||||
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