fix(camera): retail-faithful jump-tracking via contact-plane projection
Original symptom: jumping made the camera swing around the player
vertically — the basis tilted up/down with the player's Z velocity.
Root cause: ComputeHeading used the raw 3D velocity vector as the
heading direction. During a jump, velocity has a substantial Z
component (vy ≈ jump speed), and `normalize((vx, vy, vz))` produced
a heading pointing up. The basis tilted accordingly and the camera
went under/over the player.
Retail's actual ALIGN_WITH_PLANE algorithm (decomp at
acclient_2013_pseudo_c.txt:95644-95795) is different:
1. Velocity is only used as a gate. If |vx| AND |vy| > epsilon
(player is moving in XY), proceed; otherwise fall back to the
LOOK_IN_DIRECTION path (player's facing direction unchanged).
2. The base heading is `localtoglobalvec(player, (0, 1, 0))` —
the player's local +Y axis in world space, which in our
convention is `(cos yaw, sin yaw, 0)`.
3. Pick a surface normal:
grounded: contact_plane.N
airborne: (0, 0, 1) [world up]
4. Project the base heading onto the plane perpendicular to that
normal: projected = forward - normal * dot(forward, normal).
5. Normalize. Fall back to the base if projection collapses.
Behaviorally:
* Standing jump (vx≈0, vy≈0): gate fails → base heading. Camera
doesn't move with the jump.
* Running jump (vx, vy, vz all nonzero, airborne): projects onto
world up → no-op since base is already horizontal. Camera basis
stays horizontal; player visibly rises in frame.
* Walking uphill (grounded, slope normal tilted): projection
adds a Z component matching the slope angle. Camera basis tilts
with the terrain.
* Walking on flat ground: projection is a no-op. Camera basis
horizontal.
Surface changes:
* RetailChaseCamera.ComputeHeading gains `isOnGround` and
`contactPlaneNormal` parameters.
* RetailChaseCamera.Update gains the same two parameters and
threads them through.
* GameWindow's two Update call sites pass `result.IsOnGround` and
`_playerController.ContactPlane.Normal` (already exposed on
PlayerMovementController — no plumbing change there).
* Tests: 2 existing heading tests reshaped (Moving* and Uphill);
2 new tests added (AirborneJumping straight-up + running-jump);
1 renamed (SlopeAlignDisabled). Net 25 → 27 tests in
RetailChaseCameraTests; full AcDream.App.Tests: 39 → 41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f30e13317
commit
b7e954e50b
3 changed files with 188 additions and 34 deletions
|
|
@ -4757,7 +4757,11 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
// 4. Recenter chase camera on the new position.
|
// 4. Recenter chase camera on the new position.
|
||||||
_chaseCamera?.Update(snappedPos, _playerController.Yaw);
|
_chaseCamera?.Update(snappedPos, _playerController.Yaw);
|
||||||
_retailChaseCamera?.Update(snappedPos, _playerController.Yaw, System.Numerics.Vector3.Zero, dt: 1f / 60f);
|
_retailChaseCamera?.Update(snappedPos, _playerController.Yaw,
|
||||||
|
playerVelocity: System.Numerics.Vector3.Zero,
|
||||||
|
isOnGround: true,
|
||||||
|
contactPlaneNormal: System.Numerics.Vector3.UnitZ,
|
||||||
|
dt: 1f / 60f);
|
||||||
|
|
||||||
// 5. Return to InWorld.
|
// 5. Return to InWorld.
|
||||||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||||
|
|
@ -6429,10 +6433,18 @@ public sealed class GameWindow : IDisposable
|
||||||
_chaseCamera.Update(result.RenderPosition, _playerController.Yaw,
|
_chaseCamera.Update(result.RenderPosition, _playerController.Yaw,
|
||||||
isOnGround: result.IsOnGround,
|
isOnGround: result.IsOnGround,
|
||||||
dt: (float)dt);
|
dt: (float)dt);
|
||||||
// RetailChaseCamera: takes world velocity for slope-aligned heading;
|
// RetailChaseCamera: heading is the player's facing direction
|
||||||
// jump-feedback falls out of damping naturally, no isOnGround needed.
|
// projected onto the contact plane when grounded, or the
|
||||||
|
// world XY plane when airborne. The contact plane normal
|
||||||
|
// tilts the camera basis with terrain; the airborne
|
||||||
|
// fallback keeps the basis horizontal during jumps so the
|
||||||
|
// player visibly rises in frame without the camera
|
||||||
|
// swinging vertically (was the symptom of using raw
|
||||||
|
// velocity-vector heading).
|
||||||
_retailChaseCamera!.Update(result.RenderPosition, _playerController.Yaw,
|
_retailChaseCamera!.Update(result.RenderPosition, _playerController.Yaw,
|
||||||
playerVelocity: _playerController.BodyVelocity,
|
playerVelocity: _playerController.BodyVelocity,
|
||||||
|
isOnGround: result.IsOnGround,
|
||||||
|
contactPlaneNormal: _playerController.ContactPlane.Normal,
|
||||||
dt: (float)dt);
|
dt: (float)dt);
|
||||||
|
|
||||||
// Send outbound movement messages to the live server.
|
// Send outbound movement messages to the live server.
|
||||||
|
|
|
||||||
|
|
@ -83,14 +83,28 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
/// <see cref="View"/>, and <see cref="PlayerTranslucency"/> reflect
|
/// <see cref="View"/>, and <see cref="PlayerTranslucency"/> reflect
|
||||||
/// the new state.
|
/// the new state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Update(Vector3 playerPosition, float playerYaw, Vector3 playerVelocity, float dt)
|
public void Update(
|
||||||
|
Vector3 playerPosition,
|
||||||
|
float playerYaw,
|
||||||
|
Vector3 playerVelocity,
|
||||||
|
bool isOnGround,
|
||||||
|
Vector3 contactPlaneNormal,
|
||||||
|
float dt)
|
||||||
{
|
{
|
||||||
// 1. Push velocity into 5-frame ring, get average.
|
// 1. Push velocity into 5-frame ring, get average.
|
||||||
PushVelocity(_velocityRing, ref _velocityCount, playerVelocity);
|
PushVelocity(_velocityRing, ref _velocityCount, playerVelocity);
|
||||||
Vector3 avgVel = AverageVelocity(_velocityRing, _velocityCount);
|
Vector3 avgVel = AverageVelocity(_velocityRing, _velocityCount);
|
||||||
|
|
||||||
// 2. Heading vector — slope-aligned when fast enough, flat fallback otherwise.
|
// 2. Heading vector — player's facing projected onto the contact
|
||||||
Vector3 heading = ComputeHeading(avgVel, playerYaw + YawOffset, CameraDiagnostics.AlignToSlope);
|
// plane (grounded) or world XY (airborne). See ComputeHeading
|
||||||
|
// doc + retail decomp :95644-95795 for why this is facing-based
|
||||||
|
// rather than velocity-based.
|
||||||
|
Vector3 heading = ComputeHeading(
|
||||||
|
avgVel,
|
||||||
|
playerYaw + YawOffset,
|
||||||
|
isOnGround,
|
||||||
|
contactPlaneNormal,
|
||||||
|
CameraDiagnostics.AlignToSlope);
|
||||||
|
|
||||||
// 3. Orthonormal heading-frame basis.
|
// 3. Orthonormal heading-frame basis.
|
||||||
var (forward, _, up) = BuildBasis(heading);
|
var (forward, _, up) = BuildBasis(heading);
|
||||||
|
|
@ -165,18 +179,81 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
// Math primitives — pure, internal-static for unit-testability.
|
// Math primitives — pure, internal-static for unit-testability.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pick the heading vector that drives the camera basis. Slope-
|
/// Pick the heading vector that drives the camera basis. Mirrors
|
||||||
/// aligned when velocity is non-trivial and the toggle is on; flat
|
/// retail's <c>CameraManager::UpdateCamera</c> ALIGN_WITH_PLANE
|
||||||
/// fallback otherwise. Matches retail's <c>target_status &
|
/// path (decomp <c>acclient_2013_pseudo_c.txt:95644-95795</c>):
|
||||||
/// ALIGN_WITH_PLANE</c> path with the contact-plane branch
|
/// <list type="number">
|
||||||
/// collapsed into the flat fallback.
|
/// <item><description>Base heading is the player's facing
|
||||||
|
/// direction in world space — <c>(cos yaw, sin yaw, 0)</c>
|
||||||
|
/// — not the velocity vector. Velocity only gates whether
|
||||||
|
/// slope-alignment fires.</description></item>
|
||||||
|
/// <item><description>If <paramref name="alignToSlope"/> is off
|
||||||
|
/// OR the player's horizontal velocity is below epsilon (i.e.
|
||||||
|
/// stationary OR jumping straight up), return that base
|
||||||
|
/// heading unchanged. This is the bit that keeps the camera
|
||||||
|
/// from swinging vertically during a jump.</description></item>
|
||||||
|
/// <item><description>Otherwise project the base heading onto
|
||||||
|
/// the plane perpendicular to a surface normal:
|
||||||
|
/// <see cref="System.Numerics.Plane"/>'s <c>Normal</c> when
|
||||||
|
/// grounded (slope-aligned), world <c>(0, 0, 1)</c> when
|
||||||
|
/// airborne (which is a no-op since the base is already
|
||||||
|
/// horizontal).</description></item>
|
||||||
|
/// <item><description>Normalize. If the projection collapsed
|
||||||
|
/// (heading parallel to normal), fall back to the unprojected
|
||||||
|
/// base.</description></item>
|
||||||
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static Vector3 ComputeHeading(Vector3 avgVelocity, float yaw, bool alignToSlope)
|
/// <param name="avgVelocity">5-frame averaged player velocity in world space.</param>
|
||||||
|
/// <param name="yaw">Player facing yaw + any orbit offset, radians.</param>
|
||||||
|
/// <param name="isOnGround">Player's <c>transient_state & 1</c> — does <paramref name="contactPlaneNormal"/> describe a valid contact plane?</param>
|
||||||
|
/// <param name="contactPlaneNormal">Player's current contact plane normal in world space; ignored when <paramref name="isOnGround"/> is false.</param>
|
||||||
|
/// <param name="alignToSlope">User-tunable; when false skips the projection and returns the flat facing direction.</param>
|
||||||
|
internal static Vector3 ComputeHeading(
|
||||||
|
Vector3 avgVelocity,
|
||||||
|
float yaw,
|
||||||
|
bool isOnGround,
|
||||||
|
Vector3 contactPlaneNormal,
|
||||||
|
bool alignToSlope)
|
||||||
{
|
{
|
||||||
if (alignToSlope && avgVelocity.LengthSquared() > 1e-4f)
|
// Base heading: player's facing direction in world XY plane.
|
||||||
return Vector3.Normalize(avgVelocity);
|
Vector3 baseHeading = new(MathF.Cos(yaw), MathF.Sin(yaw), 0f);
|
||||||
|
|
||||||
return new Vector3(MathF.Cos(yaw), MathF.Sin(yaw), 0f);
|
if (!alignToSlope) return baseHeading;
|
||||||
|
|
||||||
|
// Slope-align gate: player must be moving in XY. Retail tests
|
||||||
|
// |vx| > 0.0002 AND |vy| > 0.0002 (decomp :95704, :95713). The
|
||||||
|
// horizontal-magnitude-squared form is a cleaner equivalent.
|
||||||
|
// Without this, the airborne path would still project against
|
||||||
|
// world up (no-op) which is fine — but the standing-jump case
|
||||||
|
// wants the historical `direction` fallback that retail uses.
|
||||||
|
float hMagSq = avgVelocity.X * avgVelocity.X + avgVelocity.Y * avgVelocity.Y;
|
||||||
|
if (hMagSq < 1e-4f) return baseHeading;
|
||||||
|
|
||||||
|
// Pick the projection plane normal:
|
||||||
|
// grounded → contact_plane.N (slope-aligned camera basis)
|
||||||
|
// airborne → world up (projection becomes a no-op because
|
||||||
|
// baseHeading is already in the XY plane — but
|
||||||
|
// keeping the code path uniform makes the airborne
|
||||||
|
// case impossible to swing vertically).
|
||||||
|
Vector3 normal;
|
||||||
|
if (isOnGround && contactPlaneNormal.LengthSquared() > 0.01f)
|
||||||
|
normal = Vector3.Normalize(contactPlaneNormal);
|
||||||
|
else
|
||||||
|
normal = new Vector3(0f, 0f, 1f);
|
||||||
|
|
||||||
|
// Project baseHeading onto plane perpendicular to normal:
|
||||||
|
// projected = forward - normal * dot(forward, normal)
|
||||||
|
// On flat ground this is a no-op (dot ≈ 0). On a slope the
|
||||||
|
// projected vector gains a Z component matching the slope angle,
|
||||||
|
// which tilts the camera basis with the terrain.
|
||||||
|
float dot = Vector3.Dot(baseHeading, normal);
|
||||||
|
Vector3 projected = baseHeading - normal * dot;
|
||||||
|
|
||||||
|
// Degenerate: facing nearly parallel to normal (rare — would
|
||||||
|
// require player rotated to face into the ground). Fall back to
|
||||||
|
// the unprojected base heading.
|
||||||
|
if (projected.LengthSquared() < 1e-4f) return baseHeading;
|
||||||
|
return Vector3.Normalize(projected);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@ public class RetailChaseCameraTests
|
||||||
var avgVel = Vector3.Zero;
|
var avgVel = Vector3.Zero;
|
||||||
float yaw = MathF.PI / 4f; // 45°
|
float yaw = MathF.PI / 4f; // 45°
|
||||||
|
|
||||||
var h = RetailChaseCamera.ComputeHeading(avgVel, yaw, alignToSlope: true);
|
var h = RetailChaseCamera.ComputeHeading(
|
||||||
|
avgVel, yaw,
|
||||||
|
isOnGround: true, contactPlaneNormal: Vector3.UnitZ,
|
||||||
|
alignToSlope: true);
|
||||||
|
|
||||||
Assert.Equal(MathF.Cos(yaw), h.X, 5);
|
Assert.Equal(MathF.Cos(yaw), h.X, 5);
|
||||||
Assert.Equal(MathF.Sin(yaw), h.Y, 5);
|
Assert.Equal(MathF.Sin(yaw), h.Y, 5);
|
||||||
|
|
@ -24,30 +27,87 @@ public class RetailChaseCameraTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Heading_MovingHorizontal_MatchesNormalizedVelocity()
|
public void Heading_MovingOnFlatGround_HeadingIsHorizontalFacing()
|
||||||
{
|
{
|
||||||
|
// Player moving forward (yaw=0 = +X), on flat ground. Heading
|
||||||
|
// should be the yaw vector — the projection onto (0,0,1)-normal
|
||||||
|
// plane is a no-op since the base is already horizontal.
|
||||||
var avgVel = new Vector3(3f, 0f, 0f);
|
var avgVel = new Vector3(3f, 0f, 0f);
|
||||||
var h = RetailChaseCamera.ComputeHeading(avgVel, yaw: 0f, alignToSlope: true);
|
var h = RetailChaseCamera.ComputeHeading(
|
||||||
|
avgVel, yaw: 0f,
|
||||||
|
isOnGround: true, contactPlaneNormal: Vector3.UnitZ,
|
||||||
|
alignToSlope: true);
|
||||||
Assert.Equal(1f, h.X, 5);
|
Assert.Equal(1f, h.X, 5);
|
||||||
Assert.Equal(0f, h.Y, 5);
|
Assert.Equal(0f, h.Y, 5);
|
||||||
Assert.Equal(0f, h.Z, 5);
|
Assert.Equal(0f, h.Z, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Heading_MovingUphill_HasPositiveZ()
|
public void Heading_OnUphillSlope_TiltsWithSlope()
|
||||||
{
|
{
|
||||||
var avgVel = new Vector3(1f, 0f, 0.5f);
|
// Player facing +Y (yaw=π/2), walking up a slope rising in +Y.
|
||||||
var h = RetailChaseCamera.ComputeHeading(avgVel, yaw: 0f, alignToSlope: true);
|
// Slope normal tilts back-up: (0, -0.5, 0.866) (30° rise).
|
||||||
Assert.True(h.Z > 0f, $"expected positive Z component, got {h.Z}");
|
// Projection of (0,1,0) onto plane perpendicular to (0,-0.5,0.866):
|
||||||
|
// dot = 1*(-0.5) = -0.5
|
||||||
|
// projected = (0,1,0) - (0,-0.5,0.866)*(-0.5) = (0, 0.75, 0.433)
|
||||||
|
// normalized → (0, 0.866, 0.5) — slope-aligned heading with +Z tilt.
|
||||||
|
var avgVel = new Vector3(0f, 3f, 1.5f); // moving up the slope
|
||||||
|
var normal = new Vector3(0f, -0.5f, 0.866f);
|
||||||
|
var h = RetailChaseCamera.ComputeHeading(
|
||||||
|
avgVel, yaw: MathF.PI / 2f,
|
||||||
|
isOnGround: true, contactPlaneNormal: normal,
|
||||||
|
alignToSlope: true);
|
||||||
|
Assert.True(h.Z > 0.4f, $"expected slope-aligned +Z tilt, got Z={h.Z}");
|
||||||
|
Assert.Equal(1f, h.Length(), 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Heading_SlopeAlignDisabled_IgnoresVelocity()
|
public void Heading_AirborneJumpingStraightUp_StaysHorizontal()
|
||||||
{
|
{
|
||||||
var avgVel = new Vector3(0f, 0f, 1f); // pure upward; would dominate if slope-align were on
|
// Player standing still, then jumps straight up. avgVel.xy is
|
||||||
float yaw = 0f;
|
// zero, the horizontal-velocity gate fires → returns the base
|
||||||
|
// facing direction. The vertical-velocity component is ignored.
|
||||||
|
// This is THE bug the contact-plane fix prevents: in the old
|
||||||
|
// code, normalize((0,0,5)) → (0,0,1) → camera basis tilted up.
|
||||||
|
var avgVel = new Vector3(0f, 0f, 5f);
|
||||||
|
var h = RetailChaseCamera.ComputeHeading(
|
||||||
|
avgVel, yaw: 0f,
|
||||||
|
isOnGround: false, contactPlaneNormal: Vector3.Zero,
|
||||||
|
alignToSlope: true);
|
||||||
|
Assert.Equal(1f, h.X, 5);
|
||||||
|
Assert.Equal(0f, h.Y, 5);
|
||||||
|
Assert.Equal(0f, h.Z, 5);
|
||||||
|
}
|
||||||
|
|
||||||
var h = RetailChaseCamera.ComputeHeading(avgVel, yaw, alignToSlope: false);
|
[Fact]
|
||||||
|
public void Heading_AirborneRunningJump_StaysHorizontal()
|
||||||
|
{
|
||||||
|
// Running jump: horizontal velocity nonzero, vertical also
|
||||||
|
// nonzero. Airborne path projects onto world up — strips Z
|
||||||
|
// from the (already horizontal) base heading, no-op. Camera
|
||||||
|
// basis stays horizontal even though player is rising.
|
||||||
|
var avgVel = new Vector3(3f, 0f, 4f);
|
||||||
|
var h = RetailChaseCamera.ComputeHeading(
|
||||||
|
avgVel, yaw: 0f,
|
||||||
|
isOnGround: false, contactPlaneNormal: Vector3.Zero,
|
||||||
|
alignToSlope: true);
|
||||||
|
Assert.Equal(1f, h.X, 5);
|
||||||
|
Assert.Equal(0f, h.Y, 5);
|
||||||
|
Assert.Equal(0f, h.Z, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Heading_SlopeAlignDisabled_IgnoresVelocityAndContactPlane()
|
||||||
|
{
|
||||||
|
// Pure-vertical velocity + a tilted contact normal — neither
|
||||||
|
// should affect the heading when alignToSlope is off.
|
||||||
|
var avgVel = new Vector3(0f, 0f, 1f);
|
||||||
|
var tiltedNormal = new Vector3(0f, -0.5f, 0.866f);
|
||||||
|
|
||||||
|
var h = RetailChaseCamera.ComputeHeading(
|
||||||
|
avgVel, yaw: 0f,
|
||||||
|
isOnGround: true, contactPlaneNormal: tiltedNormal,
|
||||||
|
alignToSlope: false);
|
||||||
|
|
||||||
Assert.Equal(1f, h.X, 5); // (cos 0, sin 0, 0) = (1, 0, 0)
|
Assert.Equal(1f, h.X, 5); // (cos 0, sin 0, 0) = (1, 0, 0)
|
||||||
Assert.Equal(0f, h.Y, 5);
|
Assert.Equal(0f, h.Y, 5);
|
||||||
|
|
@ -280,10 +340,12 @@ public class RetailChaseCameraTests
|
||||||
CameraDiagnostics.AlignToSlope = false; // deterministic: heading = yaw vec
|
CameraDiagnostics.AlignToSlope = false; // deterministic: heading = yaw vec
|
||||||
|
|
||||||
cam.Update(
|
cam.Update(
|
||||||
playerPosition: new Vector3(10f, 20f, 30f),
|
playerPosition: new Vector3(10f, 20f, 30f),
|
||||||
playerYaw: 0f, // forward = +X
|
playerYaw: 0f, // forward = +X
|
||||||
playerVelocity: Vector3.Zero,
|
playerVelocity: Vector3.Zero,
|
||||||
dt: 1f / 60f);
|
isOnGround: true,
|
||||||
|
contactPlaneNormal: Vector3.UnitZ, // flat
|
||||||
|
dt: 1f / 60f);
|
||||||
|
|
||||||
// Expected target eye:
|
// Expected target eye:
|
||||||
// pivot = (10, 20, 30+1.5=31.5)
|
// pivot = (10, 20, 30+1.5=31.5)
|
||||||
|
|
@ -317,7 +379,8 @@ public class RetailChaseCameraTests
|
||||||
CameraDiagnostics.RotationStiffness = 0.45f;
|
CameraDiagnostics.RotationStiffness = 0.45f;
|
||||||
|
|
||||||
// First update at origin: dampedEye = (-5, 0, 1.5).
|
// First update at origin: dampedEye = (-5, 0, 1.5).
|
||||||
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero,
|
||||||
|
isOnGround: true, contactPlaneNormal: Vector3.UnitZ, dt: 1f / 60f);
|
||||||
var firstEye = cam.Position;
|
var firstEye = cam.Position;
|
||||||
|
|
||||||
// Teleport the player one frame later. Target eye now at (10-5, 0, 1.5) = (5, 0, 1.5).
|
// Teleport the player one frame later. Target eye now at (10-5, 0, 1.5) = (5, 0, 1.5).
|
||||||
|
|
@ -326,7 +389,8 @@ public class RetailChaseCameraTests
|
||||||
// = (-5,0,1.5) + 0.075 * ((5,0,1.5) - (-5,0,1.5))
|
// = (-5,0,1.5) + 0.075 * ((5,0,1.5) - (-5,0,1.5))
|
||||||
// = (-5,0,1.5) + 0.075 * (10,0,0)
|
// = (-5,0,1.5) + 0.075 * (10,0,0)
|
||||||
// = (-4.25, 0, 1.5)
|
// = (-4.25, 0, 1.5)
|
||||||
cam.Update(new Vector3(10f, 0f, 0f), playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
cam.Update(new Vector3(10f, 0f, 0f), playerYaw: 0f, playerVelocity: Vector3.Zero,
|
||||||
|
isOnGround: true, contactPlaneNormal: Vector3.UnitZ, dt: 1f / 60f);
|
||||||
|
|
||||||
Assert.Equal(-4.25f, cam.Position.X, 3);
|
Assert.Equal(-4.25f, cam.Position.X, 3);
|
||||||
Assert.Equal(0f, cam.Position.Y, 4);
|
Assert.Equal(0f, cam.Position.Y, 4);
|
||||||
|
|
@ -350,7 +414,8 @@ public class RetailChaseCameraTests
|
||||||
CameraDiagnostics.AlignToSlope = false;
|
CameraDiagnostics.AlignToSlope = false;
|
||||||
|
|
||||||
// Far from pivot — translucency should be 0.
|
// Far from pivot — translucency should be 0.
|
||||||
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero,
|
||||||
|
isOnGround: true, contactPlaneNormal: Vector3.UnitZ, dt: 1f / 60f);
|
||||||
Assert.Equal(0f, cam.PlayerTranslucency, 5);
|
Assert.Equal(0f, cam.PlayerTranslucency, 5);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue