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:
parent
e5cdd2364e
commit
b3ac5872e9
8 changed files with 90 additions and 12 deletions
|
|
@ -136,6 +136,11 @@ public sealed class RetailFrameWalk
|
||||||
{
|
{
|
||||||
sink.Emit(WalkEvent.Building(building.PositionCellId));
|
sink.Emit(WalkEvent.Building(building.PositionCellId));
|
||||||
if (!building.HasGeometry) return;
|
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);
|
int viewCount = Math.Max(activeViews.ViewCount, 0);
|
||||||
var passSink = new PortalPassSink(sink);
|
var passSink = new PortalPassSink(sink);
|
||||||
|
|
@ -144,11 +149,11 @@ public sealed class RetailFrameWalk
|
||||||
{
|
{
|
||||||
ctx.SetActiveView(activeViews, v);
|
ctx.SetActiveView(activeViews, v);
|
||||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||||
building.DrawingBsp, 1, viewpoint,
|
bsp, 1, viewpoint,
|
||||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||||
WalkBuildingPortals.BuildDrawPortalsOnly(
|
WalkBuildingPortals.BuildDrawPortalsOnly(
|
||||||
building.DrawingBsp, 2, viewpoint,
|
bsp, 2, viewpoint,
|
||||||
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
(portalRef, pass) => WalkBuildingPortals.DrawPortal(
|
||||||
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
_outdoorPView, building, portalRef, pass, ctx, passSink));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ public sealed class WalkBspNode
|
||||||
public bool IsPortal => InPortals is not null;
|
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> +
|
/// <summary>The walk's building model (retail <c>CBuildingObj</c> +
|
||||||
/// <c>BuildInfo</c> as the frame walk consumes them).</summary>
|
/// <c>BuildInfo</c> as the frame walk consumes them).</summary>
|
||||||
public sealed class WalkBuilding
|
public sealed class WalkBuilding
|
||||||
|
|
@ -47,12 +51,35 @@ public sealed class WalkBuilding
|
||||||
|
|
||||||
public WalkBldPortal[] Portals = [];
|
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;
|
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
|
/// <summary><c>part->gfxobj[deg_level] != 0</c> — a degraded-out slot
|
||||||
/// skips the whole building AFTER publishing the portal list.</summary>
|
/// skips the whole building AFTER publishing the portal list.</summary>
|
||||||
public bool HasGeometry = true;
|
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>
|
/// <summary>
|
||||||
|
|
@ -246,6 +273,11 @@ public interface IWalkBuildingFrameContext
|
||||||
{
|
{
|
||||||
Vector3 ViewpointInBuilding(WalkBuilding building);
|
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
|
/// <summary>Project + clip one building-local portal polygon against
|
||||||
/// the ACTIVE view (retail: GetClip with do_clip=1 in the building
|
/// the ACTIVE view (retail: GetClip with do_clip=1 in the building
|
||||||
/// frame). Returns the surviving count.</summary>
|
/// frame). Returns the surviving count.</summary>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ public sealed class RetailFrameWalkTests
|
||||||
public float ViewportHeight => 480f;
|
public float ViewportHeight => 480f;
|
||||||
|
|
||||||
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
|
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
|
||||||
|
public float ViewerDistanceTo(WalkBuilding building) => 0f;
|
||||||
public IWalkFrameContext CellContext => this;
|
public IWalkFrameContext CellContext => this;
|
||||||
// Permissive near plane: every column wholly inside.
|
// Permissive near plane: every column wholly inside.
|
||||||
public WalkPlane CyPlane => new(new Vector3(0, 0, 1), 0f);
|
public WalkPlane CyPlane => new(new Vector3(0, 0, 1), 0f);
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ public sealed class WalkBuildingPortalTests
|
||||||
public float ViewportHeight => 480f;
|
public float ViewportHeight => 480f;
|
||||||
|
|
||||||
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
|
public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero;
|
||||||
|
public float ViewerDistanceTo(WalkBuilding building) => 0f;
|
||||||
public IWalkFrameContext CellContext => this;
|
public IWalkFrameContext CellContext => this;
|
||||||
|
|
||||||
public int ClipBuildingPolygon(
|
public int ClipBuildingPolygon(
|
||||||
|
|
|
||||||
|
|
@ -108,9 +108,31 @@ public static class WalkLandscapeDatBuilder
|
||||||
int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1;
|
int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1;
|
||||||
if (cellIndex >= 0 && cellIndex < 64)
|
if (cellIndex >= 0 && cellIndex < 64)
|
||||||
block.CellBuildings[cellIndex] = entry.Building;
|
block.CellBuildings[cellIndex] = entry.Building;
|
||||||
|
|
||||||
|
// Retail's loaded-interior rule (CLandBlock::init_buildings
|
||||||
|
// @0052fd80 → add_to_stablist → grab_visible_cells): a
|
||||||
|
// full-res block loads exactly its buildings' portal
|
||||||
|
// stab cells — NOT every interior. The flood halts at
|
||||||
|
// unloaded cells (GetVisible null), which is what
|
||||||
|
// limits retail's look-in punches to nearby doorways.
|
||||||
|
foreach (WalkBldPortal portal in entry.Building.Portals)
|
||||||
|
{
|
||||||
|
if (portal.OtherCellId != 0xFFFFFFFFu
|
||||||
|
&& !cells.ContainsKey(portal.OtherCellId))
|
||||||
|
{
|
||||||
|
WalkCell? c = WalkWorldDatAdapter.BuildCell(
|
||||||
|
dats, portal.OtherCellId, blockOffset);
|
||||||
|
if (c is not null) cells[c.CellId] = c;
|
||||||
|
}
|
||||||
|
foreach (uint stab in portal.StabList)
|
||||||
|
{
|
||||||
|
if (cells.ContainsKey(stab)) continue;
|
||||||
|
WalkCell? c = WalkWorldDatAdapter.BuildCell(
|
||||||
|
dats, stab, blockOffset);
|
||||||
|
if (c is not null) cells[c.CellId] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WalkWorldDatAdapter.BuildInteriorCells(
|
|
||||||
dats, landblockId, blockOffset, cells);
|
|
||||||
}
|
}
|
||||||
landscape.Blocks[gx * midWidth + gy] = block;
|
landscape.Blocks[gx * midWidth + gy] = block;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,7 @@ public sealed class WalkLookInGateSweepTests
|
||||||
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
|
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "Adjudicated 2026-08-30: NO gate decode reproduces retail, and "
|
[Fact]
|
||||||
+ "the join diagnostic proves the data is right — the missing mechanism "
|
|
||||||
+ "is CEnvCell::GetVisible's LOADED-interior-cell gate (retail punched "
|
|
||||||
+ "only the two buildings nearest the player; their interiors were "
|
|
||||||
+ "loaded, farther ones were not; the replay loads everything). Port "
|
|
||||||
+ "the interior load radius (landcell stab-list pull around the "
|
|
||||||
+ "player) and re-run this sweep to pin the plane/side decode.")]
|
|
||||||
public void Sweep_the_lookin_gate_decodes_against_the_street_fixture()
|
public void Sweep_the_lookin_gate_decodes_against_the_street_fixture()
|
||||||
{
|
{
|
||||||
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,10 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
|
||||||
public Vector3 ViewpointInBuilding(WalkBuilding building)
|
public Vector3 ViewpointInBuilding(WalkBuilding building)
|
||||||
=> Vector3.Transform(WorldViewpoint, Buildings[building].InverseWorldTransform);
|
=> Vector3.Transform(WorldViewpoint, Buildings[building].InverseWorldTransform);
|
||||||
|
|
||||||
|
public float ViewerDistanceTo(WalkBuilding building)
|
||||||
|
=> Vector3.Distance(
|
||||||
|
WorldViewpoint, Buildings[building].WorldTransform.Translation);
|
||||||
|
|
||||||
public int ClipBuildingPolygon(
|
public int ClipBuildingPolygon(
|
||||||
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
|
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -148,8 +148,26 @@ public static class WalkWorldDatAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
WalkBspNode? bsp = null;
|
WalkBspNode? bsp = null;
|
||||||
|
var degradeLevels = new List<WalkBuildingDegradeLevel>();
|
||||||
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj)
|
if (dats.Get<GfxObj>(buildingInfo.ModelId) is GfxObj gfxObj)
|
||||||
|
{
|
||||||
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
|
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
|
||||||
|
if (gfxObj.DIDDegrade != 0
|
||||||
|
&& dats.Get<GfxObjDegradeInfo>(gfxObj.DIDDegrade)
|
||||||
|
is GfxObjDegradeInfo degradeInfo)
|
||||||
|
{
|
||||||
|
foreach (GfxObjInfo level in degradeInfo.Degrades)
|
||||||
|
{
|
||||||
|
WalkBspNode? levelBsp = null;
|
||||||
|
if (level.Id != 0
|
||||||
|
&& dats.Get<GfxObj>((uint)level.Id) is GfxObj levelGfx)
|
||||||
|
{
|
||||||
|
levelBsp = ConvertDrawingBsp(levelGfx, levelGfx.DrawingBSP?.Root);
|
||||||
|
}
|
||||||
|
degradeLevels.Add(new WalkBuildingDegradeLevel(level.MaxDist, levelBsp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Matrix4x4 worldTransform =
|
Matrix4x4 worldTransform =
|
||||||
Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation)
|
Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation)
|
||||||
|
|
@ -161,6 +179,7 @@ public static class WalkWorldDatAdapter
|
||||||
PositionCellId = positionCellId,
|
PositionCellId = positionCellId,
|
||||||
Portals = portals,
|
Portals = portals,
|
||||||
DrawingBsp = bsp,
|
DrawingBsp = bsp,
|
||||||
|
DegradeLevels = degradeLevels.ToArray(),
|
||||||
},
|
},
|
||||||
worldTransform,
|
worldTransform,
|
||||||
inverse));
|
inverse));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue