fix #451: stabilize portal seam rendering
This commit is contained in:
parent
f6fe0f2a4f
commit
1d2f2f738f
29 changed files with 1650 additions and 239 deletions
|
|
@ -50,8 +50,10 @@ public sealed class RetailPViewRenderer
|
|||
private readonly Stack<PortalVisibilityFrame> _lookInFramePool = new();
|
||||
private readonly HashSet<uint> _lookInPrepareScratch = new();
|
||||
|
||||
// #131/#132: the late landscape phase's scene-particle owner survivors
|
||||
// (statics + outside-stage dynamics passing the slice cone).
|
||||
// #131/#132: landscape scene-particle owner survivors. With building
|
||||
// look-ins, static owners use the pre-building alpha barrier and the late
|
||||
// phase contains only outside-stage dynamics; otherwise the late phase
|
||||
// carries both sets.
|
||||
private readonly HashSet<uint> _lateParticleOwnerScratch = new();
|
||||
private readonly HashSet<uint> _cellParticleOwnerScratch = new();
|
||||
private readonly HashSet<uint> _dynamicParticleOwnerScratch = new();
|
||||
|
|
@ -144,6 +146,7 @@ public sealed class RetailPViewRenderer
|
|||
var clipAssembly = passes.AssembleClipFrame(
|
||||
pvFrame,
|
||||
_clipAssemblyScratch);
|
||||
passes.AppendLookInClipFrames(_lookInFrames, clipAssembly);
|
||||
int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2);
|
||||
passes.PrepareClipFrame(terrainUploadCount);
|
||||
|
||||
|
|
@ -413,6 +416,9 @@ public sealed class RetailPViewRenderer
|
|||
group, ctx.ViewerEyePos, ctx.Cells.Find, ctx.ViewProjection,
|
||||
OutdoorBuildingSeedDistance, pvFrame.OutsideView.Polygons,
|
||||
reuseFrame: frameScratch);
|
||||
LoadedCell sourceCell = group[0];
|
||||
frame.SourceBuildingKey = sourceCell.BuildingId ?? sourceCell.CellId;
|
||||
frame.SourceBuildingLandblockId = sourceCell.CellId & 0xFFFF0000u;
|
||||
if (frame.OrderedVisibleCells.Count > 0)
|
||||
_lookInFrames.Add(frame);
|
||||
else
|
||||
|
|
@ -454,65 +460,67 @@ public sealed class RetailPViewRenderer
|
|||
// then draw the flooded cells' shells + statics far→near (the nested
|
||||
// DrawCells' DrawEnvCell + DrawObjCellForDummies; its outside_view is
|
||||
// empty by construction — PView ctor draw_landscape=0 — so no recursive
|
||||
// landscape/clear/seal). Anything rasterized outside an aperture is
|
||||
// repainted by the root's own shells after the depth clear, so over-draw
|
||||
// here is color-safe; statics draw whole (the main viewcone has no entry
|
||||
// for look-in cells; over-include is the safe direction).
|
||||
// landscape/clear/seal). Retail CEnvCell::setup_view installs every cell's
|
||||
// nested portal_view before DrawEnvCell, while DrawMesh iterates that same
|
||||
// PortalList for cell objects. Preserve that per-slice gate here; drawing a
|
||||
// nested cell whole lets its floor, details, and emitters escape the authored
|
||||
// aperture even when the outer depth choreography is otherwise correct.
|
||||
private void DrawBuildingLookIns(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
InteriorEntityPartition.Result? partition,
|
||||
ViewconeCuller viewcone,
|
||||
IRenderFrameEntityPassExecutor? frameEntityPasses,
|
||||
in RenderFrameView frameView)
|
||||
{
|
||||
if (_lookInFrames.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var frame in _lookInFrames)
|
||||
int outsideSliceCount = clipAssembly.OutsideViewSlices.Length;
|
||||
int lookInRouteIndex = 0;
|
||||
for (int frameIndex = 0; frameIndex < _lookInFrames.Count; frameIndex++)
|
||||
{
|
||||
PortalVisibilityFrame frame = _lookInFrames[frameIndex];
|
||||
|
||||
// Retail enters DrawBuilding once per building and drains every
|
||||
// alpha submission accumulated by the preceding building before
|
||||
// punching the next building's portals. The first building uses
|
||||
// the pre-look-in barrier in DrawLandscapeThroughOutsideView.
|
||||
if (frameIndex > 0)
|
||||
passes.FlushLandscapeAlpha();
|
||||
|
||||
// Pass 1: far-Z punch every aperture of this building.
|
||||
foreach (uint cellId in frame.OrderedVisibleCells)
|
||||
foreach (ExteriorPortalSeed seed in frame.ExteriorSeedPortals)
|
||||
{
|
||||
if (!frame.CellViews.TryGetValue(cellId, out var view))
|
||||
continue;
|
||||
foreach (var poly in view.Polygons)
|
||||
foreach (var poly in seed.View.Polygons)
|
||||
{
|
||||
var cps = ClipPlaneSet.From(poly);
|
||||
if (cps.IsNothingVisible)
|
||||
continue;
|
||||
passes.DrawLookInPortalPunch(ctx, new RetailPViewCellSliceContext(
|
||||
cellId,
|
||||
seed.CellId,
|
||||
new ClipViewSlice(
|
||||
0,
|
||||
new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY),
|
||||
cps.PlaneArray),
|
||||
NoParticleOwners));
|
||||
NoParticleOwners),
|
||||
seed.PortalIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: shells + statics, far→near.
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Opaque shells batched per building into ONE Render (this building's
|
||||
// aperture punches above already ran; z-buffer handles order and
|
||||
// lighting is per-instance CellId-keyed) — was one heavy per-frame
|
||||
// Render per cell. Per-cell entity/particle work stays in the loop.
|
||||
_shellBatch.Clear();
|
||||
foreach (uint cid in frame.OrderedVisibleCells)
|
||||
_shellBatch.Add(cid);
|
||||
if (_shellBatch.Count > 0)
|
||||
passes.DrawOpaqueCellShells(_shellBatch);
|
||||
|
||||
// Pass 2: shells + objects, far→near, once per portal_view slice.
|
||||
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||
{
|
||||
uint cellId = frame.OrderedVisibleCells[i];
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
// Opaque shell batched above. Transparent stays per-cell (far→near)
|
||||
// for correct compositing; skipped for opaque-only cells.
|
||||
if (passes.CellHasTransparentShell(cellId))
|
||||
passes.DrawTransparentCellShells(_oneCell);
|
||||
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
|
||||
|
|
@ -528,8 +536,7 @@ public sealed class RetailPViewRenderer
|
|||
// post-clear they would z-fail against the root's seal anyway
|
||||
// (the #118 lesson). Retail draws a look-in cell's objects
|
||||
// inside the NESTED DrawCells (DrawObjCellForDummies,
|
||||
// pc:432878+), i.e. right here in the landscape stage. Drawn
|
||||
// WHOLE like the statics (AP-33's documented over-include).
|
||||
// pc:432878+), i.e. right here in the landscape stage.
|
||||
// No double-draw: dynamics-last keeps culling them (their
|
||||
// cell is absent from the main cone), and their emitters ride
|
||||
// the DrawCellParticles call below, not DrawDynamicsParticles
|
||||
|
|
@ -541,48 +548,154 @@ public sealed class RetailPViewRenderer
|
|||
_cellStaticScratch.Add(e);
|
||||
}
|
||||
|
||||
if (frameEntityPasses is not null)
|
||||
foreach (ClipViewSlice slice in cellSlices)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_cellParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
i,
|
||||
cellId);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceOwnerIds(
|
||||
_cellParticleOwnerScratch,
|
||||
_cellStaticScratch);
|
||||
}
|
||||
int routeIndex = lookInRouteIndex++;
|
||||
passes.UseCellPortalViewRouting(cellId, slice);
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
passes.DrawOpaqueCellShells(_oneCell);
|
||||
if (passes.CellHasTransparentShell(cellId))
|
||||
passes.DrawTransparentCellShells(_oneCell);
|
||||
|
||||
if (frameEntityPasses is not null
|
||||
|| _cellStaticScratch.Count > 0)
|
||||
{
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LookInObject,
|
||||
i,
|
||||
cellId,
|
||||
_cellStaticScratch);
|
||||
DrawEntityRouteOrLegacy(
|
||||
ctx,
|
||||
passes,
|
||||
frameEntityPasses,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
i,
|
||||
cellId,
|
||||
_cellStaticScratch,
|
||||
_oneCell);
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_cellParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceOwnerIds(
|
||||
_cellParticleOwnerScratch,
|
||||
_cellStaticScratch);
|
||||
}
|
||||
|
||||
// The cell-particles pass for look-in cells — retail's
|
||||
// nested DrawCells draws objects WITH their emitters.
|
||||
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
||||
if (frameEntityPasses is not null
|
||||
|| _cellStaticScratch.Count > 0)
|
||||
{
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId,
|
||||
_cellStaticScratch);
|
||||
DrawEntityRouteOrLegacy(
|
||||
ctx,
|
||||
passes,
|
||||
frameEntityPasses,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
routeIndex,
|
||||
cellId,
|
||||
_cellStaticScratch,
|
||||
_oneCell);
|
||||
|
||||
// The nested DrawCells object pass includes emitters and
|
||||
// retains the exact setup_view clip until alpha playback.
|
||||
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
|
||||
cellId, slice, _cellParticleOwnerScratch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The ordinary exterior building shell is clipped by the outer
|
||||
// outside_view, not by the nested cell PortalList.
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
|
||||
// Retail's ordinary shell pass immediately follows this same
|
||||
// building's portal-only pass. Pair by the shell's authored
|
||||
// anchor EnvCell; never let an unrelated building repaint a
|
||||
// look-in merely because both happen to be nearby.
|
||||
int sliceIndex = 0;
|
||||
foreach (ClipViewSlice slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
int shellRouteIndex = LookInBuildingShellRouteIndex(
|
||||
frameIndex,
|
||||
outsideSliceCount,
|
||||
sliceIndex);
|
||||
_buildingShellScratch.Clear();
|
||||
if (partition is not null)
|
||||
{
|
||||
foreach (WorldEntity entity in partition.OutdoorStatic)
|
||||
{
|
||||
if (!entity.IsBuildingShell
|
||||
|| FindLookInFrameIndex(
|
||||
entity.BuildingShellAnchorCellId ?? 0,
|
||||
_lookInFrames,
|
||||
ctx.Cells) != frameIndex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EntitySphere(entity, out Vector3 center, out float radius);
|
||||
if (viewcone.SphereVisibleInOutsideSlice(
|
||||
sliceIndex,
|
||||
center,
|
||||
radius))
|
||||
{
|
||||
_buildingShellScratch.Add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LandscapeBuildingShell,
|
||||
shellRouteIndex,
|
||||
0,
|
||||
_buildingShellScratch);
|
||||
bool hasPackedShell = frameEntityPasses is not null
|
||||
&& HasExactRoute(
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LandscapeBuildingShell,
|
||||
shellRouteIndex,
|
||||
0);
|
||||
if (hasPackedShell || _buildingShellScratch.Count > 0)
|
||||
{
|
||||
RenderFrameEntityDrawRequest? shellDraw =
|
||||
frameEntityPasses is null
|
||||
? null
|
||||
: new RenderFrameEntityDrawRequest(
|
||||
frameView,
|
||||
RenderFrameCandidateRoute.LandscapeBuildingShell,
|
||||
shellRouteIndex,
|
||||
0,
|
||||
ctx.PlayerLandblockId ?? 0);
|
||||
passes.DrawLandscapeBuildingShellSlice(
|
||||
ctx,
|
||||
new RetailPViewLandscapeBuildingShellSliceContext(
|
||||
slice,
|
||||
_buildingShellScratch)
|
||||
{
|
||||
EntityDraw = shellDraw,
|
||||
});
|
||||
|
||||
_lateParticleOwnerScratch.Clear();
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_lateParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LandscapeBuildingShell,
|
||||
shellRouteIndex,
|
||||
0);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplaceOwnerIds(
|
||||
_lateParticleOwnerScratch,
|
||||
_buildingShellScratch);
|
||||
}
|
||||
passes.DrawLandscapeStaticParticles(
|
||||
ctx,
|
||||
new RetailPViewLandscapeStaticParticleContext(
|
||||
slice,
|
||||
_lateParticleOwnerScratch));
|
||||
}
|
||||
sliceIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -598,18 +711,13 @@ public sealed class RetailPViewRenderer
|
|||
if (clipAssembly.OutsideViewSlices.Length == 0)
|
||||
return;
|
||||
|
||||
// #131/#132 (the FlushAlphaList deferral): retail collects ALL alpha
|
||||
// draws of the landscape stage and flushes them ONCE after LScape::draw
|
||||
// (D3DPolyRender::FlushAlphaList, DrawCells pc:432722) — so translucent
|
||||
// landscape content (portal swirl meshes, flame particles) composites
|
||||
// AFTER the building look-ins. Our dispatcher draws translucency inside
|
||||
// each Draw call, so the stage is split in TWO phases instead: EARLY =
|
||||
// sky + terrain + outdoor STATIC meshes (the look-in punches need their
|
||||
// depth to mark against, the #117 lesson); then the look-ins; then
|
||||
// LATE = outside-stage dynamics' meshes + ALL scene particles +
|
||||
// weather. Content drawn early and overlapped by a look-in aperture
|
||||
// was otherwise overpainted by the far interior (translucents write no
|
||||
// depth to protect themselves) — the portal-swirl/candle-flame class.
|
||||
// #131/#132: retail drains the remaining landscape alpha after
|
||||
// LScape::draw (DrawCells pc:432720), while each DrawBuilding is also
|
||||
// an earlier alpha barrier before its portal traversal (pc:427954).
|
||||
// Our dispatcher batches outdoor content, so the stage is split into:
|
||||
// EARLY sky/terrain/static meshes; an optional pre-look-in static-alpha
|
||||
// barrier; building look-ins; then LATE outside-stage dynamics,
|
||||
// remaining particles, and weather; followed by the outer flush.
|
||||
int probeSliceIndex = 0;
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
|
|
@ -628,6 +736,14 @@ public sealed class RetailPViewRenderer
|
|||
{
|
||||
foreach (var e in partition.OutdoorStatic)
|
||||
{
|
||||
if (e.IsBuildingShell
|
||||
&& FindLookInFrameIndex(
|
||||
e.BuildingShellAnchorCellId ?? 0,
|
||||
_lookInFrames,
|
||||
ctx.Cells) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (viewcone.SphereVisibleInOutsideSlice(
|
||||
probeSliceIndex,
|
||||
|
|
@ -663,15 +779,81 @@ public sealed class RetailPViewRenderer
|
|||
});
|
||||
}
|
||||
|
||||
// Retail DrawBuilding flushes every alpha submission accumulated before
|
||||
// the building immediately before its portal-only traversal
|
||||
// (RenderDeviceD3D::DrawBuilding pc:427954-427956). That barrier is
|
||||
// essential at open-air seams: foliage and static emitters encountered
|
||||
// before the building must not be flushed after the look-in cell floor
|
||||
// and repaint it. Our outdoor statics are one retained batch rather than
|
||||
// retail's BSP-by-building walk, so use one barrier before the first
|
||||
// look-in; DrawBuildingLookIns adds the corresponding barrier between
|
||||
// each later building pair. Submit the early static owners' particles
|
||||
// into the same alpha queue first; their mesh alpha was already
|
||||
// submitted by the EARLY entity route above.
|
||||
bool hasBuildingLookIns = _lookInFrames.Count > 0;
|
||||
if (hasBuildingLookIns)
|
||||
{
|
||||
int barrierSliceIndex = 0;
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
{
|
||||
// Ownerless outdoor emitters cannot ride an entity route. Retail
|
||||
// draws their meshes once for every installed outside_view;
|
||||
// retain that slot through deferred alpha playback.
|
||||
passes.DrawUnattachedSceneParticles(ctx, slice);
|
||||
|
||||
_lateParticleOwnerScratch.Clear();
|
||||
if (partition is not null)
|
||||
{
|
||||
foreach (var e in partition.OutdoorStatic)
|
||||
{
|
||||
if (e.IsBuildingShell
|
||||
&& FindLookInFrameIndex(
|
||||
e.BuildingShellAnchorCellId ?? 0,
|
||||
_lookInFrames,
|
||||
ctx.Cells) >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (viewcone.SphereVisibleInOutsideSlice(
|
||||
barrierSliceIndex,
|
||||
c,
|
||||
r))
|
||||
{
|
||||
_lateParticleOwnerScratch.Add(e.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_lateParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
|
||||
barrierSliceIndex,
|
||||
0);
|
||||
}
|
||||
|
||||
passes.DrawLandscapeStaticParticles(
|
||||
ctx,
|
||||
new RetailPViewLandscapeStaticParticleContext(
|
||||
slice,
|
||||
_lateParticleOwnerScratch));
|
||||
barrierSliceIndex++;
|
||||
}
|
||||
passes.FlushLandscapeAlpha();
|
||||
}
|
||||
|
||||
// #124: far-building look-ins draw HERE — still inside the landscape
|
||||
// stage (their punches mark against the terrain/exterior depth just
|
||||
// drawn), strictly BEFORE the depth clear + seals below, matching
|
||||
// drawn), strictly BEFORE the outer depth clear + seals below, matching
|
||||
// retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785).
|
||||
DrawBuildingLookIns(
|
||||
ctx,
|
||||
passes,
|
||||
clipAssembly,
|
||||
partition,
|
||||
viewcone,
|
||||
frameEntityPasses,
|
||||
in frameView);
|
||||
|
||||
|
|
@ -687,8 +869,8 @@ public sealed class RetailPViewRenderer
|
|||
passes.ClearClipRouting();
|
||||
|
||||
_outdoorStaticScratch.Clear(); // late: dynamics survivors
|
||||
_lateParticleOwnerScratch.Clear(); // late: statics + dynamics survivors
|
||||
if (partition is not null)
|
||||
_lateParticleOwnerScratch.Clear(); // late: dynamics, plus statics without look-ins
|
||||
if (!hasBuildingLookIns && partition is not null)
|
||||
{
|
||||
foreach (var e in partition.OutdoorStatic)
|
||||
{
|
||||
|
|
@ -712,12 +894,19 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
if (frameEntityPasses is not null)
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_lateParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
|
||||
probeSliceIndex,
|
||||
0);
|
||||
if (hasBuildingLookIns)
|
||||
{
|
||||
_lateParticleOwnerScratch.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderFrameRouteOwnerSelector.Replace(
|
||||
_lateParticleOwnerScratch,
|
||||
in frameView,
|
||||
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
|
||||
probeSliceIndex,
|
||||
0);
|
||||
}
|
||||
RenderFrameRouteOwnerSelector.Union(
|
||||
_lateParticleOwnerScratch,
|
||||
in frameView,
|
||||
|
|
@ -753,13 +942,19 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
// #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls,
|
||||
// campfires, ground effects anchored at a position) have no owner id
|
||||
// to ride any of the id-filtered particle passes. The outdoor root
|
||||
// has the dedicated T3 pass for them; an INTERIOR root had NO pass
|
||||
// at all. Draw them ONCE per frame (not per slice — alpha particles
|
||||
// must not double-draw, the #121 lesson), at the END of the landscape
|
||||
// stage: after the clear they would z-fail against the doorway seal.
|
||||
if (!ctx.RootCell.IsOutdoorNode)
|
||||
passes.DrawUnattachedSceneParticles(ctx);
|
||||
// to ride any of the id-filtered particle passes. Draw once per
|
||||
// installed outside_view for BOTH root kinds, matching retail's
|
||||
// landscape-stage placement and preserving the slot in each deferred
|
||||
// draw. The former outdoor-root post-world tail ran after building
|
||||
// cells and let exterior alpha repaint the cathedral transition.
|
||||
// With no look-ins they drain at the end of the landscape stage; the
|
||||
// look-in path submits them at its pre-building barrier so later opaque
|
||||
// cell floors can cover them.
|
||||
if (!hasBuildingLookIns)
|
||||
{
|
||||
foreach (ClipViewSlice slice in clipAssembly.OutsideViewSlices)
|
||||
passes.DrawUnattachedSceneParticles(ctx, slice);
|
||||
}
|
||||
|
||||
// Retail PView::DrawCells 0x005A4872 drains the landscape alpha list
|
||||
// immediately after LScape::draw and before the optional depth clear.
|
||||
|
|
@ -779,6 +974,58 @@ public sealed class RetailPViewRenderer
|
|||
passes.UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
||||
internal static int LookInBuildingShellRouteIndex(
|
||||
int frameIndex,
|
||||
int outsideSliceCount,
|
||||
int sliceIndex) =>
|
||||
checked((frameIndex * outsideSliceCount) + sliceIndex);
|
||||
|
||||
internal static int FindLookInFrameIndex(
|
||||
uint buildingShellAnchorCellId,
|
||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||
IRetailPViewCellSource cells)
|
||||
{
|
||||
if (buildingShellAnchorCellId == 0)
|
||||
return -1;
|
||||
|
||||
LoadedCell? anchorCell = cells.Find(buildingShellAnchorCellId);
|
||||
if (anchorCell is null)
|
||||
return -1;
|
||||
|
||||
uint buildingKey = anchorCell.BuildingId ?? anchorCell.CellId;
|
||||
uint landblockId = anchorCell.CellId & 0xFFFF0000u;
|
||||
for (int frameIndex = 0; frameIndex < lookInFrames.Count; frameIndex++)
|
||||
{
|
||||
PortalVisibilityFrame frame = lookInFrames[frameIndex];
|
||||
if (frame.SourceBuildingKey == buildingKey
|
||||
&& frame.SourceBuildingLandblockId == landblockId)
|
||||
{
|
||||
return frameIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool HasExactRoute(
|
||||
in RenderFrameView view,
|
||||
RenderFrameCandidateRoute route,
|
||||
int routeIndex,
|
||||
uint cellId)
|
||||
{
|
||||
foreach (RenderFrameCandidateRange range in view.RouteRanges)
|
||||
{
|
||||
if (range.Route == route
|
||||
&& range.RouteIndex == routeIndex
|
||||
&& range.CellId == cellId
|
||||
&& range.Count > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void DrawExitPortalMasks(
|
||||
RetailPViewFrameInput ctx,
|
||||
IRetailPViewPassExecutor passes,
|
||||
|
|
@ -1180,6 +1427,7 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
// T3 scratch lists (render thread only; cleared per use).
|
||||
private readonly List<WorldEntity> _outdoorStaticScratch = new();
|
||||
private readonly List<WorldEntity> _buildingShellScratch = new();
|
||||
private readonly List<WorldEntity> _cellStaticScratch = new();
|
||||
private readonly List<WorldEntity> _dynamicsScratch = new();
|
||||
// #118: dynamics assigned to the OUTSIDE stage this frame (interior roots
|
||||
|
|
@ -1343,10 +1591,14 @@ public interface IRetailPViewPassExecutor
|
|||
ClipFrameAssembly AssembleClipFrame(
|
||||
PortalVisibilityFrame portalFrame,
|
||||
ClipFrameAssembly reuseAssembly);
|
||||
void AppendLookInClipFrames(
|
||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||
ClipFrameAssembly assembly);
|
||||
void PrepareClipFrame(int terrainUploadCount);
|
||||
void SetTerrainClip(ReadOnlySpan<Vector4> planes);
|
||||
void ClearClipRouting();
|
||||
void UseIndoorMembershipOnlyRouting();
|
||||
void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice);
|
||||
void PrepareCellBatches(
|
||||
RetailPViewFrameInput frame,
|
||||
HashSet<uint> visibleCellIds);
|
||||
|
|
@ -1363,11 +1615,22 @@ public interface IRetailPViewPassExecutor
|
|||
ClipViewSlice slice,
|
||||
int sliceIndex);
|
||||
void DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context);
|
||||
void DrawLandscapeStaticParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewLandscapeStaticParticleContext context);
|
||||
void DrawLandscapeBuildingShellSlice(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewLandscapeBuildingShellSliceContext context);
|
||||
void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context);
|
||||
void ClearInteriorDepth();
|
||||
void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context);
|
||||
void DrawLookInPortalPunch(RetailPViewFrameInput frame, RetailPViewCellSliceContext context);
|
||||
void DrawUnattachedSceneParticles(RetailPViewFrameInput frame);
|
||||
void DrawLookInPortalPunch(
|
||||
RetailPViewFrameInput frame,
|
||||
RetailPViewCellSliceContext context,
|
||||
int portalIndex);
|
||||
void DrawUnattachedSceneParticles(
|
||||
RetailPViewFrameInput frame,
|
||||
ClipViewSlice slice);
|
||||
void FlushLandscapeAlpha();
|
||||
void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context);
|
||||
void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlySet<uint> ownerIds);
|
||||
|
|
@ -1690,9 +1953,27 @@ public readonly record struct RetailPViewLandscapeSliceContext(
|
|||
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outdoor-static emitters submitted at retail's pre-building alpha barrier.
|
||||
/// Mesh alpha for the same owners is already queued by the early landscape
|
||||
/// entity route.
|
||||
/// </summary>
|
||||
public readonly record struct RetailPViewLandscapeStaticParticleContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlySet<uint> ParticleOwnerIds);
|
||||
|
||||
/// <summary>Retail DrawBuilding's ordinary exterior-shell pass, issued after
|
||||
/// the same building's portal-only look-in traversal.</summary>
|
||||
public readonly record struct RetailPViewLandscapeBuildingShellSliceContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlyList<WorldEntity> BuildingShells)
|
||||
{
|
||||
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
||||
/// outside-stage dynamics to mesh-draw, plus the full scene-particle owner
|
||||
/// set (statics + dynamics cone survivors) the attached-emitter filter keys on.</summary>
|
||||
/// outside-stage dynamics to mesh-draw, plus the particle owners not already
|
||||
/// submitted at a pre-building barrier.</summary>
|
||||
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlyList<WorldEntity> Dynamics,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue