research(render): Phase U.4c — DISPROVE the side-test fix (PortalSide port is a no-op)

InitCell decode (PortalFlags.PortalSide=0x2) + a swept-pose A8CellAudit comparison
(O=centroid, A=winding-corrected PortalSide, B=opposite) over the real flap cells.
A is IDENTICAL to O at every pose/every portal — the (Flags&2)==0 boolean convention
makes the dat PortalSide sense equal to our centroid sense, so swapping is a no-op
and cannot fix the flap. B culls true-interior poses (wrong polarity). Conclusion:
the flap is NOT the side-test sense — it's the 3rd-person camera eye crossing an
interior portal plane while FindCameraCell still roots in the cell; ANY plane-side
test culls there. No production code changed (no no-op shipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-31 10:32:52 +02:00
parent fdeede8796
commit b5f2bf2b8f
2 changed files with 207 additions and 15 deletions

View file

@ -189,11 +189,20 @@ static void DumpCellPortals(DatCollection dats, uint envCellId)
(missing.Count > 0 ? $" MISSING=[{string.Join(",", missing)}]" : "");
}
// U.4c characterization: compute the plane + centroid-derived InsideSide
// EXACTLY as GameWindow.BuildLoadedCell does, and compare its traversal
// SENSE against the dat's authored PortalSide flag (retail portal_side).
// If they disagree for a portal the camera should see through, that is the
// flap's root: our centroid guess culls where retail's authored bit traverses.
// U.4c side-test validation (supersedes the earlier `retailTraversesAtCentroid`
// column, which the characterization doc flagged UNRELIABLE). Builds the portal
// plane EXACTLY as GameWindow.BuildLoadedCell (Cross(p1-p0,p2-p0), d=-dot(N,p0)),
// derives the OLD centroid InsideSide, reads the dat PortalSide, and runs THREE
// candidate side senses for a camera SWEPT from the cell centroid toward (and a
// bit past) the portal plane:
// O = OLD centroid sense (CameraOnInteriorSide pre-fix)
// A = PortalSide, winding-corrected (docs/research/2026-05-31-u4c-initcell-pseudocode.md)
// B = PortalSide, doc-§314 literal intuition (opposite polarity)
// FINDING (2026-05-31): for the real Holtburg cottage cells, A is BYTE-IDENTICAL
// to O at every pose (the PortalSide swap is a NO-OP for the flap), and B culls
// true-interior poses (wrong polarity). No side-test sense fixes the flap — the
// flap is the 3rd-person eye legitimately crossing the portal plane while still
// rooted in the cell, which any plane-side test culls. See the BLOCKED report.
string sideText = "no-cellStruct";
if (cellStruct is not null && cellStruct.Polygons.TryGetValue(portal.PolygonId, out var sp)
&& sp.VertexIds.Count >= 3
@ -210,19 +219,40 @@ static void DumpCellPortals(DatCollection dats, uint envCellId)
{ mn = Vector3.Min(mn, v.Origin); mx = Vector3.Max(mx, v.Origin); }
var centroid = (mn + mx) * 0.5f;
float centroidDot = Vector3.Dot(n, centroid) + d;
int ourInsideSide = centroidDot >= 0 ? 0 : 1; // GameWindow.cs:5648
int datPortalSide = portal.Flags.HasFlag(PortalFlags.PortalSide) ? 1 : 0;
// Our test traverses when the camera is on the centroid's side of the plane.
// Retail (InitCell 432962) traverses when computed-sidedness != portal_side,
// where sidedness==1 means "in front" (dot>eps). At the centroid our test
// always traverses; the meaningful question is whether the centroid side
// (front if centroidDot>0) matches retail's traverse-sense for this portal.
int centroidSidedness = centroidDot > 0 ? 1 : 0; // 1=front
bool retailTraversesAtCentroid = centroidSidedness != datPortalSide;
int ourInsideSide = centroidDot >= 0 ? 0 : 1; // GameWindow.cs:5648
bool portalSide = !portal.Flags.HasFlag(PortalFlags.PortalSide); // PortalInfo.cs:44 bool
int datPortalSide = portalSide ? 0 : 1; // raw bit (0 = bool true)
const float eps = 0.01f; // PortalVisibilityBuilder.PortalSideEpsilon
// Nearest portal-polygon vertex = the plane-ward sweep anchor.
var pnear = s0; float bestD = centroidDot;
foreach (var vid in sp.VertexIds)
if (TryGetOrigin(cellStruct, (ushort)vid, out var vv))
{
float vd = Vector3.Dot(n, vv) + d;
if (Math.Abs(vd) < Math.Abs(bestD)) { bestD = vd; pnear = vv; }
}
static bool OldSense(int side, float dot, float e) => side == 0 ? dot >= -e : dot <= e;
static bool SenseA(bool ps, float dot, float e) => ps ? dot < e : dot > -e;
static bool SenseB(bool ps, float dot, float e) => ps ? dot > -e : dot < e;
bool oAll = true, aAll = true, bAll = true;
var trace = new List<string>();
foreach (float t in new[] { 0f, 0.25f, 0.5f, 0.75f, 1.0f, 1.15f, 1.3f })
{
var cam = Vector3.Lerp(centroid, pnear, t);
float dot = Vector3.Dot(n, cam) + d;
bool os = OldSense(ourInsideSide, dot, eps);
bool a = SenseA(portalSide, dot, eps);
bool b = SenseB(portalSide, dot, eps);
if (!os) oAll = false; if (!a) aAll = false; if (!b) bAll = false;
trace.Add($"t{t:F2}:D={dot:F2}/O={(os ? "T" : "x")}/A={(a ? "T" : "x")}/B={(b ? "T" : "x")}");
}
sideText =
$"N=({n.X:F2},{n.Y:F2},{n.Z:F2}) d={d:F2} centroidDot={centroidDot:F3} " +
$"ourInsideSide={ourInsideSide} datPortalSide={datPortalSide} " +
$"retailTraversesAtCentroid={(retailTraversesAtCentroid ? "Y" : "n")}";
$"sweepTraversesAll[OLD={oAll} A={aAll} B={bAll}] [{string.Join(" ", trace)}]";
}
Console.WriteLine(