fix(render): Part 1 — camera boom convergence snap (kills the at-rest viewer-cell flicker trigger)

Port retail CameraManager::UpdateCamera's convergence snap (0x00456fcd):
once the per-frame lerp step is below 0.0004 m AND the rotation within
0.000199999995, freeze the damped eye at an exact fixed point instead of
Vector3.Lerp's endless sub-mm asymptote. The drift was walking the 3rd-person
eye across the vestibule/room portal plane at rest, flipping the per-frame
viewer-cell resolve 0170<->0171 -> the indoor grey/texture flicker. The
collided-eye firewall (separate publishedEye local) is already present.

Adds ApplyConvergenceSnap static (TDD: 3 unit tests + 1 integration freeze
test) + SnapEpsilon/RotCloseEpsilon. App suite 183 -> 187, all green.

Plan: docs/superpowers/plans/2026-06-05-indoor-viewer-cell-flicker-fix.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-05 15:56:04 +02:00
parent 9601ef39c3
commit d2212cfaea
3 changed files with 688 additions and 2 deletions

View file

@ -605,4 +605,102 @@ public class RetailChaseCameraTests
Assert.True(cam.Position.X < -2f,
$"published eye should fully recover to the target after release, got {cam.Position}");
}
// ── Convergence snap (Part 1: kills the at-rest boom drift) ────────
[Fact]
public void ConvergenceSnap_StepBelowEpsilon_FreezesAtCurrent()
{
// Both the translation step and the rotation step are below the retail snap
// thresholds (0.0004 m / 0.0002) → freeze: return the CURRENT damped state,
// not the candidate. This is the exact fixed point retail's UpdateCamera reaches.
var damped = new Vector3(5f, 6f, 7f);
var forward = Vector3.Normalize(new Vector3(1f, 0f, 0f));
var candidate = damped + new Vector3(0.0001f, 0f, 0f); // 0.1 mm step < 0.4 mm
var candFwd = forward; // no rotation step
var (eye, fwd, frozen) = RetailChaseCamera.ApplyConvergenceSnap(damped, forward, candidate, candFwd);
Assert.True(frozen);
Assert.Equal(damped, eye); // exact — returns the input, freezing the drift
Assert.Equal(forward, fwd);
}
[Fact]
public void ConvergenceSnap_TranslationStepAboveEpsilon_ReturnsCandidate()
{
var damped = new Vector3(5f, 6f, 7f);
var forward = Vector3.Normalize(new Vector3(1f, 0f, 0f));
var candidate = damped + new Vector3(0.01f, 0f, 0f); // 1 cm step ≫ 0.4 mm
var candFwd = forward;
var (eye, fwd, frozen) = RetailChaseCamera.ApplyConvergenceSnap(damped, forward, candidate, candFwd);
Assert.False(frozen);
Assert.Equal(candidate, eye); // still converging → apply the lerp step
Assert.Equal(candFwd, fwd);
}
[Fact]
public void ConvergenceSnap_RotationStepAboveEpsilon_ReturnsCandidate()
{
// Translation has converged but the heading is still turning — retail does NOT
// freeze unless BOTH are close (it returns the interpolated frame). So a small
// translation step must NOT freeze while the forward is still rotating.
var damped = new Vector3(5f, 6f, 7f);
var forward = Vector3.Normalize(new Vector3(1f, 0f, 0f));
var candidate = damped + new Vector3(0.0001f, 0f, 0f); // sub-epsilon translation
var candFwd = Vector3.Normalize(new Vector3(1f, 0.05f, 0f)); // ~0.05 rad turn ≫ 0.0002
var (_, _, frozen) = RetailChaseCamera.ApplyConvergenceSnap(damped, forward, candidate, candFwd);
Assert.False(frozen);
}
[Fact]
public void Update_AtRestAfterConvergence_BoomFreezesAtExactFixedPoint()
{
// The retail UpdateCamera snap freezes the boom at an exact fixed point once the
// per-frame step falls below ~0.4 mm. Without it, Vector3.Lerp asymptotes forever
// — the eye dithers sub-millimetre every frame and walks across the portal plane,
// flipping the viewer cell (the indoor flicker). Hold a pose DIFFERENT from the
// init pose so the boom has to converge over many frames; with collision OFF
// (Position == _dampedEye), two consecutive post-convergence frames must be
// BIT-IDENTICAL. (At frame 120, α≈0.075, displacement ~7 m, the un-snapped step is
// ~5e-5 m ≈ tens of float ULP — distinguishably nonzero — so this is a real RED.)
bool savedAlign = CameraDiagnostics.AlignToSlope;
bool savedColl = CameraDiagnostics.CollideCamera;
float savedT = CameraDiagnostics.TranslationStiffness;
float savedR = CameraDiagnostics.RotationStiffness;
try
{
CameraDiagnostics.AlignToSlope = false; // deterministic heading
CameraDiagnostics.CollideCamera = false; // Position == _dampedEye
CameraDiagnostics.TranslationStiffness = 0.45f;
CameraDiagnostics.RotationStiffness = 0.45f;
var cam = new RetailChaseCamera { Distance = 2.61f, Pitch = 0.291f };
// Frame 1 at pose A: init snaps the damped eye to A's target.
cam.Update(Vector3.Zero, 0.5f, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f);
// Hold pose B for many frames → the boom lerps A's target → B's target.
var posB = new Vector3(5f, 5f, 0f);
for (int i = 0; i < 120; i++)
cam.Update(posB, 0.5f, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f);
Vector3 a = cam.Position;
cam.Update(posB, 0.5f, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f);
Vector3 b = cam.Position;
Assert.Equal(a, b); // exact — frozen, not dithering
}
finally
{
CameraDiagnostics.AlignToSlope = savedAlign;
CameraDiagnostics.CollideCamera = savedColl;
CameraDiagnostics.TranslationStiffness = savedT;
CameraDiagnostics.RotationStiffness = savedR;
}
}
}