diff --git a/src/AcDream.App/Rendering/ChaseCamera.cs b/src/AcDream.App/Rendering/ChaseCamera.cs
new file mode 100644
index 0000000..d70950a
--- /dev/null
+++ b/src/AcDream.App/Rendering/ChaseCamera.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Numerics;
+
+namespace AcDream.App.Rendering;
+
+///
+/// Third-person chase camera that follows behind and above a player
+/// character. Implements so it plugs into the
+/// existing renderer pipeline.
+///
+public sealed class ChaseCamera : ICamera
+{
+ public Vector3 Position { get; private set; }
+ public float Aspect { get; set; } = 16f / 9f;
+ public float FovY { get; set; } = MathF.PI / 3f;
+
+ /// Distance behind the player.
+ public float Distance { get; set; } = 8f;
+
+ /// Camera pitch above horizontal (radians). Positive = look down.
+ public float Pitch { get; set; } = 0.35f; // ~20 degrees
+
+ /// Vertical offset from the player's feet to the look-at point (eye height).
+ public float EyeHeight { get; set; } = 1.5f;
+
+ private const float PitchMin = 0.05f;
+ private const float PitchMax = 1.4f; // ~80 degrees
+
+ private float _playerYaw;
+ private Vector3 _lookAt;
+
+ public Matrix4x4 View =>
+ Matrix4x4.CreateLookAt(Position, _lookAt, Vector3.UnitZ);
+
+ public Matrix4x4 Projection =>
+ Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 1f, 5000f);
+
+ ///
+ /// Update the camera position to follow the player.
+ ///
+ public void Update(Vector3 playerPosition, float playerYaw)
+ {
+ _playerYaw = playerYaw;
+ _lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
+
+ // Camera offset: behind the player (-forward direction) and above.
+ float forwardX = MathF.Cos(playerYaw);
+ float forwardY = MathF.Sin(playerYaw);
+
+ float horizontalDist = Distance * MathF.Cos(Pitch);
+ float verticalDist = Distance * MathF.Sin(Pitch);
+
+ Position = new Vector3(
+ playerPosition.X - forwardX * horizontalDist,
+ playerPosition.Y - forwardY * horizontalDist,
+ playerPosition.Z + EyeHeight + verticalDist);
+ }
+
+ ///
+ /// Adjust pitch by a delta (from mouse Y movement).
+ ///
+ public void AdjustPitch(float delta)
+ {
+ Pitch = Math.Clamp(Pitch + delta, PitchMin, PitchMax);
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Rendering/ChaseCameraTests.cs b/tests/AcDream.Core.Tests/Rendering/ChaseCameraTests.cs
new file mode 100644
index 0000000..bd1ebfe
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Rendering/ChaseCameraTests.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Numerics;
+using AcDream.App.Rendering;
+using Xunit;
+
+namespace AcDream.Core.Tests.Rendering;
+
+public class ChaseCameraTests
+{
+ [Fact]
+ public void Position_BehindPlayer_WhenYawIsZero()
+ {
+ var camera = new ChaseCamera { Aspect = 16f / 9f };
+ camera.Update(playerPosition: Vector3.Zero, playerYaw: 0f);
+
+ // Yaw=0 means facing +X (cos(0)=1, sin(0)=0).
+ // Camera should be BEHIND the player: negative X direction.
+ Assert.True(camera.Position.X < -1f, $"Camera X={camera.Position.X} should be behind player (negative X)");
+ Assert.True(camera.Position.Z > 0f, $"Camera Z={camera.Position.Z} should be above player");
+ }
+
+ [Fact]
+ public void Position_BehindPlayer_WhenYawIsHalfPi()
+ {
+ var camera = new ChaseCamera { Aspect = 16f / 9f };
+ camera.Update(playerPosition: Vector3.Zero, playerYaw: MathF.PI / 2f);
+
+ // Yaw=PI/2 means facing +Y. Camera should be behind: negative Y.
+ Assert.True(camera.Position.Y < -1f, $"Camera Y={camera.Position.Y} should be behind player (negative Y)");
+ }
+
+ [Fact]
+ public void Position_FollowsPlayerPosition()
+ {
+ var camera = new ChaseCamera { Aspect = 16f / 9f };
+ var playerPos = new Vector3(100f, 200f, 50f);
+ camera.Update(playerPosition: playerPos, playerYaw: 0f);
+
+ // Camera should be near the player, not at the origin.
+ Assert.InRange(camera.Position.X, 85f, 100f); // behind but close
+ Assert.InRange(camera.Position.Y, 195f, 205f); // roughly same Y
+ }
+
+ [Fact]
+ public void PitchAdjustment_ChangesHeight()
+ {
+ var camera = new ChaseCamera { Aspect = 16f / 9f };
+ camera.Update(playerPosition: Vector3.Zero, playerYaw: 0f);
+ float z1 = camera.Position.Z;
+
+ camera.AdjustPitch(0.2f);
+ camera.Update(playerPosition: Vector3.Zero, playerYaw: 0f);
+ float z2 = camera.Position.Z;
+
+ Assert.True(z2 > z1, "Increasing pitch should raise the camera");
+ }
+
+ [Fact]
+ public void ImplementsICamera()
+ {
+ ICamera camera = new ChaseCamera { Aspect = 16f / 9f };
+ camera.ToString(); // just proves interface is implemented
+ }
+}