T2 slice 1 (BR-4): multi-view UNION merge + retail 1-px vertex merge (the fixpoint floor)
(a) MergeBuildingFrame now UNIONS a building flood's views into cells already present in the frame (retail Render::copy_view APPENDS every clipped portal polygon as a new view_poly, Ghidra 0x0054dfc0 - a cell visible through two apertures holds two views). The old first-wins 'ContainsKey -> continue' dropped the second aperture's views: the multiview-loss-first-wins divergence, a named #109 suspect. (b) ClipToRegion output now runs retail's post-divide vertex merge: consecutive vertices closer than ~1 pixel collapse (copy_view's |dx|<=1 && |dy|<=1 screen-unit merge), polygons that collapse below 3 distinct verts return empty (retail's '<3 survivors -> count 0'). This is the flood's PHYSICAL fixpoint floor - re-clipping can only insert sub-pixel slivers, which the merge removes, so accumulated views converge instead of drifting. Unit note: builder has no viewport, so 1 px is expressed as NDC at reference 1080p (0.00185); coarser at higher res, which only strengthens convergence. This is the prerequisite for removing the MaxReprocessPerCell=16 drift cap (T2 slice 2) and the EyeInsidePortalOpening rescue. Gates: all 10 flood conformance tests green (CornerSweep monotone pin included); App 226 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
579c8b06bc
commit
cf8a2c379b
2 changed files with 75 additions and 8 deletions
|
|
@ -129,8 +129,59 @@ public static class PortalProjection
|
||||||
float w = poly[i].W;
|
float w = poly[i].W;
|
||||||
ndc[i] = new Vector2(poly[i].X / w, poly[i].Y / w);
|
ndc[i] = new Vector2(poly[i].X / w, poly[i].Y / w);
|
||||||
}
|
}
|
||||||
EnsureCcw(ndc);
|
|
||||||
return ndc;
|
// T2 (BR-4): retail's post-divide vertex merge — Render::copy_view
|
||||||
|
// (Ghidra 0x0054dfc0) collapses consecutive vertices closer than ~1
|
||||||
|
// PIXEL (|dx|<=1 && |dy|<=1 screen units) after the perspective divide.
|
||||||
|
// This is the flood's physical fixpoint floor: re-clipping a view can
|
||||||
|
// only insert sub-pixel sliver vertices, which this merge removes, so
|
||||||
|
// accumulated views converge instead of drifting (the drift is what
|
||||||
|
// forced the MaxReprocessPerCell=16 cap). Unit approximation: the
|
||||||
|
// builder has no viewport, so 1 px is expressed in NDC at a reference
|
||||||
|
// 1080p (2/1080 ≈ 0.00185); at higher resolutions the merge is merely
|
||||||
|
// slightly coarser than retail's, which only strengthens convergence.
|
||||||
|
var merged = MergeSubPixelVertices(ndc);
|
||||||
|
if (merged.Length < 3)
|
||||||
|
return System.Array.Empty<Vector2>();
|
||||||
|
|
||||||
|
EnsureCcw(merged);
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retail copy_view's ~1-pixel vertex merge (see ClipToRegion). Collapses
|
||||||
|
// runs of consecutive near-identical vertices, including across the
|
||||||
|
// wrap-around. A polygon that collapses below 3 distinct vertices is
|
||||||
|
// degenerate (sub-pixel sliver) and returns empty — exactly retail's
|
||||||
|
// "<3 surviving verts → output count 0".
|
||||||
|
private const float VertexMergeEpsilonNdc = 2f / 1080f;
|
||||||
|
|
||||||
|
private static Vector2[] MergeSubPixelVertices(Vector2[] poly)
|
||||||
|
{
|
||||||
|
if (poly.Length < 3) return poly;
|
||||||
|
var kept = new List<Vector2>(poly.Length);
|
||||||
|
foreach (var v in poly)
|
||||||
|
{
|
||||||
|
if (kept.Count > 0)
|
||||||
|
{
|
||||||
|
var prev = kept[^1];
|
||||||
|
if (MathF.Abs(v.X - prev.X) <= VertexMergeEpsilonNdc
|
||||||
|
&& MathF.Abs(v.Y - prev.Y) <= VertexMergeEpsilonNdc)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
kept.Add(v);
|
||||||
|
}
|
||||||
|
// Wrap-around: last ≈ first.
|
||||||
|
while (kept.Count >= 2)
|
||||||
|
{
|
||||||
|
var first = kept[0];
|
||||||
|
var last = kept[^1];
|
||||||
|
if (MathF.Abs(first.X - last.X) <= VertexMergeEpsilonNdc
|
||||||
|
&& MathF.Abs(first.Y - last.Y) <= VertexMergeEpsilonNdc)
|
||||||
|
kept.RemoveAt(kept.Count - 1);
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return kept.Count == poly.Length ? poly : kept.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside
|
// One Sutherland-Hodgman half-plane against the directed NDC edge a→b, keeping the CCW-inside
|
||||||
|
|
|
||||||
|
|
@ -144,17 +144,33 @@ public sealed class RetailPViewRenderer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append a per-building flood's cells + views into the frame. Each building cell belongs to exactly
|
// T2 (BR-4): merge a per-building flood's cells + views into the frame as a
|
||||||
// one building, so there is no cross-building overlap; ContainsKey is a safety dedup. OutsideView is
|
// UNION. Retail accumulates EVERY clipped portal polygon as a new view_poly
|
||||||
// NOT merged — the outdoor root already seeds full-screen terrain, and ConstructViewBuilding
|
// on the cell (Render::copy_view appends + view_count++, Ghidra 0x0054dfc0;
|
||||||
// (BuildFromExterior) leaves OutsideView empty (it stops at exit portals once inside the building).
|
// a cell visible through two apertures holds two views, all consumed
|
||||||
|
// downstream). The old first-wins (`ContainsKey -> continue`) dropped the
|
||||||
|
// second building flood's views whenever a cell was already in the frame —
|
||||||
|
// the multiview-loss-first-wins divergence (a named #109 suspect: per-frame
|
||||||
|
// winner flips between apertures). CellView.Add dedups exact/collinear
|
||||||
|
// re-emissions (the dac8f6a CanonicalKey), so unioning is convergent.
|
||||||
|
// 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)
|
private static void MergeBuildingFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
|
||||||
{
|
{
|
||||||
foreach (uint cellId in src.OrderedVisibleCells)
|
foreach (uint cellId in src.OrderedVisibleCells)
|
||||||
{
|
{
|
||||||
if (target.CellViews.ContainsKey(cellId))
|
if (!src.CellViews.TryGetValue(cellId, out var srcView))
|
||||||
continue;
|
continue;
|
||||||
target.CellViews[cellId] = src.CellViews[cellId];
|
|
||||||
|
if (target.CellViews.TryGetValue(cellId, out var existing))
|
||||||
|
{
|
||||||
|
foreach (var p in srcView.Polygons)
|
||||||
|
existing.Add(p);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.CellViews[cellId] = srcView;
|
||||||
target.OrderedVisibleCells.Add(cellId);
|
target.OrderedVisibleCells.Add(cellId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue