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:
parent
fdeede8796
commit
b5f2bf2b8f
2 changed files with 207 additions and 15 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue