From 9b1857ac52849f6c8fae8a4bd524b0fae9d96e7e Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 8 Jun 2026 15:08:25 +0200 Subject: [PATCH] =?UTF-8?q?Revert=20"fix(camera):=20rest-snap=20render=20p?= =?UTF-8?q?osition=20=E2=80=94=20kills=20the=20indoor=20doorway=20standing?= =?UTF-8?q?-still=20flicker"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit cd974b29bccf27de2a66e898195fb1ee8c0f0325. --- .../Input/PlayerMovementController.cs | 25 +------ .../Input/PlayerMovementControllerTests.cs | 67 ------------------- 2 files changed, 1 insertion(+), 91 deletions(-) diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index d422738f..5332fe37 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -810,30 +810,7 @@ public sealed class PlayerMovementController private Vector3 ComputeRenderPosition() { float alpha = Math.Clamp(_physicsAccum / PhysicsBody.MinQuantum, 0f, 1f); - return ComputeRenderPosition(_prevPhysicsPos, _currPhysicsPos, _body.Position, _body.Velocity, alpha); - } - - // Render-position rest-snap (2026-06-08, indoor doorway flap). At rest the authoritative - // body position is byte-stable, but the two physics-tick snapshots (prev/curr) can lag it by - // microns — the per-frame resolve edge-settles the resting sphere against doorframe geometry - // after the last tick wrote curr — so Lerp(prev, curr, alpha) with a per-frame-VARYING - // leftover-accumulator alpha dithers the render position by microns. The grazing-doorframe - // camera-collision sweep (PhysicsCameraCollisionProbe.SweepEye) amplifies that ~1000x into a - // ~1.3 mm eye jitter that trips the portal-flood clip → the standing-still indoor flicker - // (pinned live, flap-churn.log: rawPlayer 1 distinct, RenderPosition 15 distinct, eye 17). - // When the body is at rest (velocity below epsilon) render AT the authoritative position so - // the camera's pivot input is byte-stable. Mirrors retail (a resting object renders - // bit-stable) + the boom convergence snap (RetailChaseCamera.ApplyConvergenceSnap, d2212cf), - // one layer earlier. Interpolation between tick snapshots is preserved during motion - // (velocity above epsilon), so sub-tick movement stays smooth. - internal const float RestVelocityEpsilonSq = 1e-4f; // (0.01 m/s)^2 — below this the body is at rest - - internal static Vector3 ComputeRenderPosition( - Vector3 prevPhysicsPos, Vector3 currPhysicsPos, Vector3 bodyPosition, Vector3 bodyVelocity, float alpha) - { - if (bodyVelocity.LengthSquared() < RestVelocityEpsilonSq) - return bodyPosition; // at rest: render at the authoritative byte-stable position - return Vector3.Lerp(prevPhysicsPos, currPhysicsPos, alpha); + return Vector3.Lerp(_prevPhysicsPos, _currPhysicsPos, alpha); } public MovementResult Update(float dt, MovementInput input) diff --git a/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs b/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs index 260fd73a..1dd41b37 100644 --- a/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs +++ b/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs @@ -224,73 +224,6 @@ public class PlayerMovementControllerTests Assert.Equal(result.Position.Z, result.RenderPosition.Z, precision: 4); } - // ── Indoor doorway flap: render-position rest-snap (2026-06-08) ─────────── - // - // Live pin (flap-churn.log, user at the cottage doorway): the physics body is - // byte-stable at rest (rawPlayer = 1 distinct value), but the render position - // (Lerp of the two physics-tick snapshots) jitters ~µm and the camera EYE - // jitters ~1.3 mm — a ~1000x amplification by the grazing-doorframe camera- - // collision sweep, which trips the portal clip → the standing-still flicker. - // The dither is structural: at rest the tick snapshots (_prevPhysicsPos / - // _currPhysicsPos) can lag the settled authoritative Position by microns (door- - // frame edge-settle in the per-frame resolve), so Lerp(prev, curr, alpha) with a - // per-frame-VARYING alpha sweeps a tiny segment instead of holding still. Fix: - // at rest (velocity below epsilon) render AT the authoritative body position — - // byte-identical and alpha-independent. Mirrors retail (a resting object renders - // bit-stable) and the boom convergence snap (RetailChaseCamera.ApplyConvergenceSnap, - // d2212cf), one layer earlier so the camera's pivot input is byte-stable too. - // - // The flat-terrain controller tests above CANNOT reproduce the doorframe-specific - // prev!=curr-at-rest condition (flat terrain collapses prev==curr), so these test - // the pure rest-snap function directly; the end-to-end acceptance is the live - // doorway visual gate. - [Fact] - public void ComputeRenderPosition_AtRestWithStaleEndpoints_SnapsToAuthoritativePosition_NoAlphaDither() - { - // prev lags curr by 30 µm (the live doorframe edge-settle lag); body = the settled - // authoritative position; velocity = 0 (at rest). Two different leftover-accumulator - // alphas must BOTH return the authoritative position, byte-identical (no dither). - var prev = new Vector3(155.525116f, 14.225600f, 94f); - var curr = new Vector3(155.525146f, 14.225600f, 94f); - var body = new Vector3(155.525146f, 14.225600f, 94f); - - var lowAlpha = PlayerMovementController.ComputeRenderPosition(prev, curr, body, Vector3.Zero, alpha: 0.15f); - var highAlpha = PlayerMovementController.ComputeRenderPosition(prev, curr, body, Vector3.Zero, alpha: 0.93f); - - Assert.Equal(body, lowAlpha); // byte-identical to the authoritative position - Assert.Equal(lowAlpha, highAlpha); // alpha-independent at rest (no dither) - } - - [Fact] - public void ComputeRenderPosition_Moving_InterpolatesBetweenTickSnapshots() - { - // Guard the no-over-fire half: while moving (velocity well above the rest epsilon) - // the render position must still interpolate smoothly between the tick snapshots. - var prev = new Vector3(96.0f, 96f, 50f); - var curr = new Vector3(96.3f, 96f, 50f); - var moving = new Vector3(3.12f, 0f, 0f); // walk speed - - var half = PlayerMovementController.ComputeRenderPosition(prev, curr, curr, moving, alpha: 0.5f); - - Assert.Equal(96.15f, half.X, precision: 3); // midpoint — interpolation preserved - } - - [Fact] - public void Update_AtRest_BodyVelocityBelowRenderRestSnapThreshold() - { - // Precondition for the render-position rest-snap: a settled grounded body's velocity must - // be below RestVelocityEpsilonSq, else ComputeRenderPosition's gate never fires at rest and - // the doorway flicker persists. kill_velocity on grounded contact drives it to zero. - var engine = MakeFlatEngine(); - var controller = new PlayerMovementController(engine); - controller.SetPosition(new Vector3(96f, 96f, 50f), 0x0001); - for (int i = 0; i < 60; i++) controller.Update(1f / 60f, new MovementInput()); - - Assert.True( - controller.BodyVelocity.LengthSquared() < PlayerMovementController.RestVelocityEpsilonSq, - $"resting body velocity {controller.BodyVelocity.Length()} m/s must be below the rest-snap threshold"); - } - [Fact] public void Update_RunForward_MoveFasterThanWalk() {