fix(render): Phase U.4c — root indoor visibility at the player's cell (the flap)
The visibility root + portal-side test now use the PLAYER position (visRootPos) in player mode instead of the camera EYE; the eye still drives the per-frame projection (envCellViewProj). Live ACDREAM_PROBE_FLAP evidence: the flap was the 3rd-person eye drifting out of the player's cell -> FindCameraCell returning the STALE cell for its grace frames -> the doorway portal culled as behind-the-eye -> exit cell + terrain + shells dropped (res=Grace eyeInRoot=n terrain=Skip on every flap frame). Retail's CellManager::ChangePosition (0x004559B0) tracks curr_cell by the player; acdream already roots lighting at the player (GameWindow:7152) for the same chase-cam reason — visibility was the lone holdout on the eye. Removed the earlier synthetic builder flap test, which modeled a disproven (side-test) hypothesis; the fix is integration- level, validated by the visual gate + [flap] probe. App tests 151/151. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f47895cc73
commit
0ee328a824
2 changed files with 30 additions and 54 deletions
|
|
@ -7139,7 +7139,21 @@ public sealed class GameWindow : IDisposable
|
|||
// Step 4: portal visibility — compute BEFORE the UBO upload so
|
||||
// the indoor flag drives the sun's intensity to zero for
|
||||
// dungeons (r13 §13.7).
|
||||
var visibility = _cellVisibility.ComputeVisibility(camPos);
|
||||
// Phase U.4c (2026-05-31): root indoor visibility at the PLAYER's cell, not the
|
||||
// camera EYE. Retail's CellManager::ChangePosition (0x004559B0) tracks curr_cell by
|
||||
// the player/physics position. The 3rd-person chase EYE drifts out of the player's
|
||||
// cell (through interior walls into AABB gaps); FindCameraCell then can't place the
|
||||
// eye and returns the STALE previous cell for its 3 grace frames, from which the
|
||||
// doorway portal is "behind" the eye → culled → the exit cell + terrain + shells
|
||||
// flap off. ACDREAM_PROBE_FLAP capture (2026-05-31): every flap frame is
|
||||
// res=Grace eyeInRoot=n terrain=Skip; every good frame is eyeInRoot=Y. The eye is
|
||||
// still used for the per-frame PROJECTION (envCellViewProj) — only the cell ROOT +
|
||||
// portal-side test track the player. This mirrors the playerInsideCell lighting
|
||||
// decision below, which already roots at the player for exactly this reason.
|
||||
var visRootPos = (_playerMode && _playerController is not null)
|
||||
? _playerController.Position
|
||||
: camPos;
|
||||
var visibility = _cellVisibility.ComputeVisibility(visRootPos);
|
||||
bool cameraInsideCell = visibility?.CameraCell is not null;
|
||||
|
||||
// Phase U.4 (2026-05-30): the [vis] probe moved DOWN to the unified
|
||||
|
|
@ -7285,9 +7299,12 @@ public sealed class GameWindow : IDisposable
|
|||
HashSet<uint>? envCellShellFilter = null; // drawable visible cells (cellIdToSlot keys)
|
||||
if (clipRoot is not null)
|
||||
{
|
||||
// Phase U.4c: side test + distance ordering use the PLAYER position (visRootPos,
|
||||
// stable inside the cell); projection uses the eye's envCellViewProj (the screen
|
||||
// view). See the visRootPos rationale at the ComputeVisibility call above.
|
||||
var pvFrame = PortalVisibilityBuilder.Build(
|
||||
clipRoot,
|
||||
camPos,
|
||||
visRootPos,
|
||||
id => _cellVisibility.TryGetCell(id, out var c) ? c : null,
|
||||
envCellViewProj);
|
||||
|
||||
|
|
|
|||
|
|
@ -365,59 +365,18 @@ public class PortalVisibilityBuilderTests
|
|||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Phase U.4c: the threshold "flap". A chain camera(C0) -> mid(C1) -> exit(C2)
|
||||
// where the C0->C1 portal's clip plane sits just in front of the camera.
|
||||
// BOTH poses are legitimately inside C0 and SHOULD see the exit window; they
|
||||
// straddle the C0->C1 side-test boundary by a few cm. Pre-fix, the pose just
|
||||
// behind the plane hard-culls C0->C1 (CameraOnInteriorSide), C2 is never
|
||||
// reached, and OutsideView empties — the flap. The fix must keep the exit
|
||||
// cell visible (OutsideView non-empty) at BOTH poses.
|
||||
// Phase U.4c: an earlier synthetic flap test (Build_NearBoundaryIntermediatePortal)
|
||||
// lived here. It modeled the doorway flap as a BUILDER side-test cull dropping the
|
||||
// exit cell. The live ACDREAM_PROBE_FLAP capture (2026-05-31) DISPROVED that model:
|
||||
// the real cause is the visibility ROOT being driven by the 3rd-person camera EYE —
|
||||
// the eye drifts out of the player's cell, FindCameraCell returns a STALE cell for
|
||||
// its grace frames, and the doorway is then culled as "behind" the eye. The fix is
|
||||
// at the GameWindow integration level (root visibility at the PLAYER's cell:
|
||||
// visRootPos), NOT in the builder — the builder's side test is correct and unchanged,
|
||||
// so the old test asserted a non-bug and was removed rather than left red. The fix is
|
||||
// validated by the visual gate + the [flap] probe (RenderingDiagnostics.ProbeFlapEnabled);
|
||||
// see docs/research/2026-05-31-u4c-flap-characterization.md.
|
||||
// -----------------------------------------------------------------------
|
||||
private static Matrix4x4 ViewProjAt(Vector3 eye)
|
||||
{
|
||||
var view = Matrix4x4.CreateLookAt(eye, eye + new Vector3(0, 0, -1), Vector3.UnitY);
|
||||
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
|
||||
return view * proj;
|
||||
}
|
||||
|
||||
private static (LoadedCell cam, Dictionary<uint, LoadedCell> all) FlapChain()
|
||||
{
|
||||
const uint C0 = 0x0001, C1 = 0x0002, C2 = 0x0003;
|
||||
// C0 -> C1 portal at z=-1, with a clip plane (normal +Z, InsideSide=0) at z=-1.
|
||||
// dot = camZ + D; with D = 1 the plane is at camZ = -1: inside iff camZ >= -1 - eps.
|
||||
var c0 = Cell(C0, new CellPortalInfo((ushort)C1, 0, 0, 0));
|
||||
c0.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -1f));
|
||||
c0.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = 1f, InsideSide = 0 });
|
||||
// C1 -> C2 (no clip plane → never culled), C2 has the exit window.
|
||||
var c1 = Cell(C1, new CellPortalInfo((ushort)C2, 0, 0, 0));
|
||||
c1.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f));
|
||||
var c2 = Cell(C2, new CellPortalInfo(0xFFFF, 0, 0, 0));
|
||||
c2.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -7f));
|
||||
var all = new Dictionary<uint, LoadedCell> { [C0] = c0, [C1] = c1, [C2] = c2 };
|
||||
return (c0, all);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NearBoundaryIntermediatePortal_ExitCellStaysVisibleAcrossPose()
|
||||
{
|
||||
var (cam, all) = FlapChain();
|
||||
Func<uint, LoadedCell?> lookup = id => all.TryGetValue(id, out var c) ? c : null;
|
||||
|
||||
// Pose A: a few cm IN FRONT of the C0->C1 plane (camZ = -0.9 >= -1 → inside).
|
||||
var poseA = new Vector3(0, 0, -0.9f);
|
||||
var frameA = PortalVisibilityBuilder.Build(cam, poseA, lookup, ViewProjAt(poseA));
|
||||
|
||||
// Pose B: a few cm BEHIND it (camZ = -1.1 < -1 → pre-fix the side test culls C0->C1).
|
||||
var poseB = new Vector3(0, 0, -1.1f);
|
||||
var frameB = PortalVisibilityBuilder.Build(cam, poseB, lookup, ViewProjAt(poseB));
|
||||
|
||||
// The exit cell — and therefore OutsideView — must be present at BOTH poses.
|
||||
Assert.False(frameA.OutsideView.IsEmpty, "pose A should see the exit window");
|
||||
Assert.False(frameB.OutsideView.IsEmpty,
|
||||
"pose B (a few cm away) must ALSO see the exit window — this is the flap: " +
|
||||
"an intermediate side-test flip must not drop the exit cell from the set");
|
||||
Assert.Contains(0x0003u, frameB.OrderedVisibleCells);
|
||||
}
|
||||
|
||||
// Signed-area-magnitude (shoelace) sum over a CellView's polygons in NDC.
|
||||
private static float CellViewArea(CellView view)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue