checkpoint: preserve cathedral look-in investigation state
This commit is contained in:
parent
c287db8651
commit
572de1ec30
11 changed files with 757 additions and 197 deletions
|
|
@ -315,7 +315,7 @@ issue is closed, the strip was missed; delete both.
|
|||
| `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` |
|
||||
| `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` |
|
||||
| `ACDREAM_PROBE_WALK_SHADOW` | Campaign FW3.2b-2 flip apparatus — dies with the flip commit | `=1` | runs the PRODUCTION retail frame walk per frame in SHADOW (no draws change) and prints one `[walk-shadow]` line per frame whose visited cell/building sets diverge from the old visibility path (the I5 dual-shadow pattern applied to the FW3 static cutover) | print-only; adds the per-frame CPU cost of the shadow walk while set | `RenderingDiagnostics.ProbeWalkShadowEnabled` |
|
||||
| `ACDREAM_PROBE_WALK_ROOT` | the cathedral Phase I instrumentation (synthesis doc `docs/research/2026-08-30-cathedral-synthesis.md`) — dies with that investigation | `=1` | prints `[walk-root]` rooting lines every ~30th frame AND for 8 frames after each interior/outdoor root flip (viewer/root cell, camera-cell resolution, slice count, the walk's OWN exit-view count, the TRUE root flood separated from the visited union, eye + forward); `[walk-cam]` replayable camera dumps (eye, viewport, full view-projection) every ~300 frames and at flips; `[falls]` lines on EVERY particle submission carrying a real falls owner (0xCF418000-13) with route + phase; `[falls-adm]` admission verdicts for F418 emitters (~every 120th ApplyRetailView); plus the earlier family — `[walk-part]` per-route submissions, `[walk-alpha]` drains, `[walk-dyn]` outside-stage verdicts (~120th product frame), `[walk-portal]` per-portal side/clip decisions (~90th frame), and `[dyn-route]` per-entity ON-CHANGE DynamicLast admission traces (parent cell, look-in exclusion, viewcone verdict, the cell's lifted-plane shape — "0" marks a pass-all zero-plane slice) | print-only; chatty near particle-heavy scenes while set | `RenderingDiagnostics.ProbeWalkRootEnabled` |
|
||||
| `ACDREAM_PROBE_WALK_ROOT` | the cathedral Phase I/FW4 instrumentation (synthesis doc `docs/research/2026-08-30-cathedral-synthesis.md`) — dies with that investigation | `=1` | prints `[walk-root]` rooting lines every ~30th frame AND for 8 frames after each interior/outdoor root flip (viewer/root cell, camera-cell resolution, slice count, the walk's OWN exit-view count, the TRUE root flood separated from the visited union, eye + forward); `[walk-cam]` replayable camera dumps (eye, viewport, full view-projection) every ~300 frames and at flips; `[lookin-frame]` ON-CHANGE/~30-frame comparisons of diagnostic-only legacy look-in frames against the walk's exact look-in cells, explicit 0xF4180112 membership, and walk-vs-legacy punch counts; `[lookin-draw]` identifies the walk-turn route index/cell actually submitted; `[lookin-punch]` fires if the legacy punch routine executes; `[falls]` lines on EVERY particle submission carrying a real falls owner (0xCF418000-13) with route + phase; `[falls-adm]` admission verdicts for F418 emitters (~every 120th ApplyRetailView); plus the earlier family — `[walk-part]` per-route submissions, `[walk-alpha]` drains, `[walk-dyn]` outside-stage verdicts (~120th product frame), `[walk-portal]` per-portal side/clip decisions (~90th frame), and `[dyn-route]` per-entity ON-CHANGE DynamicLast admission traces (parent cell, look-in exclusion, viewcone verdict, the cell's lifted-plane shape — "0" marks a pass-all zero-plane slice) | print-only; chatty near particle-heavy scenes while set | `RenderingDiagnostics.ProbeWalkRootEnabled` |
|
||||
| `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` |
|
||||
| `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B |
|
||||
| `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` |
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.Core.Vfx;
|
||||
|
||||
|
|
@ -151,6 +152,8 @@ internal sealed partial class RetailPViewPassExecutor
|
|||
int count = Math.Min(vertices.Length, world.Length);
|
||||
for (int vertex = 0; vertex < count; vertex++)
|
||||
world[vertex] = vertices[vertex];
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
WalkLookInPunchCountThisFrame++;
|
||||
_portalDepthMask.DrawDepthFan(
|
||||
world[..count],
|
||||
frame.ViewProjection,
|
||||
|
|
@ -201,6 +204,9 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
private readonly Action _drawExitSeals;
|
||||
private readonly HashSet<uint> _singleCellScratch = new();
|
||||
private readonly List<uint> _singleCellListScratch = new();
|
||||
private readonly HashSet<uint> _lookInParticleOwnerScratch = new();
|
||||
private RenderFrameView _entityFrame;
|
||||
private bool _entityFrameBound;
|
||||
|
||||
internal WalkProductionLeafRenderer(
|
||||
RetailPViewPassExecutor passes,
|
||||
|
|
@ -242,6 +248,55 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|||
_passes.DrawLandscapeStaticParticles(
|
||||
_frame, new RetailPViewLandscapeStaticParticleContext(ownerIds));
|
||||
|
||||
/// <summary>Binds the packed frame product built after the walk's Collect
|
||||
/// pass and before Replay. The walk records route indices during Collect;
|
||||
/// Replay consumes the matching product ranges through this exact view.</summary>
|
||||
internal void BindEntityFrame(in RenderFrameView view)
|
||||
{
|
||||
_entityFrame = view;
|
||||
_entityFrameBound = true;
|
||||
}
|
||||
|
||||
public void DrawLookInDynamics(
|
||||
uint cellId,
|
||||
int routeIndex,
|
||||
IReadOnlySet<uint> staticParticleOwnerIds)
|
||||
{
|
||||
if (!_entityFrameBound)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Walk look-in dynamics replayed before the packed entity frame was bound.");
|
||||
}
|
||||
|
||||
_passes.UseIndoorMembershipOnlyRouting();
|
||||
_passes.DrawEntityRoute(
|
||||
_frame.Camera,
|
||||
in _entityFrame,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId,
|
||||
_frame.PlayerLandblockId ?? 0u);
|
||||
|
||||
_lookInParticleOwnerScratch.Clear();
|
||||
_lookInParticleOwnerScratch.UnionWith(staticParticleOwnerIds);
|
||||
RenderFrameRouteOwnerSelector.Union(
|
||||
_lookInParticleOwnerScratch,
|
||||
in _entityFrame,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId);
|
||||
if (_lookInParticleOwnerScratch.Count > 0)
|
||||
{
|
||||
_passes.DrawCellParticles(
|
||||
_frame,
|
||||
new RetailPViewCellSliceContext(
|
||||
cellId,
|
||||
default,
|
||||
_lookInParticleOwnerScratch));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void DrawExitSeals() => _drawExitSeals();
|
||||
|
||||
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
||||
|
|
|
|||
|
|
@ -138,6 +138,13 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
private readonly Dictionary<uint, int> _singleCellClipRouting = new(1);
|
||||
private readonly Dictionary<uint, int> _noCellClipRouting = new(0);
|
||||
|
||||
// ACDREAM_PROBE_WALK_ROOT / cathedral FW4: observation-only counters.
|
||||
// They distinguish the walk-owned far-Z portal punches from the retired
|
||||
// legacy look-in punch route. RetailPViewRenderer samples them for the
|
||||
// diagnostic comparison after the walk-turn look-in routes have replayed.
|
||||
internal int WalkLookInPunchCountThisFrame { get; private set; }
|
||||
internal int LegacyLookInPunchCountThisFrame { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Borrowed until the next late landscape pass. The outdoor-root post-world
|
||||
/// particle pass consumes this synchronously before another PView frame.
|
||||
|
|
@ -181,7 +188,11 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
{
|
||||
_particleClassifications.BeginFrame();
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
{
|
||||
AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase = "pre";
|
||||
WalkLookInPunchCountThisFrame = 0;
|
||||
LegacyLookInPunchCountThisFrame = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the shared dispatcher, for
|
||||
|
|
@ -596,8 +607,19 @@ internal sealed partial class RetailPViewPassExecutor :
|
|||
public void DrawLookInPortalPunch(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewCellSliceContext context,
|
||||
int portalIndex) =>
|
||||
int portalIndex)
|
||||
{
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
{
|
||||
LegacyLookInPunchCountThisFrame++;
|
||||
Console.WriteLine(
|
||||
$"[lookin-punch] source=legacy viewer=0x{frame.ViewerCellId:X8} "
|
||||
+ $"root=0x{frame.RootCell.CellId:X8} cell=0x{context.CellId:X8} "
|
||||
+ $"portal={portalIndex} planes={context.Slice.Planes.Length} "
|
||||
+ $"phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}");
|
||||
}
|
||||
DrawPortalDepthWrite(context, frame, forceFarZ: true, portalIndex);
|
||||
}
|
||||
|
||||
public void DrawUnattachedSceneParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,12 @@ public sealed class RetailPViewRenderer
|
|||
private bool? _probeWalkRootPrevOutdoor;
|
||||
private int _probeWalkRootFramesLeft;
|
||||
private ulong _probeWalkRootFrame;
|
||||
private string? _probeLookInState;
|
||||
|
||||
// The parked remote player's authoritative parent cell for the current
|
||||
// cathedral FW4 investigation. This is diagnostic scope only: it changes
|
||||
// no visibility or draw decision.
|
||||
private const uint ProbeCathedralRemoteCellId = 0xF4180112u;
|
||||
|
||||
// Campaign FW3.2b-2: the walk's production world-data registries
|
||||
// (published/retired by LandblockRenderPublisher) plus the per-frame
|
||||
|
|
@ -263,6 +269,7 @@ public sealed class RetailPViewRenderer
|
|||
// runs at append time, not at Replay time), so the world data must
|
||||
// already be rebuilt for this frame before the walk starts.
|
||||
Walk.WalkFrameDriver? walkDriver = null;
|
||||
WalkProductionLeafRenderer? walkLeafRenderer = null;
|
||||
if (walkActive)
|
||||
{
|
||||
Matrix4x4 view = ctx.CameraView;
|
||||
|
|
@ -343,9 +350,12 @@ public sealed class RetailPViewRenderer
|
|||
Action drawExitSeals = () =>
|
||||
DrawWalkExitPortalMasks(ctx, passes, clipAssembly, walkDriver!);
|
||||
|
||||
var leafRenderer = new WalkProductionLeafRenderer(
|
||||
walkLeafRenderer = new WalkProductionLeafRenderer(
|
||||
walkExecutor!, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
|
||||
walkDriver = new Walk.WalkFrameDriver(walkExecutor!.Dispatcher, leafRenderer, _walkWorldData);
|
||||
walkDriver = new Walk.WalkFrameDriver(
|
||||
walkExecutor!.Dispatcher,
|
||||
walkLeafRenderer,
|
||||
_walkWorldData);
|
||||
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
{
|
||||
|
|
@ -527,7 +537,8 @@ public sealed class RetailPViewRenderer
|
|||
pvFrame,
|
||||
clipAssembly,
|
||||
viewcone,
|
||||
_lookInFrames,
|
||||
walkActive ? walkDriver!.LookInCellTurns : [],
|
||||
walkActive ? walkDriver : null,
|
||||
outsideStageFlood,
|
||||
ctx.Cells,
|
||||
ctx.AnimatedEntityIds,
|
||||
|
|
@ -539,6 +550,8 @@ public sealed class RetailPViewRenderer
|
|||
frameViewBorrowed = true;
|
||||
frameEntityPasses!.BeginEntityFrame(in frameView);
|
||||
entityFrameOpen = true;
|
||||
if (walkActive)
|
||||
walkLeafRenderer!.BindEntityFrame(in frameView);
|
||||
}
|
||||
|
||||
// The retained scene product is the production object source.
|
||||
|
|
@ -651,6 +664,7 @@ public sealed class RetailPViewRenderer
|
|||
clipAssembly,
|
||||
capturedPartition,
|
||||
capturedViewcone,
|
||||
capturedDriver,
|
||||
capturedPasses,
|
||||
in capturedView);
|
||||
};
|
||||
|
|
@ -666,6 +680,7 @@ public sealed class RetailPViewRenderer
|
|||
clipAssembly,
|
||||
partition,
|
||||
viewcone,
|
||||
walkDriver!,
|
||||
frameEntityPasses,
|
||||
in frameView);
|
||||
}
|
||||
|
|
@ -1404,7 +1419,7 @@ public sealed class RetailPViewRenderer
|
|||
_cellParticleOwnerScratch.Clear();
|
||||
foreach (uint cellId in driver.VisitedCells)
|
||||
{
|
||||
if (_lookInCellIds.Contains(cellId))
|
||||
if (driver.LookInCells.Contains(cellId))
|
||||
continue;
|
||||
UnionRecordOwners(_walkWorldData!.GetCellStatics(cellId), _cellParticleOwnerScratch);
|
||||
}
|
||||
|
|
@ -1431,15 +1446,16 @@ public sealed class RetailPViewRenderer
|
|||
/// split — the walk now draws every STATIC route (see
|
||||
/// <see cref="DrawWalkDrivenStatics"/>); this method keeps ONLY what
|
||||
/// stays on the OLD visibility pipeline per the plan's dual-compute
|
||||
/// split: outdoor-cell unattached particles, LookInObject dynamics + their
|
||||
/// per-cell particles, the late per-slice outside-dynamics/weather loop,
|
||||
/// and the late particle union submission.</summary>
|
||||
/// split: outdoor-cell unattached particles, the late per-slice outside-
|
||||
/// dynamics/weather loop, and the late particle union submission. Look-in
|
||||
/// dynamics and their cell particles are now walk-turn-owned.</summary>
|
||||
private void DrawLandscapeDynamicsPhase(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
ViewconeCuller viewcone,
|
||||
Walk.WalkFrameDriver walkDriver,
|
||||
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||
in RenderFrameView frameView)
|
||||
{
|
||||
|
|
@ -1452,8 +1468,10 @@ public sealed class RetailPViewRenderer
|
|||
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
|
||||
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
|
||||
|
||||
DrawBuildingLookInDynamics(
|
||||
ctx, passes, clipAssembly, partition, frameEntityPasses, in frameView);
|
||||
// Trace-only now. Look-in dynamics no longer have a late production
|
||||
// phase: WalkFrameDriver replays each packed route at the walk's own
|
||||
// per-cell DrawCells turn, before the enclosing building shell.
|
||||
ProbeBuildingLookInFrames(ctx, passes, walkDriver);
|
||||
|
||||
// LATE phase (per slice): outside-stage dynamics' meshes + weather —
|
||||
// unchanged from DrawLandscapeThroughOutsideView's own late loop.
|
||||
|
|
@ -1527,129 +1545,72 @@ public sealed class RetailPViewRenderer
|
|||
passes.UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
|
||||
/// <see cref="DrawBuildingLookIns"/> — punches, shells, and look-in cell
|
||||
/// STATICS are now walk-owned (<see cref="Walk.WalkFrameDriver"/>'s
|
||||
/// Building/BuildingShell/LookInStatic turns); this method keeps ONLY the
|
||||
/// LookInObject route (now dynamic-classified — see
|
||||
/// <c>RenderScenePViewFrameBuilder.BuildLookInRoutes</c>) and the per-cell
|
||||
/// particle union that route's owners feed, unioned with the walk's
|
||||
/// static owners for that SAME cell (plan §FW3 item 4 — GetCellStatics
|
||||
/// fills the gap the retired CellStatic-route particle submission left
|
||||
/// for look-in cells specifically).</summary>
|
||||
private void DrawBuildingLookInDynamics(
|
||||
/// <summary>ACDREAM_PROBE_WALK_ROOT companion for FW4's surviving
|
||||
/// through-wall dynamic. It compares the now-diagnostic-only legacy
|
||||
/// look-in frames with the production walk's exact look-in set and reports
|
||||
/// the two punch producers separately. Print-only; never participates in
|
||||
/// admission.</summary>
|
||||
private void ProbeBuildingLookInFrames(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||
in RenderFrameView frameView)
|
||||
Walk.WalkFrameDriver walkDriver)
|
||||
{
|
||||
if (_lookInFrames.Count == 0)
|
||||
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
|
||||
return;
|
||||
|
||||
int lookInRouteIndex = 0;
|
||||
for (int frameIndex = 0; frameIndex < _lookInFrames.Count; frameIndex++)
|
||||
uint[] walkLookInCells = walkDriver.LookInCells
|
||||
.OrderBy(cellId => cellId)
|
||||
.ToArray();
|
||||
bool legacyHasTarget = _lookInFrames.Any(
|
||||
frame => frame.OrderedVisibleCells.Contains(ProbeCathedralRemoteCellId));
|
||||
bool walkHasTarget = Array.IndexOf(
|
||||
walkLookInCells, ProbeCathedralRemoteCellId) >= 0;
|
||||
string legacyShape = string.Join(
|
||||
";",
|
||||
_lookInFrames.Select((frame, index) =>
|
||||
$"{index}:b{frame.SourceBuildingKey:x8}/lb{frame.SourceBuildingLandblockId:x8}"
|
||||
+ $"/s{frame.ExteriorSeedPortals.Count}/c["
|
||||
+ string.Join(",", frame.OrderedVisibleCells.Select(cell => cell.ToString("x8")))
|
||||
+ "]"));
|
||||
string walkShape = string.Join(",", walkLookInCells.Select(cell => cell.ToString("x8")));
|
||||
int walkPunches = passes is RetailPViewPassExecutor concrete
|
||||
? concrete.WalkLookInPunchCountThisFrame
|
||||
: -1;
|
||||
int legacyPunches = passes is RetailPViewPassExecutor concreteLegacy
|
||||
? concreteLegacy.LegacyLookInPunchCountThisFrame
|
||||
: -1;
|
||||
string state =
|
||||
$"viewer={ctx.ViewerCellId:x8}|root={ctx.RootCell.CellId:x8}"
|
||||
+ $"|phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase}"
|
||||
+ $"|legacy={legacyShape}|walk={walkShape}"
|
||||
+ $"|p={walkPunches}/{legacyPunches}";
|
||||
bool emit = state != _probeLookInState || _probeWalkRootFrame % 30 == 0;
|
||||
_probeLookInState = state;
|
||||
if (!emit)
|
||||
return;
|
||||
|
||||
Console.WriteLine(
|
||||
$"[lookin-frame] f={_probeWalkRootFrame} "
|
||||
+ $"viewer=0x{ctx.ViewerCellId:X8} root=0x{ctx.RootCell.CellId:X8} "
|
||||
+ $"phase={AcDream.Core.Rendering.RenderingDiagnostics.WalkRootPhase} "
|
||||
+ $"legacyFrames={_lookInFrames.Count} legacy112={(legacyHasTarget ? 1 : 0)} "
|
||||
+ $"walkLookIn={walkLookInCells.Length} walk112={(walkHasTarget ? 1 : 0)} "
|
||||
+ $"punches=walk:{walkPunches},legacy:{legacyPunches}");
|
||||
for (int index = 0; index < _lookInFrames.Count; index++)
|
||||
{
|
||||
PortalVisibilityFrame frame = _lookInFrames[frameIndex];
|
||||
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = frame.OrderedVisibleCells[i];
|
||||
var clipKey = new LookInClipCell(frameIndex, cellId);
|
||||
if (!clipAssembly.LookInCellToViewSlices.TryGetValue(
|
||||
clipKey,
|
||||
out ClipViewSlice[]? cellSlices)
|
||||
|| cellSlices.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_cellStaticScratch.Clear();
|
||||
if (partition is not null)
|
||||
{
|
||||
foreach (var e in partition.Dynamics)
|
||||
if (e.ParentCellId == cellId)
|
||||
_cellStaticScratch.Add(e);
|
||||
}
|
||||
|
||||
bool cellDrewObjects = false;
|
||||
_cellParticleUnionScratch.Clear();
|
||||
foreach (ClipViewSlice slice in cellSlices)
|
||||
{
|
||||
int routeIndex = lookInRouteIndex++;
|
||||
passes.UseCellPortalViewRouting(cellId, slice);
|
||||
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_cellParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceOwnerIds(
|
||||
_cellParticleOwnerScratch,
|
||||
_cellStaticScratch);
|
||||
}
|
||||
|
||||
if (frameEntityPasses is not null
|
||||
|| _cellStaticScratch.Count > 0)
|
||||
{
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId,
|
||||
_cellStaticScratch);
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
DrawEntityRouteOrLegacy(
|
||||
ctx,
|
||||
passes,
|
||||
frameEntityPasses,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId,
|
||||
_cellStaticScratch,
|
||||
_oneCell);
|
||||
|
||||
cellDrewObjects = true;
|
||||
_cellParticleUnionScratch.UnionWith(
|
||||
_cellParticleOwnerScratch);
|
||||
}
|
||||
}
|
||||
|
||||
// The walk already drew this cell's STATIC content
|
||||
// (WalkFrameDriver's LookInStatic turn) but never submits
|
||||
// particles for it — GetCellStatics fills that gap, unioned
|
||||
// with the dynamic route's own owners so ONE
|
||||
// DrawCellParticles call covers both.
|
||||
if (_walkWorldData is not null)
|
||||
{
|
||||
Walk.WalkFrameStaticRecords statics =
|
||||
_walkWorldData.GetCellStatics(cellId);
|
||||
foreach (RenderProjectionRecord record in statics.Records)
|
||||
{
|
||||
if (record.Source.LocalEntityId != 0)
|
||||
{
|
||||
_cellParticleUnionScratch.Add(record.Source.LocalEntityId);
|
||||
cellDrewObjects = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cellDrewObjects)
|
||||
{
|
||||
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
|
||||
cellId, NoClipSlice, _cellParticleUnionScratch));
|
||||
}
|
||||
}
|
||||
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
PortalVisibilityFrame frame = _lookInFrames[index];
|
||||
Console.WriteLine(
|
||||
$"[lookin-frame] f={_probeWalkRootFrame} index={index} "
|
||||
+ $"building=0x{frame.SourceBuildingKey:X8} "
|
||||
+ $"landblock=0x{frame.SourceBuildingLandblockId:X8} "
|
||||
+ $"seeds={frame.ExteriorSeedPortals.Count} "
|
||||
+ $"has112={(frame.OrderedVisibleCells.Contains(ProbeCathedralRemoteCellId) ? 1 : 0)} "
|
||||
+ $"cells=[{string.Join(",", frame.OrderedVisibleCells.Select(cell => $"0x{cell:X8}"))}]");
|
||||
}
|
||||
Console.WriteLine(
|
||||
$"[lookin-frame] f={_probeWalkRootFrame} walkCells=["
|
||||
+ string.Join(",", walkLookInCells.Select(cell => $"0x{cell:X8}"))
|
||||
+ "]");
|
||||
}
|
||||
|
||||
private void DrawLandscapeThroughOutsideView(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
|
|
@ -173,6 +174,13 @@ internal readonly struct RenderFrameView
|
|||
|
||||
public ClipFrameAssembly? ClipAssembly => Arena.ClipAssembly;
|
||||
|
||||
/// <summary>
|
||||
/// Exact portal-view cones captured by the retail frame walk for each
|
||||
/// building look-in turn. The packed dispatcher consumes these at
|
||||
/// per-GfxObj granularity, matching RenderDeviceD3D::DrawMesh.
|
||||
/// </summary>
|
||||
public IWalkLookInViewSource? WalkLookInViews => Arena.WalkLookInViews;
|
||||
|
||||
public RenderFrameDiagnosticCounts DiagnosticCounts =>
|
||||
Arena.DiagnosticCounts;
|
||||
|
||||
|
|
@ -247,6 +255,8 @@ internal sealed class RenderFrameArena
|
|||
|
||||
public ClipFrameAssembly? ClipAssembly { get; private set; }
|
||||
|
||||
public IWalkLookInViewSource? WalkLookInViews { get; private set; }
|
||||
|
||||
public RenderSceneDigest SourceDigest { get; private set; }
|
||||
|
||||
public RenderFrameDiagnosticCounts DiagnosticCounts =>
|
||||
|
|
@ -328,6 +338,7 @@ internal sealed class RenderFrameArena
|
|||
_alphaClassificationCount = 0;
|
||||
PortalFrame = null;
|
||||
ClipAssembly = null;
|
||||
WalkLookInViews = null;
|
||||
SourceDigest = default;
|
||||
_sourceDigestSet = false;
|
||||
Generation = generation;
|
||||
|
|
@ -341,11 +352,13 @@ internal sealed class RenderFrameArena
|
|||
internal void SetBorrowedProducts(
|
||||
ulong epoch,
|
||||
PortalVisibilityFrame? portalFrame,
|
||||
ClipFrameAssembly? clipAssembly)
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
IWalkLookInViewSource? walkLookInViews)
|
||||
{
|
||||
EnsureBuilding(epoch);
|
||||
PortalFrame = portalFrame;
|
||||
ClipAssembly = clipAssembly;
|
||||
WalkLookInViews = walkLookInViews;
|
||||
}
|
||||
|
||||
internal void SetSourceDigest(ulong epoch, in RenderSceneDigest digest)
|
||||
|
|
@ -549,6 +562,7 @@ internal sealed class RenderFrameArena
|
|||
ClearReferenceStorage();
|
||||
PortalFrame = null;
|
||||
ClipAssembly = null;
|
||||
WalkLookInViews = null;
|
||||
SourceDigest = default;
|
||||
_sourceDigestSet = false;
|
||||
_state = ArenaState.Available;
|
||||
|
|
@ -655,8 +669,13 @@ internal readonly struct RenderFrameWriter
|
|||
|
||||
public void SetBorrowedProducts(
|
||||
PortalVisibilityFrame? portalFrame,
|
||||
ClipFrameAssembly? clipAssembly) =>
|
||||
Arena.SetBorrowedProducts(_epoch, portalFrame, clipAssembly);
|
||||
ClipFrameAssembly? clipAssembly,
|
||||
IWalkLookInViewSource? walkLookInViews = null) =>
|
||||
Arena.SetBorrowedProducts(
|
||||
_epoch,
|
||||
portalFrame,
|
||||
clipAssembly,
|
||||
walkLookInViews);
|
||||
|
||||
public void SetSourceDigest(in RenderSceneDigest digest) =>
|
||||
Arena.SetSourceDigest(_epoch, in digest);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
|
|
@ -9,7 +10,8 @@ internal readonly record struct RenderScenePViewBuildInput(
|
|||
PortalVisibilityFrame PortalFrame,
|
||||
ClipFrameAssembly ClipAssembly,
|
||||
ViewconeCuller Viewcone,
|
||||
IReadOnlyList<PortalVisibilityFrame> LookInFrames,
|
||||
IReadOnlyList<uint> LookInCellTurns,
|
||||
IWalkLookInViewSource? WalkLookInViews,
|
||||
HashSet<uint> DrawableCells,
|
||||
IRetailPViewCellSource Cells,
|
||||
HashSet<uint>? AnimatedEntityIds,
|
||||
|
|
@ -185,7 +187,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
PortalVisibilityFrame portalFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
ViewconeCuller viewcone,
|
||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||
IReadOnlyList<uint> lookInCellTurns,
|
||||
IWalkLookInViewSource? walkLookInViews,
|
||||
HashSet<uint> drawableCells,
|
||||
IRetailPViewCellSource cells,
|
||||
HashSet<uint>? animatedEntityIds,
|
||||
|
|
@ -195,7 +198,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
ArgumentNullException.ThrowIfNull(portalFrame);
|
||||
ArgumentNullException.ThrowIfNull(clipAssembly);
|
||||
ArgumentNullException.ThrowIfNull(viewcone);
|
||||
ArgumentNullException.ThrowIfNull(lookInFrames);
|
||||
ArgumentNullException.ThrowIfNull(lookInCellTurns);
|
||||
ArgumentNullException.ThrowIfNull(drawableCells);
|
||||
ArgumentNullException.ThrowIfNull(cells);
|
||||
|
||||
|
|
@ -213,7 +216,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
portalFrame,
|
||||
clipAssembly,
|
||||
viewcone,
|
||||
lookInFrames,
|
||||
lookInCellTurns,
|
||||
walkLookInViews,
|
||||
drawableCells,
|
||||
cells,
|
||||
animatedEntityIds,
|
||||
|
|
@ -248,7 +252,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
portalFrame,
|
||||
clipAssembly,
|
||||
viewcone,
|
||||
lookInFrames,
|
||||
FlattenLegacyLookInCells(lookInFrames),
|
||||
walkLookInViews: null,
|
||||
drawableCells,
|
||||
cells,
|
||||
animatedEntityIds,
|
||||
|
|
@ -267,6 +272,19 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
}
|
||||
}
|
||||
|
||||
private static uint[] FlattenLegacyLookInCells(
|
||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames)
|
||||
{
|
||||
var cells = new List<uint>();
|
||||
for (int frameIndex = 0; frameIndex < lookInFrames.Count; frameIndex++)
|
||||
{
|
||||
PortalVisibilityFrame frame = lookInFrames[frameIndex];
|
||||
for (int index = frame.OrderedVisibleCells.Count - 1; index >= 0; index--)
|
||||
cells.Add(frame.OrderedVisibleCells[index]);
|
||||
}
|
||||
return cells.ToArray();
|
||||
}
|
||||
|
||||
public void CompleteProduction(in RenderFrameView view)
|
||||
{
|
||||
_packedClassification =
|
||||
|
|
@ -1171,7 +1189,8 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
{
|
||||
writer.SetBorrowedProducts(
|
||||
input.PortalFrame,
|
||||
input.ClipAssembly);
|
||||
input.ClipAssembly,
|
||||
input.WalkLookInViews);
|
||||
RenderSceneDigest digest = input.SourceDigest;
|
||||
writer.SetSourceDigest(in digest);
|
||||
_projectionIds.Clear();
|
||||
|
|
@ -1182,19 +1201,11 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
// outdoor static, building shell, and cell static (including
|
||||
// look-in cell statics) directly through OrderedDrawStream (plan
|
||||
// §FW3 "FW3.2b-2 — the production rooting", item 3). LookInObject
|
||||
// keeps emitting, but BuildLookInRoutes below is now filtered to
|
||||
// DYNAMIC candidates only — the walk owns that route's statics.
|
||||
int lookInRouteIndex = 0;
|
||||
for (int frameIndex = 0;
|
||||
frameIndex < input.LookInFrames.Count;
|
||||
frameIndex++)
|
||||
{
|
||||
BuildLookInRoutes(
|
||||
writer,
|
||||
in input,
|
||||
frameIndex,
|
||||
ref lookInRouteIndex);
|
||||
}
|
||||
// keeps the packed classifier for live animation/fades, but its
|
||||
// route ranges are keyed to THE WALK'S own look-in cell turns.
|
||||
// The legacy PortalVisibilityFrame list no longer has a production
|
||||
// admission or ordering role.
|
||||
BuildLookInRoutes(writer, in input);
|
||||
BuildOutsideDynamicRoutes(writer, in input);
|
||||
BuildDynamicLastRoute(writer, in input);
|
||||
writer.Publish();
|
||||
|
|
@ -1284,45 +1295,37 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
/// item 3).</summary>
|
||||
private void BuildLookInRoutes(
|
||||
RenderFrameWriter writer,
|
||||
in RenderScenePViewBuildInput input,
|
||||
int frameIndex,
|
||||
ref int routeIndex)
|
||||
in RenderScenePViewBuildInput input)
|
||||
{
|
||||
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
|
||||
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
for (int routeIndex = 0; routeIndex < input.LookInCellTurns.Count; routeIndex++)
|
||||
{
|
||||
uint cellId = frame.OrderedVisibleCells[i];
|
||||
var clipKey = new LookInClipCell(frameIndex, cellId);
|
||||
if (!input.ClipAssembly.LookInCellToViewSlices.TryGetValue(
|
||||
clipKey,
|
||||
out ClipViewSlice[]? slices)
|
||||
|| slices.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint cellId = input.LookInCellTurns[routeIndex];
|
||||
int count = LoadCell(
|
||||
input.Scene,
|
||||
cellId,
|
||||
includeStatics: false,
|
||||
includeDynamics: true);
|
||||
for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++)
|
||||
{
|
||||
int currentRouteIndex = routeIndex++;
|
||||
if (count == 0)
|
||||
continue;
|
||||
if (count == 0)
|
||||
continue;
|
||||
|
||||
for (int index = 0; index < count; index++)
|
||||
AddProjection(
|
||||
writer,
|
||||
in _cell[index],
|
||||
input.AnimatedEntityIds);
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
currentRouteIndex,
|
||||
cellId,
|
||||
_cell.AsSpan(0, count));
|
||||
int survivorCount = 0;
|
||||
EnsureCapacity(ref _survivors, count);
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
RenderProjectionRecord record = _cell[index];
|
||||
_survivors[survivorCount++] = record;
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
input.AnimatedEntityIds);
|
||||
}
|
||||
if (survivorCount == 0)
|
||||
continue;
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId,
|
||||
_survivors.AsSpan(0, survivorCount));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1384,14 +1387,8 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
in RenderScenePViewBuildInput input)
|
||||
{
|
||||
_lookInCellScratch.Clear();
|
||||
for (int frameIndex = 0;
|
||||
frameIndex < input.LookInFrames.Count;
|
||||
frameIndex++)
|
||||
{
|
||||
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
|
||||
for (int i = 0; i < frame.OrderedVisibleCells.Count; i++)
|
||||
_lookInCellScratch.Add(frame.OrderedVisibleCells[i]);
|
||||
}
|
||||
for (int index = 0; index < input.LookInCellTurns.Count; index++)
|
||||
_lookInCellScratch.Add(input.LookInCellTurns[index]);
|
||||
|
||||
int count = 0;
|
||||
EnsureCapacity(ref _survivors, _dynamicCount);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
|
|
@ -118,6 +119,17 @@ internal interface IWalkFrameLeafRenderer
|
|||
/// positional invariant this carries (the #132 falls containment).</summary>
|
||||
void DrawStaticParticles(IReadOnlySet<uint> ownerIds);
|
||||
|
||||
/// <summary>Draws the packed dynamic occupants of one building look-in
|
||||
/// cell at that cell's OWN <c>PView::DrawCells</c> turn, then submits the
|
||||
/// cell's static + dynamic particle owners at the same turn. The route
|
||||
/// index is assigned in the exact order Collect encountered look-in cell
|
||||
/// turns and therefore matches the frame product's walk-keyed
|
||||
/// <c>LookInObject</c> ranges.</summary>
|
||||
void DrawLookInDynamics(
|
||||
uint cellId,
|
||||
int routeIndex,
|
||||
IReadOnlySet<uint> staticParticleOwnerIds);
|
||||
|
||||
/// <summary><c>PView::DrawCells</c> @0x005a4840's gated full depth clear
|
||||
/// (pc:432731-432732) between the outside stage and the interior root's
|
||||
/// own flood — production maps this to <c>IWorldPassScope.ClearInteriorDepth</c>
|
||||
|
|
@ -259,8 +271,37 @@ internal enum WalkFrameEventKind : byte
|
|||
/// cathedral bleed; the old pipeline's user-verified #132 fix
|
||||
/// `e102fb36` encoded the same invariant).</summary>
|
||||
StaticParticles,
|
||||
|
||||
/// <summary><see cref="IWalkFrameLeafRenderer.DrawLookInDynamics"/> —
|
||||
/// <see cref="WalkFrameEvent.CellId"/> is the look-in cell and
|
||||
/// <see cref="WalkFrameEvent.IntArg"/> is its walk-ordered packed route
|
||||
/// index.</summary>
|
||||
LookInDynamics,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The exact portal-view cones installed at the walk's building look-in
|
||||
/// <c>DrawCells</c> turns. Retail <c>RenderDeviceD3D::DrawMesh</c>
|
||||
/// @0x005A0860 tests each object's drawing sphere against these views before
|
||||
/// drawing the mesh whole; cell membership alone is not an admission rule.
|
||||
/// </summary>
|
||||
internal interface IWalkLookInViewSource
|
||||
{
|
||||
IReadOnlyList<uint> LookInCellTurns { get; }
|
||||
|
||||
bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius);
|
||||
}
|
||||
|
||||
internal readonly record struct WalkLookInSlice(int PlaneStart, int PlaneCount);
|
||||
|
||||
internal readonly record struct WalkLookInTurn(
|
||||
uint CellId,
|
||||
int SliceStart,
|
||||
int SliceCount);
|
||||
|
||||
/// <summary>See <see cref="WalkFrameEventKind"/> for what each field means per
|
||||
/// kind. A single struct (rather than a kind hierarchy) keeps Collect's
|
||||
/// per-turn list a flat, allocation-cheap <c>List<WalkFrameEvent></c> —
|
||||
|
|
@ -320,6 +361,9 @@ internal readonly struct WalkFrameEvent
|
|||
internal static WalkFrameEvent BuildingShellParticles(WalkBuilding building) =>
|
||||
new(WalkFrameEventKind.StaticParticles, 0, 0, 0f, null, building);
|
||||
|
||||
internal static WalkFrameEvent LookInDynamics(uint cellId, int routeIndex) =>
|
||||
new(WalkFrameEventKind.LookInDynamics, routeIndex, cellId, 0f, null);
|
||||
|
||||
internal static WalkFrameEvent ClearInteriorDepth() =>
|
||||
new(WalkFrameEventKind.ClearInteriorDepth, 0, 0, 0f, null);
|
||||
|
||||
|
|
@ -404,7 +448,7 @@ internal readonly struct WalkFrameEvent
|
|||
/// @0x0059f2a0 (the <c>part->gfxobj[deg_level]!=0</c> gate @0x0059f2d3
|
||||
/// and the alpha-barrier → portal-pass → shell order @0x0059f30b–0x0059f345).</para>
|
||||
/// </summary>
|
||||
internal sealed class WalkFrameDriver : IWalkEventSink
|
||||
internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
||||
{
|
||||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly WalkStaticStreamPopulator _populator;
|
||||
|
|
@ -422,6 +466,23 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
// walk pass dedicated to nothing but set-gathering.
|
||||
internal HashSet<uint> VisitedCells { get; } = new();
|
||||
|
||||
/// <summary>Building look-in cells in the exact order the walk encountered
|
||||
/// their <c>DrawCells</c> turns. Duplicates are intentional: two authored
|
||||
/// portal views can independently visit the same cell and therefore own
|
||||
/// distinct packed route indices.</summary>
|
||||
internal List<uint> LookInCellTurns { get; } = new();
|
||||
|
||||
private readonly List<WalkLookInTurn> _lookInTurns = new();
|
||||
private readonly List<WalkLookInSlice> _lookInSlices = new();
|
||||
private readonly List<WalkPlane> _lookInPlanes = new();
|
||||
private WalkPlane _lookInCyPlane;
|
||||
|
||||
IReadOnlyList<uint> IWalkLookInViewSource.LookInCellTurns => LookInCellTurns;
|
||||
|
||||
/// <summary>The set form of <see cref="LookInCellTurns"/>, for drawn-once
|
||||
/// exclusion and root-flood particle bookkeeping.</summary>
|
||||
internal HashSet<uint> LookInCells { get; } = new();
|
||||
|
||||
/// <summary>FW4 slice 2: the interior root's ORDERED flood cell list,
|
||||
/// exactly as retail's <c>PView::DrawCells</c> iterates it for the
|
||||
/// exit-portal seals (pc:432785-432786) — captured at
|
||||
|
|
@ -459,6 +520,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
private bool _skyDrawnThisFrame;
|
||||
private WalkDrawStage? _currentDcStage;
|
||||
private bool _readyToReplay;
|
||||
private int _lookInRouteIndex;
|
||||
|
||||
internal WalkFrameDriver(
|
||||
WbDrawDispatcher dispatcher,
|
||||
|
|
@ -534,7 +596,7 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
/// GPU work — see this type's own doc comment.
|
||||
/// </summary>
|
||||
internal void BeginFrame(
|
||||
IWalkBuildingFrameContext ctx,
|
||||
IRetailFrameWalkContext ctx,
|
||||
Matrix4x4 viewProjection,
|
||||
Vector3 cameraWorldPosition)
|
||||
{
|
||||
|
|
@ -557,9 +619,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
_events.Clear();
|
||||
_markPositions.Clear();
|
||||
VisitedCells.Clear();
|
||||
LookInCellTurns.Clear();
|
||||
_lookInTurns.Clear();
|
||||
_lookInSlices.Clear();
|
||||
_lookInPlanes.Clear();
|
||||
_lookInCyPlane = ctx.CyPlane;
|
||||
LookInCells.Clear();
|
||||
VisitedBuildings.Clear();
|
||||
VisitedLandscapeCellIds.Clear();
|
||||
InteriorFloodCells.Clear();
|
||||
_lookInRouteIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Records the final segment mark (plan §FW3.2b-1's "at frame
|
||||
|
|
@ -655,6 +724,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
if (_staticParticleOwnerScratch.Count > 0)
|
||||
_leafRenderer.DrawStaticParticles(_staticParticleOwnerScratch);
|
||||
break;
|
||||
case WalkFrameEventKind.LookInDynamics:
|
||||
_staticParticleOwnerScratch.Clear();
|
||||
UnionOwners(
|
||||
_worldData.GetCellStatics(e.CellId),
|
||||
_staticParticleOwnerScratch);
|
||||
_leafRenderer.DrawLookInDynamics(
|
||||
e.CellId,
|
||||
e.IntArg,
|
||||
_staticParticleOwnerScratch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -883,6 +962,76 @@ internal sealed class WalkFrameDriver : IWalkEventSink
|
|||
_populator.PopulateCell(
|
||||
_stream, stage, cellId, records.Records, records.TupleLandblockId,
|
||||
_cameraWorldPosition, _viewProjection);
|
||||
if (stage == WalkDrawStage.LookInStatic)
|
||||
{
|
||||
// Retail draws a look-in cell's complete object list at this
|
||||
// re-entrant DrawCells turn. The packed dynamic route used to run
|
||||
// much later at the pre-clear boundary, after nearer building
|
||||
// shells, which let the cathedral's 0x112 remote player overpaint
|
||||
// opaque walls. Keep animation/fade in the packed route, but replay
|
||||
// it here between this cell's content and the building shell.
|
||||
MarkIfGrown();
|
||||
int routeIndex = _lookInRouteIndex++;
|
||||
LookInCellTurns.Add(cellId);
|
||||
LookInCells.Add(cellId);
|
||||
CaptureLookInViews(cellId);
|
||||
_events.Add(WalkFrameEvent.LookInDynamics(cellId, routeIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureLookInViews(uint cellId)
|
||||
{
|
||||
IWalkBuildingFrameContext ctx = RequireOpenFrame();
|
||||
WalkCell? cell = ctx.GetVisible(cellId);
|
||||
if (cell is null || cell.NumView <= 0)
|
||||
{
|
||||
_lookInTurns.Add(new WalkLookInTurn(
|
||||
cellId, _lookInSlices.Count, 0));
|
||||
return;
|
||||
}
|
||||
|
||||
WalkPortalView portalView = cell.TopView;
|
||||
int sliceStart = _lookInSlices.Count;
|
||||
for (int sliceIndex = 0; sliceIndex < portalView.ViewCount; sliceIndex++)
|
||||
{
|
||||
WalkViewPoly poly = portalView.View.Polys[sliceIndex];
|
||||
int planeStart = _lookInPlanes.Count;
|
||||
for (int edge = 0; edge < poly.VertexCount; edge++)
|
||||
{
|
||||
_lookInPlanes.Add(
|
||||
portalView.View.Vertices[poly.VertexIndex + edge].Plane);
|
||||
}
|
||||
_lookInSlices.Add(new WalkLookInSlice(
|
||||
planeStart, poly.VertexCount));
|
||||
}
|
||||
_lookInTurns.Add(new WalkLookInTurn(
|
||||
cellId, sliceStart, _lookInSlices.Count - sliceStart));
|
||||
}
|
||||
|
||||
public bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius)
|
||||
{
|
||||
if ((uint)routeIndex >= (uint)_lookInTurns.Count)
|
||||
return false;
|
||||
|
||||
WalkLookInTurn turn = _lookInTurns[routeIndex];
|
||||
for (int sliceOffset = 0; sliceOffset < turn.SliceCount; sliceOffset++)
|
||||
{
|
||||
WalkLookInSlice slice = _lookInSlices[turn.SliceStart + sliceOffset];
|
||||
if (WalkVisibilityMath.ViewconeCheck(
|
||||
center,
|
||||
radius,
|
||||
_lookInCyPlane,
|
||||
CollectionsMarshal.AsSpan(_lookInPlanes).Slice(
|
||||
slice.PlaneStart,
|
||||
slice.PlaneCount)) != WalkBoundingType.Outside)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Campaign FW3.4a: the collect-time analogue of the old
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using AcDream.Core.Lighting;
|
|||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
|
@ -40,6 +41,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
private RenderSceneGeneration _packedProductionGeneration;
|
||||
private ulong _packedProductionFrameSequence;
|
||||
private int _packedProductionNextRange;
|
||||
private readonly Dictionary<(int RouteIndex, uint LocalEntityId, int PartIndex, uint GfxObjId), string>
|
||||
_probeLookInPartStates = [];
|
||||
|
||||
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
|
||||
PackedDispatcherSubmissions => _packedSubmissions;
|
||||
|
|
@ -295,7 +298,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
source.MeshPartOffset,
|
||||
source.MeshPartCount),
|
||||
ref anyVao,
|
||||
publishSelection);
|
||||
publishSelection,
|
||||
range.Route is RenderFrameCandidateRoute.LookInObject
|
||||
? view.WalkLookInViews
|
||||
: null,
|
||||
range.RouteIndex,
|
||||
range.CellId);
|
||||
}
|
||||
|
||||
return new PackedRangeClassification(
|
||||
|
|
@ -334,7 +342,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
in RenderInstanceCandidate entity,
|
||||
ReadOnlySpan<RenderFrameMeshPart> meshParts,
|
||||
ref uint anyVao,
|
||||
bool publishSelection)
|
||||
bool publishSelection,
|
||||
IWalkLookInViewSource? lookInViews,
|
||||
int lookInRouteIndex,
|
||||
uint lookInCellId)
|
||||
{
|
||||
(uint slot, bool culled) = ResolveSlotForFrame(
|
||||
_clipRoutingActive,
|
||||
|
|
@ -361,7 +372,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
: new Vector2(0f, 1f);
|
||||
|
||||
PackedProjectionClassificationEntry? cacheEntry = null;
|
||||
if (!entity.Animated)
|
||||
bool lookInConeActive = lookInViews is not null;
|
||||
if (!entity.Animated && !lookInConeActive)
|
||||
{
|
||||
PackedClassificationIdentity identity =
|
||||
PackedClassificationIdentity.From(in projection);
|
||||
|
|
@ -394,7 +406,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
entity.ProjectionId,
|
||||
in identity);
|
||||
}
|
||||
else
|
||||
else if (entity.Animated)
|
||||
{
|
||||
_packedClassificationCache.RecordAnimatedClassification();
|
||||
}
|
||||
|
|
@ -485,6 +497,21 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
partTransform * meshRef.PartTransform;
|
||||
Matrix4x4 model =
|
||||
restPose * entity.RootWorld;
|
||||
int selectionPartIndex = unchecked(
|
||||
(partIndex << 16)
|
||||
| (setupPartIndex & 0xFFFF));
|
||||
if (!PartVisibleInLookInTurn(
|
||||
lookInViews,
|
||||
lookInRouteIndex,
|
||||
lookInCellId,
|
||||
in entity,
|
||||
selectionPartIndex,
|
||||
(uint)gfxObjId,
|
||||
partData,
|
||||
model))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!ClassifyPackedBatches(
|
||||
partData,
|
||||
restPose,
|
||||
|
|
@ -502,9 +529,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
{
|
||||
reusableAcrossFrames = false;
|
||||
}
|
||||
int selectionPartIndex = unchecked(
|
||||
(partIndex << 16)
|
||||
| (setupPartIndex & 0xFFFF));
|
||||
cacheEntry?.SelectionParts.Add(
|
||||
new PackedClassifiedSelectionPart(
|
||||
selectionPartIndex,
|
||||
|
|
@ -538,6 +562,18 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
|
||||
Matrix4x4 restPose = meshRef.PartTransform;
|
||||
Matrix4x4 model = restPose * entity.RootWorld;
|
||||
if (!PartVisibleInLookInTurn(
|
||||
lookInViews,
|
||||
lookInRouteIndex,
|
||||
lookInCellId,
|
||||
in entity,
|
||||
partIndex,
|
||||
meshRef.GfxObjId,
|
||||
renderData,
|
||||
model))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!ClassifyPackedBatches(
|
||||
renderData,
|
||||
restPose,
|
||||
|
|
@ -576,6 +612,97 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail RenderDeviceD3D::DrawMesh @0x005A0860 admits each CGfxObj
|
||||
/// independently by transforming its drawing sphere into the active
|
||||
/// portal view. A whole-entity sphere is not equivalent for multipart
|
||||
/// creatures: the union can be roughly ten metres wide and intersect an
|
||||
/// aperture while every actual body/equipment part is behind its wall.
|
||||
/// </summary>
|
||||
private bool PartVisibleInLookInTurn(
|
||||
IWalkLookInViewSource? lookInViews,
|
||||
int routeIndex,
|
||||
uint cellId,
|
||||
in RenderInstanceCandidate entity,
|
||||
int partIndex,
|
||||
uint gfxObjId,
|
||||
ObjectRenderData renderData,
|
||||
Matrix4x4 localToWorld)
|
||||
{
|
||||
if (lookInViews is null)
|
||||
return true;
|
||||
|
||||
// ObjectRenderData.SelectionSphere is retained per GfxObj by the
|
||||
// prepared mesh payload. It is never the entity-union sphere and is
|
||||
// therefore the correct granularity for DrawMesh admission. The
|
||||
// current package stores a conservative vertex-derived sphere here;
|
||||
// this preserves whole-mesh drawing while avoiding the invalid
|
||||
// aggregate-character admission that caused the cathedral bleed.
|
||||
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
|
||||
return true;
|
||||
|
||||
bool visible = LookInDrawingSphereVisible(
|
||||
lookInViews,
|
||||
routeIndex,
|
||||
sphere,
|
||||
localToWorld,
|
||||
out Vector3 center,
|
||||
out float radius);
|
||||
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled
|
||||
&& cellId == 0xF4180112u)
|
||||
{
|
||||
var key = (routeIndex, entity.LocalEntityId, partIndex, gfxObjId);
|
||||
string state =
|
||||
$"visible={(visible ? 1 : 0)} "
|
||||
+ $"center=({center.X:F2},{center.Y:F2},{center.Z:F2}) "
|
||||
+ $"r={radius:F2}";
|
||||
if (!_probeLookInPartStates.TryGetValue(key, out string? previous)
|
||||
|| previous != state)
|
||||
{
|
||||
_probeLookInPartStates[key] = state;
|
||||
Console.WriteLine(
|
||||
$"[lookin-part] route={routeIndex} cell=0x{cellId:X8} "
|
||||
+ $"id={entity.LocalEntityId:x} part={partIndex} "
|
||||
+ $"gfx=0x{gfxObjId:X8} {state}");
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
internal static bool LookInDrawingSphereVisible(
|
||||
IWalkLookInViewSource lookInViews,
|
||||
int routeIndex,
|
||||
DatReaderWriter.Types.Sphere sphere,
|
||||
Matrix4x4 localToWorld,
|
||||
out Vector3 center,
|
||||
out float radius)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(lookInViews);
|
||||
ArgumentNullException.ThrowIfNull(sphere);
|
||||
|
||||
center = Vector3.Transform(sphere.Origin, localToWorld);
|
||||
float scaleX = new Vector3(
|
||||
localToWorld.M11,
|
||||
localToWorld.M12,
|
||||
localToWorld.M13).Length();
|
||||
float scaleY = new Vector3(
|
||||
localToWorld.M21,
|
||||
localToWorld.M22,
|
||||
localToWorld.M23).Length();
|
||||
float scaleZ = new Vector3(
|
||||
localToWorld.M31,
|
||||
localToWorld.M32,
|
||||
localToWorld.M33).Length();
|
||||
radius = sphere.Radius
|
||||
* MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
|
||||
return lookInViews.SphereVisibleInLookInTurn(
|
||||
routeIndex,
|
||||
in center,
|
||||
radius);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the production dispatcher's no-VAO early return. That return
|
||||
/// records an empty submission with transparent deferral disabled even
|
||||
|
|
|
|||
|
|
@ -3,12 +3,23 @@ using AcDream.App.Rendering;
|
|||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Scene.Arch;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class RenderScenePViewFrameProductTests
|
||||
{
|
||||
private sealed class RejectAllLookInViews(uint cellId) : IWalkLookInViewSource
|
||||
{
|
||||
public IReadOnlyList<uint> LookInCellTurns { get; } = [cellId];
|
||||
|
||||
public bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius) => false;
|
||||
}
|
||||
|
||||
private const uint Landblock = 0xA9B4FFFF;
|
||||
private const uint Cell = 0xA9B40170;
|
||||
private static readonly RenderSceneGeneration Generation =
|
||||
|
|
@ -159,6 +170,7 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
clip,
|
||||
viewcone,
|
||||
[],
|
||||
null,
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
[],
|
||||
|
|
@ -168,6 +180,134 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_KeysLookInDynamicsToWalkTurnsAndExcludesThemFromDynamicLast()
|
||||
{
|
||||
const uint lookInCell = 0xA9B4_0171;
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord rootDynamic = Record(
|
||||
0x0100_0000_0000_0021,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: Cell);
|
||||
RenderProjectionRecord lookInDynamic = Record(
|
||||
0x0100_0000_0000_0022,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: lookInCell);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(Generation, 1, rootDynamic),
|
||||
RenderProjectionDelta.Register(Generation, 2, lookInDynamic),
|
||||
]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||
ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
RenderSceneQuery query = scene.OpenQuery();
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
query,
|
||||
new RenderSceneDigest(query.Generation, query.Counts, default),
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
LookInCellTurns: [lookInCell],
|
||||
WalkLookInViews: null,
|
||||
DrawableCells: [Cell, lookInCell],
|
||||
Cells: EmptyCellSource.Instance,
|
||||
AnimatedEntityIds: [],
|
||||
RootIsOutdoor: false,
|
||||
RootFloodCells: new HashSet<uint> { Cell });
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView frame = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
RenderFrameCandidateRange lookInRange = Assert.Single(
|
||||
frame.RouteRanges.ToArray(),
|
||||
range => range.Route == RenderFrameCandidateRoute.LookInObject);
|
||||
Assert.Equal(0, lookInRange.RouteIndex);
|
||||
Assert.Equal(lookInCell, lookInRange.CellId);
|
||||
Assert.Equal(
|
||||
[lookInDynamic.Id],
|
||||
frame.RouteCandidates
|
||||
.Slice(lookInRange.Offset, lookInRange.Count)
|
||||
.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
|
||||
RenderFrameCandidateRange dynamicLast = Assert.Single(
|
||||
frame.RouteRanges.ToArray(),
|
||||
range => range.Route == RenderFrameCandidateRoute.DynamicLast);
|
||||
RenderProjectionId[] lastIds = frame.RouteCandidates
|
||||
.Slice(dynamicLast.Offset, dynamicLast.Count)
|
||||
.ToArray()
|
||||
.Select(static record => record.Id)
|
||||
.ToArray();
|
||||
Assert.Contains(rootDynamic.Id, lastIds);
|
||||
Assert.DoesNotContain(lookInDynamic.Id, lastIds);
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in frame);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Builder_CarriesExactWalkConeForPerPartDispatcherAdmission()
|
||||
{
|
||||
const uint lookInCell = 0xA9B4_0171;
|
||||
using var scene = new ArchRenderScene(Generation);
|
||||
RenderProjectionRecord lookInDynamic = Record(
|
||||
0x0100_0000_0000_0022,
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
parentCell: lookInCell);
|
||||
scene.Apply([RenderProjectionDelta.Register(Generation, 1, lookInDynamic)]);
|
||||
|
||||
PortalVisibilityFrame portal = Portal(Cell);
|
||||
ClipFrameAssembly clip = FullScreenClip(Cell);
|
||||
ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity);
|
||||
RenderSceneQuery query = scene.OpenQuery();
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
query,
|
||||
new RenderSceneDigest(query.Generation, query.Counts, default),
|
||||
portal,
|
||||
clip,
|
||||
viewcone,
|
||||
LookInCellTurns: [lookInCell],
|
||||
WalkLookInViews: new RejectAllLookInViews(lookInCell),
|
||||
DrawableCells: [Cell, lookInCell],
|
||||
Cells: EmptyCellSource.Instance,
|
||||
AnimatedEntityIds: [],
|
||||
RootIsOutdoor: false,
|
||||
RootFloodCells: new HashSet<uint> { Cell });
|
||||
var exchange = new RenderFrameExchange();
|
||||
var builder = new RenderScenePViewFrameBuilder();
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
RenderFrameView frame = exchange.BorrowLatest(Generation, 1);
|
||||
try
|
||||
{
|
||||
RenderFrameCandidateRange range = Assert.Single(
|
||||
frame.RouteRanges.ToArray(),
|
||||
candidate => candidate.Route == RenderFrameCandidateRoute.LookInObject);
|
||||
Assert.Equal(
|
||||
[lookInDynamic.Id],
|
||||
frame.RouteCandidates
|
||||
.Slice(range.Offset, range.Count)
|
||||
.ToArray()
|
||||
.Select(static record => record.Id));
|
||||
Assert.NotNull(frame.WalkLookInViews);
|
||||
Assert.False(frame.WalkLookInViews!.SphereVisibleInLookInTurn(
|
||||
routeIndex: 0,
|
||||
center: Vector3.Zero,
|
||||
radius: 1f));
|
||||
}
|
||||
finally
|
||||
{
|
||||
exchange.Release(in frame);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -67,6 +67,18 @@ public sealed class WalkFrameDriverTests
|
|||
sorted.Sort();
|
||||
log.Add($"PARTICLES:{string.Join(",", sorted.ConvertAll(o => o.ToString("x")))}");
|
||||
}
|
||||
|
||||
public void DrawLookInDynamics(
|
||||
uint cellId,
|
||||
int routeIndex,
|
||||
IReadOnlySet<uint> staticParticleOwnerIds)
|
||||
{
|
||||
var sorted = new List<uint>(staticParticleOwnerIds);
|
||||
sorted.Sort();
|
||||
log.Add(
|
||||
$"LOOKIN:{cellId:x8}:{routeIndex}:"
|
||||
+ string.Join(",", sorted.ConvertAll(o => o.ToString("x"))));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace
|
||||
|
|
@ -417,10 +429,28 @@ public sealed class WalkFrameDriverTests
|
|||
new[]
|
||||
{
|
||||
"ALPHA:12.50", "PUNCH:4@v0", "SHELL:00000104",
|
||||
"FLUSH:1:LookInStatic", "FLUSH:1:BuildingShell", "PARTICLES:c9",
|
||||
"FLUSH:1:LookInStatic", "LOOKIN:00000104:0:ca",
|
||||
"FLUSH:1:BuildingShell", "PARTICLES:c9",
|
||||
},
|
||||
log);
|
||||
|
||||
Assert.Equal([0x104u], driver.LookInCellTurns);
|
||||
Assert.Equal([0x104u], driver.LookInCells);
|
||||
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
|
||||
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
|
||||
Vector2 capturedCenter = Vector2.Zero;
|
||||
for (int edge = 0; edge < capturedPoly.VertexCount; edge++)
|
||||
{
|
||||
capturedCenter += capturedView.View.Vertices[
|
||||
capturedPoly.VertexIndex + edge].Point;
|
||||
}
|
||||
capturedCenter /= capturedPoly.VertexCount;
|
||||
Vector3 insideCone = ctx.Rays.RayThrough(capturedCenter.X, capturedCenter.Y);
|
||||
Assert.True(driver.SphereVisibleInLookInTurn(
|
||||
0, in insideCone, 0.1f));
|
||||
Assert.False(driver.SphereVisibleInLookInTurn(
|
||||
0, new Vector3(10_000f, 0f, 10f), 0.1f));
|
||||
|
||||
// The punch polygon reached the leaf renderer in WORLD space: the
|
||||
// building-local Quad(-2f) vertex (-0.5,-0.5,-2) translates by
|
||||
// (10,0,0) under the caller-supplied building world transform.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Wb;
|
||||
|
||||
public sealed class WbDrawDispatcherLookInConeTests
|
||||
{
|
||||
[Fact]
|
||||
public void DrawingSphereAdmissionUsesTheIndividualPartTransformAndLargestScale()
|
||||
{
|
||||
var cone = new RecordingLookInViews();
|
||||
var sphere = new Sphere
|
||||
{
|
||||
Origin = new Vector3(1f, 2f, 3f),
|
||||
Radius = 0.5f,
|
||||
};
|
||||
Matrix4x4 transform =
|
||||
Matrix4x4.CreateScale(2f, 3f, 4f)
|
||||
* Matrix4x4.CreateTranslation(10f, 20f, 30f);
|
||||
|
||||
bool visible = WbDrawDispatcher.LookInDrawingSphereVisible(
|
||||
cone,
|
||||
routeIndex: 7,
|
||||
sphere,
|
||||
transform,
|
||||
out Vector3 center,
|
||||
out float radius);
|
||||
|
||||
Assert.True(visible);
|
||||
Assert.Equal(7, cone.RouteIndex);
|
||||
Assert.Equal(new Vector3(12f, 26f, 42f), center);
|
||||
Assert.Equal(center, cone.Center);
|
||||
Assert.Equal(2f, radius);
|
||||
Assert.Equal(radius, cone.Radius);
|
||||
}
|
||||
|
||||
private sealed class RecordingLookInViews : IWalkLookInViewSource
|
||||
{
|
||||
public IReadOnlyList<uint> LookInCellTurns { get; } = [0xF4180112u];
|
||||
|
||||
public int RouteIndex { get; private set; }
|
||||
|
||||
public Vector3 Center { get; private set; }
|
||||
|
||||
public float Radius { get; private set; }
|
||||
|
||||
public bool SphereVisibleInLookInTurn(
|
||||
int routeIndex,
|
||||
in Vector3 center,
|
||||
float radius)
|
||||
{
|
||||
RouteIndex = routeIndex;
|
||||
Center = center;
|
||||
Radius = radius;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue