feat(camera): wire RetailChaseCamera Update() + tunables + state
Adds the per-frame Update(playerPos, yaw, velocity, dt) entrypoint that composes the math primitives into a renderable View matrix + PlayerTranslucency. State: 5-frame velocity ring, damped eye + forward unit vector, first-frame snap flag, mouse-filter shared state. Public surface: Distance/Pitch/YawOffset/PivotHeight tunables, AdjustDistance/Pitch (with clamps), FilterMouseDelta entry, View + Position + PlayerTranslucency outputs. 5 new integration tests, all pass; total RetailChaseCamera test count 25. Also folds in two minor cleanups from the Task 2 code review: - AverageVelocity uses ring.Length instead of hardcoded 5 - Basis_NearVerticalHeading test asserts orthogonality of right & up Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8ebd33dc8f
commit
0c1403f2e6
2 changed files with 220 additions and 3 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
|
@ -84,6 +85,7 @@ public class RetailChaseCameraTests
|
|||
|
||||
Assert.Equal(1f, right.Length(), 5);
|
||||
Assert.Equal(1f, up.Length(), 5);
|
||||
Assert.Equal(0f, Vector3.Dot(right, up), 5);
|
||||
}
|
||||
|
||||
// ── Velocity ring & averaging ────────────────────────────────────
|
||||
|
|
@ -265,4 +267,89 @@ public class RetailChaseCameraTests
|
|||
Assert.Equal(1f, RetailChaseCamera.ComputeTranslucency(distance: 0.10f), 5);
|
||||
Assert.Equal(1f, RetailChaseCamera.ComputeTranslucency(distance: 0.0f), 5);
|
||||
}
|
||||
|
||||
// ── Update() integration ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void FirstUpdate_SnapsToTarget()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f };
|
||||
CameraDiagnostics.AlignToSlope = false; // deterministic: heading = yaw vec
|
||||
|
||||
cam.Update(
|
||||
playerPosition: new Vector3(10f, 20f, 30f),
|
||||
playerYaw: 0f, // forward = +X
|
||||
playerVelocity: Vector3.Zero,
|
||||
dt: 1f / 60f);
|
||||
|
||||
// Expected target eye:
|
||||
// pivot = (10, 20, 30+1.5=31.5)
|
||||
// forward (yaw=0)= (1, 0, 0)
|
||||
// right = (0, -1, 0) since (1,0,0) × (0,0,1) = (0, -1, 0)
|
||||
// up = right × forward = (0,-1,0) × (1,0,0) = (0,0,1)
|
||||
// viewer_offset = (0, -5, 0) (Distance=5, Pitch=0 → -Distance*cos = -5, sin = 0)
|
||||
// eye = pivot + right*0 + forward*-5 + up*0
|
||||
// = (10 - 5, 20, 31.5) = (5, 20, 31.5)
|
||||
Assert.Equal(5f, cam.Position.X, 4);
|
||||
Assert.Equal(20f, cam.Position.Y, 4);
|
||||
Assert.Equal(31.5f, cam.Position.Z, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SecondUpdate_LerpsTowardTarget()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f };
|
||||
CameraDiagnostics.AlignToSlope = false;
|
||||
CameraDiagnostics.TranslationStiffness = 0.45f;
|
||||
CameraDiagnostics.RotationStiffness = 0.45f;
|
||||
|
||||
// First update at origin: dampedEye = (-5, 0, 1.5).
|
||||
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
var firstEye = cam.Position;
|
||||
|
||||
// Teleport the player one frame later. Target eye now at (10-5, 0, 1.5) = (5, 0, 1.5).
|
||||
// alpha = 0.45 * (1/60) * 10 = 0.075.
|
||||
// New eye = firstEye + 0.075 * (target - firstEye)
|
||||
// = (-5,0,1.5) + 0.075 * ((5,0,1.5) - (-5,0,1.5))
|
||||
// = (-5,0,1.5) + 0.075 * (10,0,0)
|
||||
// = (-4.25, 0, 1.5)
|
||||
cam.Update(new Vector3(10f, 0f, 0f), playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
|
||||
Assert.Equal(-4.25f, cam.Position.X, 3);
|
||||
Assert.Equal(0f, cam.Position.Y, 4);
|
||||
Assert.Equal(1.5f, cam.Position.Z, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Translucency_PropertyReflectsCurrentDampedDistance()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f, Pitch = 0f, PivotHeight = 1.5f };
|
||||
CameraDiagnostics.AlignToSlope = false;
|
||||
|
||||
// Far from pivot — translucency should be 0.
|
||||
cam.Update(Vector3.Zero, playerYaw: 0f, playerVelocity: Vector3.Zero, dt: 1f / 60f);
|
||||
Assert.Equal(0f, cam.PlayerTranslucency, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustDistance_ClampsToRange()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Distance = 5f };
|
||||
cam.AdjustDistance(-100f);
|
||||
Assert.Equal(RetailChaseCamera.DistanceMin, cam.Distance);
|
||||
|
||||
cam.AdjustDistance(+200f);
|
||||
Assert.Equal(RetailChaseCamera.DistanceMax, cam.Distance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustPitch_ClampsToRange()
|
||||
{
|
||||
var cam = new RetailChaseCamera { Pitch = 0f };
|
||||
cam.AdjustPitch(-10f);
|
||||
Assert.Equal(RetailChaseCamera.PitchMin, cam.Pitch);
|
||||
|
||||
cam.AdjustPitch(+10f);
|
||||
Assert.Equal(RetailChaseCamera.PitchMax, cam.Pitch);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue