feat(render): R-A2 — per-building floods (the flap fix)
Replace the outdoor root's single unified reverse-portal flood (whose root-level portal-side test oscillated as the chase eye grazed a doorway — the measured flood 2<->6) with retail's per-building floods. - OutdoorCellNode.Build(uint): portal-less land root; floods only itself -> full-screen OutsideView -> terrain (PortalVisibilityBuilder IsOutdoorNode seed). - PortalVisibilityBuilder.ConstructViewBuilding: per-building flood seeded at a building's own finite entrance (retail ConstructView(CBldPortal) 0x5a59a0 via DrawPortal 0x5a5ab0 / portal_draw_portals_only 0x53d870). Entrance-bounded -> consistent ~2-cell depth (measured retail cell_draw_num, handoff OPTION-A 3.4). - RetailPViewRenderer.DrawInside: when the root is the outdoor node, group nearby cells by BuildingId and merge each per-building flood into the frame before assembly; existing shells/object-list draw path unchanged. 48 m seed cutoff. - GameWindow: pass flat NearbyBuildingCells only on outdoor-node frames. Tests: +3 PortalVisibilityRobustnessTests (per-building touches ~2 cells, membership stable under the measured 36 um eye jitter). UnifiedFloodTests retired (its subject, the unified flood from the outdoor node, is removed); surviving full-screen-OutsideView coverage moved to OutdoorCellNodeTests. App Rendering 207/207, Core movement 14/14. Conformance-verified sound; the grazing-doorway flap is the visual acceptance test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7fe98098f5
commit
c62663d7cb
8 changed files with 251 additions and 198 deletions
|
|
@ -24,6 +24,11 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
private readonly HashSet<uint> _oneCell = new(1);
|
||||
private readonly Dictionary<uint, int> _oneCellSlot = new(1);
|
||||
|
||||
// R-A2: per-building flood grouping, reused across frames (inner lists cleared each frame).
|
||||
private readonly Dictionary<uint, List<LoadedCell>> _buildingGroups = new();
|
||||
private const float OutdoorBuildingSeedDistance = 48f;
|
||||
|
||||
public RetailPViewRenderer(
|
||||
GL gl,
|
||||
ClipFrame clipFrame,
|
||||
|
|
@ -46,6 +51,15 @@ public sealed class RetailPViewRenderer
|
|||
ctx.CellLookup,
|
||||
ctx.ViewProjection);
|
||||
|
||||
// R-A2: outdoor root — flood each nearby building SEPARATELY from its own entrance and merge
|
||||
// the small (~2-cell) per-building views into the frame. Retail reaches building interiors via
|
||||
// the terrain BSP -> DrawPortal -> ConstructView(CBldPortal) (decomp:326881/433895/433827); the
|
||||
// land root itself has no portals (it floods nothing into buildings). Per-building seeding is
|
||||
// robust to the eye's ~36 µm rest jitter where the pre-R-A2 single reverse-portal flood
|
||||
// oscillated as the chase eye grazed a doorway (the indoor flap).
|
||||
if (ctx.RootCell.IsOutdoorNode && ctx.NearbyBuildingCells is not null)
|
||||
MergeNearbyBuildingFloods(ctx, pvFrame);
|
||||
|
||||
var clipAssembly = ClipFrameAssembler.Assemble(_clipFrame, pvFrame);
|
||||
UploadClipFrame(ctx.SetTerrainClipUbo);
|
||||
|
||||
|
|
@ -85,6 +99,52 @@ public sealed class RetailPViewRenderer
|
|||
return result;
|
||||
}
|
||||
|
||||
// R-A2: group the nearby building cells by BuildingId and run one per-building flood per group
|
||||
// (retail's per-building ConstructView(CBldPortal)), merging each small view into the frame. The
|
||||
// grouping dict is reused across frames; inner lists are cleared each frame so a building that left
|
||||
// the near set simply contributes an empty (skipped) group.
|
||||
private void MergeNearbyBuildingFloods(RetailPViewDrawContext ctx, PortalVisibilityFrame pvFrame)
|
||||
{
|
||||
foreach (var group in _buildingGroups.Values)
|
||||
group.Clear();
|
||||
|
||||
foreach (var cell in ctx.NearbyBuildingCells!)
|
||||
{
|
||||
if (cell.BuildingId is not uint buildingId)
|
||||
continue; // outdoor surface cells (no building) don't flood
|
||||
if (!_buildingGroups.TryGetValue(buildingId, out var group))
|
||||
{
|
||||
group = new List<LoadedCell>();
|
||||
_buildingGroups[buildingId] = group;
|
||||
}
|
||||
group.Add(cell);
|
||||
}
|
||||
|
||||
foreach (var group in _buildingGroups.Values)
|
||||
{
|
||||
if (group.Count == 0)
|
||||
continue;
|
||||
var buildingFrame = PortalVisibilityBuilder.ConstructViewBuilding(
|
||||
group, ctx.ViewerEyePos, ctx.CellLookup, ctx.ViewProjection, OutdoorBuildingSeedDistance);
|
||||
MergeBuildingFrame(pvFrame, buildingFrame);
|
||||
}
|
||||
}
|
||||
|
||||
// Append a per-building flood's cells + views into the frame. Each building cell belongs to exactly
|
||||
// one building, so there is no cross-building overlap; ContainsKey is a safety dedup. OutsideView is
|
||||
// NOT merged — the outdoor root already seeds full-screen terrain, and ConstructViewBuilding
|
||||
// (BuildFromExterior) leaves OutsideView empty (it stops at exit portals once inside the building).
|
||||
private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
|
||||
{
|
||||
foreach (uint cellId in src.OrderedVisibleCells)
|
||||
{
|
||||
if (target.CellViews.ContainsKey(cellId))
|
||||
continue;
|
||||
target.CellViews[cellId] = src.CellViews[cellId];
|
||||
target.OrderedVisibleCells.Add(cellId);
|
||||
}
|
||||
}
|
||||
|
||||
public RetailPViewFrameResult? DrawPortal(RetailPViewPortalDrawContext ctx)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ctx);
|
||||
|
|
@ -310,6 +370,11 @@ public interface IRetailPViewCellDrawContext : IRetailPViewCellDrawCallbacks
|
|||
public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
||||
{
|
||||
public required LoadedCell RootCell { get; init; }
|
||||
|
||||
/// <summary>R-A2: nearby building cells (BuildingId-tagged) flooded per-building when the root is the
|
||||
/// outdoor node. Null for interior roots. Grouped by BuildingId inside <see cref="DrawInside"/>.</summary>
|
||||
public IReadOnlyList<LoadedCell>? NearbyBuildingCells { get; init; }
|
||||
|
||||
public required Vector3 ViewerEyePos { get; init; }
|
||||
public required Matrix4x4 ViewProjection { get; init; }
|
||||
public required Func<uint, LoadedCell?> CellLookup { get; init; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue