From d7ecf019cbe66409d2e035f92ac4241f0c24ac8b Mon Sep 17 00:00:00 2001 From: MDietermann Date: Thu, 28 May 2026 12:04:30 +0200 Subject: [PATCH] feat: add fly-camera with view/projection matrices --- Rendering/Camera.cs | 54 ++++++++++++++++++++++++++++++++ VoxelEngine.Tests/CameraTests.cs | 43 +++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 Rendering/Camera.cs create mode 100644 VoxelEngine.Tests/CameraTests.cs diff --git a/Rendering/Camera.cs b/Rendering/Camera.cs new file mode 100644 index 0000000..357a415 --- /dev/null +++ b/Rendering/Camera.cs @@ -0,0 +1,54 @@ +using OpenTK.Mathematics; + +namespace VoxelEngine.Rendering; + +public sealed class Camera +{ + private static readonly Vector3 WorldUp = Vector3.UnitY; + + public Vector3 Position { get; private set; } + public float Yaw { get; private set; } = -90f; // degrees; -90 looks down -Z + public float Pitch { get; private set; } = 0f; // degrees + public float Fov { get; set; } = 60f; // degrees + public float AspectRatio { get; set; } + + public Vector3 Front { get; private set; } = -Vector3.UnitZ; + public Vector3 Right { get; private set; } = Vector3.UnitX; + public Vector3 Up { get; private set; } = Vector3.UnitY; + + public Camera(Vector3 position, float aspectRatio) + { + Position = position; + AspectRatio = aspectRatio; + UpdateVectors(); + } + + public void Move(Vector3 delta) => Position += delta; + + public void AddRotation(float yawDelta, float pitchDelta) + { + Yaw += yawDelta; + Pitch = MathHelper.Clamp(Pitch + pitchDelta, -89f, 89f); + UpdateVectors(); + } + + public Matrix4 GetViewMatrix() => Matrix4.LookAt(Position, Position + Front, Up); + + public Matrix4 GetProjectionMatrix() => + Matrix4.CreatePerspectiveFieldOfView( + MathHelper.DegreesToRadians(Fov), AspectRatio, 0.1f, 100f); + + private void UpdateVectors() + { + float yaw = MathHelper.DegreesToRadians(Yaw); + float pitch = MathHelper.DegreesToRadians(Pitch); + + Front = Vector3.Normalize(new Vector3( + MathF.Cos(yaw) * MathF.Cos(pitch), + MathF.Sin(pitch), + MathF.Sin(yaw) * MathF.Cos(pitch))); + + Right = Vector3.Normalize(Vector3.Cross(Front, WorldUp)); + Up = Vector3.Normalize(Vector3.Cross(Right, Front)); + } +} diff --git a/VoxelEngine.Tests/CameraTests.cs b/VoxelEngine.Tests/CameraTests.cs new file mode 100644 index 0000000..0ac5a96 --- /dev/null +++ b/VoxelEngine.Tests/CameraTests.cs @@ -0,0 +1,43 @@ +using OpenTK.Mathematics; +using VoxelEngine.Rendering; +using Xunit; + +namespace VoxelEngine.Tests; + +public class CameraTests +{ + [Fact] + public void DefaultFront_PointsTowardNegativeZ() + { + var camera = new Camera(Vector3.Zero, 16f / 9f); + + Assert.Equal(0f, camera.Front.X, 3); + Assert.Equal(0f, camera.Front.Y, 3); + Assert.Equal(-1f, camera.Front.Z, 3); + } + + [Fact] + public void Move_TranslatesPosition() + { + var camera = new Camera(Vector3.Zero, 1f); + camera.Move(new Vector3(1f, 2f, 3f)); + Assert.Equal(new Vector3(1f, 2f, 3f), camera.Position); + } + + [Fact] + public void AddRotation_ClampsPitchTo89Degrees() + { + var camera = new Camera(Vector3.Zero, 1f); + camera.AddRotation(0f, 200f); // try to over-rotate up + Assert.True(camera.Pitch <= 89.0001f); + camera.AddRotation(0f, -400f); + Assert.True(camera.Pitch >= -89.0001f); + } + + [Fact] + public void ProjectionMatrix_IsNotIdentity() + { + var camera = new Camera(Vector3.Zero, 1.5f); + Assert.NotEqual(Matrix4.Identity, camera.GetProjectionMatrix()); + } +}