fix(render): Phase A8.F — camera collision no longer corrupts the damped eye (wall-press vibration)

Visual verification showed the camera vibrating/bouncing when pressed against a
wall. Cause: the sweep wrote its clamped result back into _dampedEye, so the
next frame's damping lerped from the wall toward the target and the sweep
re-clamped it — a per-frame feedback loop. Retail keeps viewer_sought_position
(damped, uncollided) separate from viewer (the published collided eye). Fix:
collide into a separate publishedEye for Position/View/fade and leave _dampedEye
as the clean sought position. New regression test
Update_CollisionDoesNotCorruptDampedState (clamp-then-release → full recovery).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-30 09:40:08 +02:00
parent 05161399de
commit aae5300fea
2 changed files with 58 additions and 10 deletions

View file

@ -143,19 +143,26 @@ public sealed class RetailChaseCamera : ICamera
}
// 5b. Spring-arm collision (A8.F). Retail SmartBox::update_viewer
// (0x00453ce0) sweeps viewer_sphere from the head-pivot to the
// desired eye and uses the stopped position. Keeps the eye out of
// walls so the A8.F camera-cell + portal side-tests stay stable.
// A null probe or disabled flag leaves the eye unchanged.
// (0x00453ce0) keeps TWO states: viewer_sought_position (the damped
// desired eye) and viewer (the published eye = set_viewer(curr_pos)).
// The collision produces the PUBLISHED eye each frame but must NOT
// feed back into the damped state — writing the clamped result into
// _dampedEye makes next frame's lerp start from the wall and fight
// the clamp, which shows up as visible oscillation/vibration when the
// eye is pressed against a wall. So collide into a separate local and
// leave _dampedEye as the clean, uncollided sought position.
Vector3 publishedEye = _dampedEye;
if (CameraDiagnostics.CollideCamera && CollisionProbe is not null)
_dampedEye = CollisionProbe.SweepEye(pivotWorld, _dampedEye, cellId, selfEntityId);
publishedEye = CollisionProbe.SweepEye(pivotWorld, _dampedEye, cellId, selfEntityId);
// 6. Publish renderer surface.
Position = _dampedEye;
View = Matrix4x4.CreateLookAt(_dampedEye, _dampedEye + _dampedForward, new Vector3(0f, 0f, 1f));
// 6. Publish renderer surface (from the collided eye; rotation stays the
// smoothly-damped look direction toward the pivot).
Position = publishedEye;
View = Matrix4x4.CreateLookAt(publishedEye, publishedEye + _dampedForward, new Vector3(0f, 0f, 1f));
// 7. Auto-fade translucency.
float d = Vector3.Distance(_dampedEye, pivotWorld);
// 7. Auto-fade translucency — uses the published (collided) eye so the
// player fades once the eye is pulled in close.
float d = Vector3.Distance(publishedEye, pivotWorld);
PlayerTranslucency = ComputeTranslucency(d);
}