From 22f684e8c6a2aebb9f8e6782f6e64f4cc5442023 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Apr 2026 20:27:11 +0200 Subject: [PATCH] feat(app): add CameraController with F toggle and cursor capture Co-Authored-By: Claude Sonnet 4.6 --- src/AcDream.App/Rendering/CameraController.cs | 31 +++++++ src/AcDream.App/Rendering/GameWindow.cs | 84 +++++++++++++++---- 2 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 src/AcDream.App/Rendering/CameraController.cs diff --git a/src/AcDream.App/Rendering/CameraController.cs b/src/AcDream.App/Rendering/CameraController.cs new file mode 100644 index 0000000..97ff925 --- /dev/null +++ b/src/AcDream.App/Rendering/CameraController.cs @@ -0,0 +1,31 @@ +// src/AcDream.App/Rendering/CameraController.cs +namespace AcDream.App.Rendering; + +public sealed class CameraController +{ + public OrbitCamera Orbit { get; } + public FlyCamera Fly { get; } + public ICamera Active { get; private set; } + public bool IsFlyMode => Active == Fly; + + public event Action? ModeChanged; + + public CameraController(OrbitCamera orbit, FlyCamera fly) + { + Orbit = orbit; + Fly = fly; + Active = orbit; + } + + public void ToggleFly() + { + Active = IsFlyMode ? (ICamera)Orbit : Fly; + ModeChanged?.Invoke(IsFlyMode); + } + + public void SetAspect(float aspect) + { + Orbit.Aspect = aspect; + Fly.Aspect = aspect; + } +} diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 3c63255..126eccb 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -15,7 +15,8 @@ public sealed class GameWindow : IDisposable private IInputContext? _input; private TerrainRenderer? _terrain; private Shader? _shader; - private OrbitCamera? _camera; + private CameraController? _cameraController; + private IMouse? _capturedMouse; private DatCollection? _dats; private float _lastMouseX; private float _lastMouseY; @@ -42,6 +43,7 @@ public sealed class GameWindow : IDisposable _window = Window.Create(options); _window.Load += OnLoad; + _window.Update += OnUpdate; _window.Render += OnRender; _window.Closing += OnClosing; @@ -55,26 +57,49 @@ public sealed class GameWindow : IDisposable foreach (var kb in _input.Keyboards) kb.KeyDown += (_, key, _) => { - if (key == Key.Escape) - _window!.Close(); + if (key == Key.F) + _cameraController?.ToggleFly(); + else if (key == Key.Escape) + { + if (_cameraController?.IsFlyMode == true) + _cameraController.ToggleFly(); // exit fly, release cursor + else + _window!.Close(); + } }; foreach (var mouse in _input.Mice) { mouse.MouseMove += (m, pos) => { - if (m.IsButtonPressed(MouseButton.Left)) + if (_cameraController is null) return; + + if (_cameraController.IsFlyMode) { - _camera!.Yaw -= (pos.X - _lastMouseX) * 0.005f; - _camera!.Pitch = Math.Clamp( - _camera.Pitch + (pos.Y - _lastMouseY) * 0.005f, - 0.1f, 1.5f); + // Raw cursor mode: Silk.NET gives deltas via position. Compute delta from last. + float dx = pos.X - _lastMouseX; + float dy = pos.Y - _lastMouseY; + _cameraController.Fly.Look(dx, dy); + } + else + { + if (m.IsButtonPressed(MouseButton.Left)) + { + _cameraController.Orbit.Yaw -= (pos.X - _lastMouseX) * 0.005f; + _cameraController.Orbit.Pitch = Math.Clamp( + _cameraController.Orbit.Pitch + (pos.Y - _lastMouseY) * 0.005f, + 0.1f, 1.5f); + } } _lastMouseX = pos.X; _lastMouseY = pos.Y; }; mouse.Scroll += (_, scroll) => - _camera!.Distance = Math.Clamp(_camera.Distance - scroll.Y * 20f, 50f, 2000f); + { + if (_cameraController is null || _cameraController.IsFlyMode) return; + _cameraController.Orbit.Distance = Math.Clamp( + _cameraController.Orbit.Distance - scroll.Y * 20f, 50f, 2000f); + }; } _gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f); @@ -89,10 +114,10 @@ public sealed class GameWindow : IDisposable Path.Combine(shadersDir, "mesh.vert"), Path.Combine(shadersDir, "mesh.frag")); - _camera = new OrbitCamera - { - Aspect = _window!.Size.X / (float)_window.Size.Y, - }; + var orbit = new OrbitCamera { Aspect = _window!.Size.X / (float)_window.Size.Y }; + var fly = new FlyCamera { Aspect = _window.Size.X / (float)_window.Size.Y }; + _cameraController = new CameraController(orbit, fly); + _cameraController.ModeChanged += OnCameraModeChanged; _dats = new DatCollection(_datDir, DatAccessType.Read); @@ -199,11 +224,40 @@ public sealed class GameWindow : IDisposable Console.WriteLine($"hydrated {_entities.Count} entities"); } + private void OnUpdate(double dt) + { + if (_cameraController is null || _input is null) return; + if (!_cameraController.IsFlyMode) return; + + var kb = _input.Keyboards[0]; + _cameraController.Fly.Update( + dt, + w: kb.IsKeyPressed(Key.W), + a: kb.IsKeyPressed(Key.A), + s: kb.IsKeyPressed(Key.S), + d: kb.IsKeyPressed(Key.D), + up: kb.IsKeyPressed(Key.Space), + down: kb.IsKeyPressed(Key.ControlLeft)); + } + + private void OnCameraModeChanged(bool isFlyMode) + { + if (_input is null) return; + var mouse = _input.Mice.FirstOrDefault(); + if (mouse is null) return; + + mouse.Cursor.CursorMode = isFlyMode ? CursorMode.Raw : CursorMode.Normal; + _capturedMouse = isFlyMode ? mouse : null; + } + private void OnRender(double deltaSeconds) { _gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - _terrain?.Draw(_camera!); - _staticMesh?.Draw(_camera!, _entities); + if (_cameraController is not null) + { + _terrain?.Draw(_cameraController.Active); + _staticMesh?.Draw(_cameraController.Active, _entities); + } } private void OnClosing()