feat(render) Campaign FW1: the degrade-level BSP gate kills the wrong punches

The offline degrade probe proved the mechanism: every Holtburg building
carries PORT nodes ONLY in its level-0 GfxObj (out to ~24-48 m); every
degraded level has zero. Retail walks the CURRENT degrade level BSP
(part->gfxobj[deg_level]) - that is what limits look-in punches to the
nearest buildings. WalkBuilding gains the degrade ladder +
SelectDrawingBsp (band pick; UpdateViewerDistance hysteresis is a port
TODO), the walk selects per viewer distance, the adapter builds
per-level BSPs, and the stab-list load rule (CLandBlock::init_buildings
@0052fd80: a full-res block loads exactly its buildings portal stab
cells) replaces load-everything in the landscape builder. The sweep now
shows clean rosters with all far-building punches gone; remaining
deltas: the near buildings 001a/0022 (50 m/28 m center distance vs the
48 m band edge - sphere-adjusted distance/hysteresis to port) and the
one ring-1 frustum boundary pair (aab50002/a9b3003c).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 10:51:05 +02:00
parent e5cdd2364e
commit b3ac5872e9
8 changed files with 90 additions and 12 deletions

View file

@ -136,6 +136,11 @@ public sealed class RetailFrameWalk
{
sink.Emit(WalkEvent.Building(building.PositionCellId));
if (!building.HasGeometry) return;
// Retail walks the CURRENT degrade level's drawing BSP
// (part->gfxobj[deg_level]); a degraded-out slot skips everything
// after publishing the portal list.
WalkBspNode? bsp = building.SelectDrawingBsp(ctx.ViewerDistanceTo(building));
if (bsp is null) return;
int viewCount = Math.Max(activeViews.ViewCount, 0);
var passSink = new PortalPassSink(sink);
@ -144,11 +149,11 @@ public sealed class RetailFrameWalk
{
ctx.SetActiveView(activeViews, v);
WalkBuildingPortals.BuildDrawPortalsOnly(
building.DrawingBsp, 1, viewpoint,
bsp, 1, viewpoint,
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
_outdoorPView, building, portalRef, pass, ctx, passSink));
WalkBuildingPortals.BuildDrawPortalsOnly(
building.DrawingBsp, 2, viewpoint,
bsp, 2, viewpoint,
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
_outdoorPView, building, portalRef, pass, ctx, passSink));
}

View file

@ -37,6 +37,10 @@ public sealed class WalkBspNode
public bool IsPortal => InPortals is not null;
}
/// <summary>One degrade-ladder level: the drawing BSP of that level's
/// GfxObj (portal-only view) and the level's far distance bound.</summary>
public readonly record struct WalkBuildingDegradeLevel(float MaxDist, WalkBspNode? DrawingBsp);
/// <summary>The walk's building model (retail <c>CBuildingObj</c> +
/// <c>BuildInfo</c> as the frame walk consumes them).</summary>
public sealed class WalkBuilding
@ -47,12 +51,35 @@ public sealed class WalkBuilding
public WalkBldPortal[] Portals = [];
/// <summary>The drawing BSP of part 0's GfxObj (portal-only view).</summary>
/// <summary>The drawing BSP of part 0's BASE GfxObj (portal-only view).
/// Used directly when the model has no degrade ladder.</summary>
public WalkBspNode? DrawingBsp;
/// <summary>The degrade ladder (near→far). Retail walks the CURRENT
/// level's drawing BSP (<c>part->gfxobj[deg_level]</c>) — and building
/// degrade models beyond the first band carry NO portal nodes, which is
/// what limits look-in punches to nearby buildings (probed 2026-08-30:
/// every Holtburg building has ports at level 0 only).</summary>
public WalkBuildingDegradeLevel[] DegradeLevels = [];
/// <summary><c>part->gfxobj[deg_level] != 0</c> — a degraded-out slot
/// skips the whole building AFTER publishing the portal list.</summary>
public bool HasGeometry = true;
/// <summary>Level selection by viewer distance. Simplified band pick
/// (first level whose MaxDist covers the distance) — the faithful
/// hysteresis of <c>CPhysicsPart::UpdateViewerDistance</c> is a port
/// TODO; still fixtures are insensitive to hysteresis.</summary>
public WalkBspNode? SelectDrawingBsp(float viewerDistance)
{
if (DegradeLevels.Length == 0) return DrawingBsp;
foreach (WalkBuildingDegradeLevel level in DegradeLevels)
{
if (viewerDistance <= level.MaxDist)
return level.DrawingBsp;
}
return null; // degraded out entirely
}
}
/// <summary>
@ -246,6 +273,11 @@ public interface IWalkBuildingFrameContext
{
Vector3 ViewpointInBuilding(WalkBuilding building);
/// <summary>The viewer's distance to the building
/// (<c>CPhysicsPart::UpdateViewerDistance</c>'s input) — selects the
/// degrade level whose drawing BSP the portal pass walks.</summary>
float ViewerDistanceTo(WalkBuilding building);
/// <summary>Project + clip one building-local portal polygon against
/// the ACTIVE view (retail: GetClip with do_clip=1 in the building
/// frame). Returns the surviving count.</summary>