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

@ -531,4 +531,45 @@ public class RetailChaseCameraTests
Assert.Equal(pulledIn, cam.Position);
Assert.Equal(1f, cam.PlayerTranslucency, 3);
}
// Probe that clamps the eye to a fixed point on the FIRST call, then
// releases (returns the requested eye unchanged) on later calls.
private sealed class ClampThenReleaseProbe : ICameraCollisionProbe
{
public int Calls;
public Vector3 ClampEye;
public Vector3 SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId)
{
Calls++;
return Calls == 1 ? ClampEye : desiredEye;
}
}
[Fact]
public void Update_CollisionDoesNotCorruptDampedState()
{
// Regression for the wall-press vibration: the sweep must NOT write its
// clamped result back into the damped "sought" eye (retail keeps
// viewer_sought_position separate from viewer). Frame 1 clamps the eye
// near the pivot; frame 2 releases. With the damp state kept clean, the
// published eye returns straight to the (constant) target on frame 2; if
// it were corrupted, frame 2 would only lerp ~7.5% back from the clamp
// and stay pinned near it.
CameraDiagnostics.CollideCamera = true;
var probe = new ClampThenReleaseProbe { ClampEye = new Vector3(0f, 0f, 2f) };
var cam = new RetailChaseCamera { CollisionProbe = probe };
void Step() => cam.Update(
playerPosition: Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero,
isOnGround: true, contactPlaneNormal: Vector3.UnitZ, dt: 1f / 60f,
cellId: 0x100, selfEntityId: 0x5);
Step(); // frame 1: clamps to (0,0,2)
Step(); // frame 2: releases
// Constant pose → target eye ≈ (-2.5, 0, 2.25). Full recovery means
// Position.X is near the target (< -2), not pinned near the clamp (X≈0).
Assert.True(cam.Position.X < -2f,
$"published eye should fully recover to the target after release, got {cam.Position}");
}
}