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:
parent
9601ef39c3
commit
d2212cfaea
3 changed files with 688 additions and 2 deletions
|
|
@ -77,6 +77,16 @@ public sealed class RetailChaseCamera : ICamera
|
|||
public const float PitchMin = -0.7f;
|
||||
public const float PitchMax = 1.4f;
|
||||
|
||||
// Retail CameraManager::UpdateCamera convergence-snap thresholds (decomp
|
||||
// acclient_2013_pseudo_c.txt, 0x00456fcd–0x00457035). SnapEpsilon = 2 ×
|
||||
// 0.000199999995 m ≈ 0.0004 m — the per-frame translation step below which retail
|
||||
// freezes the boom at an exact fixed point (0x00456fe1). RotCloseEpsilon =
|
||||
// 0.000199999995 — the Frame::close_rotation tolerance (0x00456fdd). Without the
|
||||
// snap, Vector3.Lerp asymptotes forever and the boom drifts at rest, walking the eye
|
||||
// across a portal plane and flipping the viewer cell → the indoor flicker.
|
||||
private const float SnapEpsilon = 0.000199999995f * 2f;
|
||||
private const float RotCloseEpsilon = 0.000199999995f;
|
||||
|
||||
// ── Damped state ────────────────────────────────────────────────
|
||||
|
||||
private readonly Vector3[] _velocityRing = new Vector3[5];
|
||||
|
|
@ -146,8 +156,14 @@ public sealed class RetailChaseCamera : ICamera
|
|||
{
|
||||
float tAlpha = ComputeDampingAlpha(CameraDiagnostics.TranslationStiffness, dt);
|
||||
float rAlpha = ComputeDampingAlpha(CameraDiagnostics.RotationStiffness, dt);
|
||||
_dampedEye = Vector3.Lerp(_dampedEye, targetEye, tAlpha);
|
||||
_dampedForward = Vector3.Normalize(Vector3.Lerp(_dampedForward, targetForward, rAlpha));
|
||||
Vector3 candidateEye = Vector3.Lerp(_dampedEye, targetEye, tAlpha);
|
||||
Vector3 candidateForward = Vector3.Normalize(Vector3.Lerp(_dampedForward, targetForward, rAlpha));
|
||||
|
||||
// Retail UpdateCamera convergence snap (0x00456fcd): freeze at an exact fixed
|
||||
// point once the lerp step is sub-epsilon, instead of dithering forever. This is
|
||||
// the at-rest flicker fix — see ApplyConvergenceSnap + SnapEpsilon.
|
||||
(_dampedEye, _dampedForward, _) =
|
||||
ApplyConvergenceSnap(_dampedEye, _dampedForward, candidateEye, candidateForward);
|
||||
}
|
||||
|
||||
// 5b. Spring-arm collision (A8.F). Retail SmartBox::update_viewer
|
||||
|
|
@ -369,6 +385,26 @@ public sealed class RetailChaseCamera : ICamera
|
|||
return a;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CameraManager::UpdateCamera</c> convergence snap (decomp 0x00456fcd).
|
||||
/// After the per-frame lerp, if the translation step from <paramref name="dampedEye"/>
|
||||
/// to <paramref name="candidateEye"/> is below <see cref="SnapEpsilon"/> AND the
|
||||
/// rotation step is below <see cref="RotCloseEpsilon"/>, retail returns the input
|
||||
/// position unchanged — an exact fixed point. Returns <c>frozen=true</c> with the
|
||||
/// current state in that case; otherwise <c>frozen=false</c> with the candidate.
|
||||
/// Both conditions are required (retail couples origin + rotation in the snap test),
|
||||
/// so the boom keeps converging while the heading is still turning.
|
||||
/// </summary>
|
||||
internal static (Vector3 eye, Vector3 forward, bool frozen) ApplyConvergenceSnap(
|
||||
Vector3 dampedEye, Vector3 dampedForward, Vector3 candidateEye, Vector3 candidateForward)
|
||||
{
|
||||
bool translationConverged = Vector3.Distance(candidateEye, dampedEye) < SnapEpsilon;
|
||||
bool rotationConverged = Vector3.Distance(candidateForward, dampedForward) < RotCloseEpsilon;
|
||||
if (translationConverged && rotationConverged)
|
||||
return (dampedEye, dampedForward, true); // freeze: exact fixed point
|
||||
return (candidateEye, candidateForward, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Low-pass filter for a single mouse axis. Mirrors retail's
|
||||
/// <c>CameraSet::FilterMouseInput</c>: if last sample was within
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue