fix(camera): smooth chase-camera Z follow so the jump arc is visible on screen
Diagnostic from K-fix10 confirmed our local jump physics is
mathematically perfect — every full-charge jump produces
formulaPeak = actualPeakDz = vz²/19.6 to four-digit precision
(3.11 m for Jump skill 208). Yet the user observed retail
clients seeing the SAME character jump much higher than ACdream
sees of itself.
Root cause: ChaseCamera tracked player.Z 1:1. When the player
rises 3 m the camera rises 3 m too — the player's screen
position never changes during the arc, so the jump is visually
invisible. Retail's chase camera lags the Z follow, so an
observer sees the player visibly rise on screen.
Fix: low-pass filter the camera's Z target.
ChaseCamera.Update gains a dt parameter and an exponential
smoother:
alpha = 1 - exp(-dt / ZFollowTimeConstant)
smoothedZ += (player.Z - smoothedZ) * alpha
ZFollowTimeConstant defaults to 0.15 s — slow enough that a
~1 s jump arc shows up clearly on screen, fast enough that
slope walking still feels glued. The look-at point still uses
the raw player Z so the camera tilts up to keep the airborne
character in frame.
Drive-by: stripped K-fix10 jump diagnostic logging now that the
math has been confirmed correct.
Tests stay 1222 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
13cc08e506
commit
05ce090346
3 changed files with 43 additions and 33 deletions
|
|
@ -130,12 +130,6 @@ public sealed class PlayerMovementController
|
|||
// Jump charge state.
|
||||
private bool _jumpCharging;
|
||||
private float _jumpExtent;
|
||||
// K-fix10 diag (2026-04-26): track airborne arc height for the
|
||||
// jump-too-low investigation. Strip after fix.
|
||||
private bool _jumpDiagSampling;
|
||||
private float _jumpDiagStartZ;
|
||||
private float _jumpDiagPeakZ;
|
||||
private float _jumpDiagSentVz;
|
||||
// K-fix6 (2026-04-26): retail's PowerBar charge constant for jump is
|
||||
// not legible in the named decomp (the divisor was clobbered in
|
||||
// GetPowerBarLevel's FPU stack reordering at FUN_0056ade0). 2.0/s
|
||||
|
|
@ -397,16 +391,6 @@ public sealed class PlayerMovementController
|
|||
_motion.LeaveGround();
|
||||
outJumpExtent = _jumpExtent;
|
||||
outJumpVelocity = _body.Velocity; // capture after LeaveGround applies it
|
||||
|
||||
_jumpDiagSampling = true;
|
||||
_jumpDiagStartZ = _body.Position.Z;
|
||||
_jumpDiagPeakZ = _body.Position.Z;
|
||||
_jumpDiagSentVz = _body.Velocity.Z;
|
||||
Console.WriteLine(
|
||||
$"[jump.send] extent={_jumpExtent:F3} sentVz={_body.Velocity.Z:F3} " +
|
||||
$"sentVel=({_body.Velocity.X:F2},{_body.Velocity.Y:F2},{_body.Velocity.Z:F2}) " +
|
||||
$"formulaPeak={_body.Velocity.Z * _body.Velocity.Z / 19.6f:F2}m " +
|
||||
$"startZ={_body.Position.Z:F2}");
|
||||
}
|
||||
_jumpCharging = false;
|
||||
_jumpExtent = 0f;
|
||||
|
|
@ -451,14 +435,6 @@ public sealed class PlayerMovementController
|
|||
{
|
||||
_motion.HitGround();
|
||||
justLanded = true;
|
||||
if (_jumpDiagSampling)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[jump.peak] sentVz={_jumpDiagSentVz:F3} formulaPeak={_jumpDiagSentVz * _jumpDiagSentVz / 19.6f:F2}m " +
|
||||
$"actualPeakDz={(_jumpDiagPeakZ - _jumpDiagStartZ):F2}m " +
|
||||
$"startZ={_jumpDiagStartZ:F2} peakZ={_jumpDiagPeakZ:F2} landZ={_body.Position.Z:F2}");
|
||||
_jumpDiagSampling = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -475,10 +451,6 @@ public sealed class PlayerMovementController
|
|||
_body.calc_acceleration();
|
||||
}
|
||||
|
||||
// K-fix10 diag: peak Z tracking — placed AFTER the resolve branch
|
||||
// so it doesn't disrupt control flow.
|
||||
if (_jumpDiagSampling && _body.Position.Z > _jumpDiagPeakZ)
|
||||
_jumpDiagPeakZ = _body.Position.Z;
|
||||
|
||||
_wasAirborneLastFrame = !_body.OnWalkable;
|
||||
CellId = resolveResult.CellId;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,22 @@ public sealed class ChaseCamera : ICamera
|
|||
private float _playerYaw;
|
||||
private Vector3 _lookAt;
|
||||
|
||||
// K-fix11 (2026-04-26): smoothed Z follow. Without this the camera
|
||||
// tracks player.Z 1:1 — during a 3 m jump the camera also rises 3 m
|
||||
// and the player's screen position never changes, making the jump
|
||||
// visually invisible. A retail observer sees ACdream's jump arc
|
||||
// clearly because retail's chase camera lags the Z follow. Mirror
|
||||
// that: the camera Z target uses an exponential low-pass filter
|
||||
// toward player.Z with a ~150 ms time constant. Initialised on
|
||||
// first Update so spawn doesn't snap.
|
||||
private float _smoothedPlayerZ;
|
||||
private bool _smoothedZInitialised;
|
||||
/// <summary>Time constant for the camera-Z smoothing filter (seconds).
|
||||
/// Smaller = camera catches up faster (less perceptible jump arc on
|
||||
/// screen). Larger = jumps look bigger but camera lags on slope
|
||||
/// transitions. 0.15 = retail-feel feedback for a ~0.5–1.0 s jump.</summary>
|
||||
public float ZFollowTimeConstant { get; set; } = 0.15f;
|
||||
|
||||
public Matrix4x4 View =>
|
||||
Matrix4x4.CreateLookAt(Position, _lookAt, Vector3.UnitZ);
|
||||
|
||||
|
|
@ -53,11 +69,30 @@ public sealed class ChaseCamera : ICamera
|
|||
Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 1f, 5000f);
|
||||
|
||||
/// <summary>
|
||||
/// Update the camera position to follow the player.
|
||||
/// Update the camera position to follow the player. <paramref name="dt"/>
|
||||
/// drives the Z-smoothing filter.
|
||||
/// </summary>
|
||||
public void Update(Vector3 playerPosition, float playerYaw)
|
||||
public void Update(Vector3 playerPosition, float playerYaw, float dt = 1f / 60f)
|
||||
{
|
||||
_playerYaw = playerYaw;
|
||||
|
||||
// K-fix11: smooth the camera's Z target using an exponential
|
||||
// low-pass filter. Camera Z lags player Z so airborne motion
|
||||
// visibly shows up on screen.
|
||||
if (!_smoothedZInitialised)
|
||||
{
|
||||
_smoothedPlayerZ = playerPosition.Z;
|
||||
_smoothedZInitialised = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float alpha = 1f - MathF.Exp(-MathF.Max(dt, 1e-4f) / MathF.Max(ZFollowTimeConstant, 1e-4f));
|
||||
_smoothedPlayerZ += (playerPosition.Z - _smoothedPlayerZ) * alpha;
|
||||
}
|
||||
|
||||
// Look-at uses the actual player Z so the camera always points
|
||||
// at the character — when player rises above the lagged camera
|
||||
// the camera tilts up slightly to keep them in frame.
|
||||
_lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
|
||||
|
||||
// Camera offset: behind the player (-forward direction) plus any
|
||||
|
|
@ -72,7 +107,7 @@ public sealed class ChaseCamera : ICamera
|
|||
Position = new Vector3(
|
||||
playerPosition.X - forwardX * horizontalDist,
|
||||
playerPosition.Y - forwardY * horizontalDist,
|
||||
playerPosition.Z + EyeHeight + verticalDist);
|
||||
_smoothedPlayerZ + EyeHeight + verticalDist); // ← uses smoothed Z, not raw
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -3987,8 +3987,11 @@ public sealed class GameWindow : IDisposable
|
|||
_worldState.RelocateEntity(pe, currentLb);
|
||||
}
|
||||
|
||||
// Update chase camera.
|
||||
_chaseCamera.Update(result.Position, _playerController.Yaw);
|
||||
// Update chase camera. K-fix11 (2026-04-26): pass dt so the
|
||||
// Z-smoothing low-pass filter tracks correctly on variable
|
||||
// frame rates — without this the camera tracks player.Z 1:1
|
||||
// and jumps look invisible (camera rises with player).
|
||||
_chaseCamera.Update(result.Position, _playerController.Yaw, (float)dt);
|
||||
|
||||
// Send outbound movement messages to the live server.
|
||||
if (_liveSession is not null)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue