fix(runtime/camera) #429: presented player and chase camera share the object clock
Two halves of the felt run-hitch (the visible one-frame player lurch): - The presentation lerp normalized the pending object-clock time by the fixed 30 Hz MinQuantum, but retail's object clock simulates VARIABLE-length quanta (CPhysicsObj::update_object 0x00515D10: capped at MaxQuantum, everything above MinQuantum runs as ONE step). After a long frame the view froze for the quantum and then fast-replayed it. ComputeRenderPosition now spans the ACTUAL last quantum (_lastQuantumSeconds), and PresentedDeltaSeconds accounts continuous presented time across quantum boundaries. - The chase camera damped toward the presented player using wall dt while the player presents on the object clock, so a long frame stepped the camera far past the under-advanced player — measured up to ~1 m of camera/player decoherence in a single frame. Retail ties camera update to the physics-update callback (SmartBox::PlayerPhysicsUpdatedCallback 0x00452d60), i.e. the same clock as the body; both chase cameras now integrate PresentedDeltaSeconds. Manual zoom/pitch adjustment stays on wall dt (a user-input rate, not target chasing). Owner gate: camera-vs-player boom-length change fell from ~1 m spikes to 0.2-1.2 cm median on long frames; teleports settle clean. Two Runtime tests updated to pin the continuous-rate contract. The temporary PlayerPresentationProbe apparatus that measured this is retired with the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0330fcd0d1
commit
4873c10673
5 changed files with 110 additions and 100 deletions
|
|
@ -546,8 +546,14 @@ public class PlayerMovementControllerTests
|
|||
Assert.True(halfFrame.RenderPosition.X < firstTick.Position.X,
|
||||
$"Render X={halfFrame.RenderPosition.X} should stay between {start.X} and {firstTick.Position.X}");
|
||||
|
||||
float expectedMidpoint = start.X + ((firstTick.Position.X - start.X) * 0.5f);
|
||||
Assert.Equal(expectedMidpoint, halfFrame.RenderPosition.X, precision: 3);
|
||||
// #429 defect 2 (residual): the interpolation normalizes by the LAST
|
||||
// SIMULATED QUANTUM's length — here the ObjectTick-long remainder
|
||||
// quantum, not the fixed MinQuantum — so the presented position
|
||||
// advances at a continuous rate across variable-length quanta instead
|
||||
// of freezing then over-speeding after a long host frame.
|
||||
float alpha = (PhysicsBody.MinQuantum * 0.5f) / ObjectTick;
|
||||
float expected = start.X + ((firstTick.Position.X - start.X) * alpha);
|
||||
Assert.Equal(expected, halfFrame.RenderPosition.X, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -653,27 +659,32 @@ public class PlayerMovementControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_LeftoverAboveMinQuantum_ClampsRenderAlphaToCurrentPhysicsPosition()
|
||||
public void Update_LeftoverAboveMinQuantum_InterpolatesAcrossTheActualQuantumInterval()
|
||||
{
|
||||
var engine = MakeFlatEngine();
|
||||
var controller = new PlayerMovementController(engine);
|
||||
controller.SeedPlacementForTest(new Vector3(96f, 96f, 50f), 0x0001, new Vector3(96f, 96f, 50f));
|
||||
var start = new Vector3(96f, 96f, 50f);
|
||||
controller.SeedPlacementForTest(start, 0x0001, start);
|
||||
controller.Yaw = 0f;
|
||||
|
||||
var result = controller.Update(
|
||||
PhysicsBody.MaxQuantum + PhysicsBody.MinQuantum,
|
||||
new MovementInput(Forward: true));
|
||||
|
||||
// Tolerance, not decimal `precision:` — the AP-7 friction port (P2)
|
||||
// shifts the velocity-fallback trajectory by micrometers, and
|
||||
// Math.Round-based precision comparison fails when two essentially
|
||||
// equal values straddle a 5e-5 rounding boundary (observed: X
|
||||
// 96.3427505 vs 96.3427429 — a 7.6 µm gap rounding to 96.3428 vs
|
||||
// 96.3427). The clamp contract is "render == physics for
|
||||
// presentation"; 1 mm is far below visibility and boundary-immune.
|
||||
Assert.Equal(result.Position.X, result.RenderPosition.X, tolerance: 1e-3f);
|
||||
Assert.Equal(result.Position.Y, result.RenderPosition.Y, tolerance: 1e-3f);
|
||||
Assert.Equal(result.Position.Z, result.RenderPosition.Z, tolerance: 1e-3f);
|
||||
// #429 defect 2 (residual): one MaxQuantum step simulates and
|
||||
// MinQuantum is retained as pending, so the prev→curr interpolation
|
||||
// pair spans a MaxQuantum-long interval and the presented position
|
||||
// sits MinQuantum INTO it — lerp(start, Position, Min/Max) — rather
|
||||
// than clamping onto the authoritative body. The former clamp
|
||||
// contract ("render == physics when leftover >= MinQuantum")
|
||||
// presented a forward rate spike after every long host frame; the
|
||||
// continuous-rate contract is what keeps the presentation and the
|
||||
// chase camera (which integrates PresentedDeltaSeconds) coherent.
|
||||
float alpha = PhysicsBody.MinQuantum / PhysicsBody.MaxQuantum;
|
||||
Vector3 expected = Vector3.Lerp(start, result.Position, alpha);
|
||||
Assert.Equal(expected.X, result.RenderPosition.X, tolerance: 1e-3f);
|
||||
Assert.Equal(expected.Y, result.RenderPosition.Y, tolerance: 1e-3f);
|
||||
Assert.Equal(expected.Z, result.RenderPosition.Z, tolerance: 1e-3f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue