From 61ff0887e2770465bb8623e4641f05471cd9a8a1 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sun, 19 Oct 2025 00:28:40 +0300 Subject: [PATCH] feat: 3D camera added --- Engine.Core/Abstract/ICamera3D.cs | 26 ++++ .../Behaviours/MonoGameCamera3D.cs | 131 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 Engine.Core/Abstract/ICamera3D.cs create mode 100644 Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs diff --git a/Engine.Core/Abstract/ICamera3D.cs b/Engine.Core/Abstract/ICamera3D.cs new file mode 100644 index 0000000..01ce185 --- /dev/null +++ b/Engine.Core/Abstract/ICamera3D.cs @@ -0,0 +1,26 @@ +namespace Engine.Core; + +/// +/// Represents a 3D camera in the engine. +/// +public interface ICamera3D : IBehaviour3D +{ + /// + /// Field of View (FOV) value of the camera + /// + float FieldOfView { get; set; } + + /// + /// Converts a position from screen coordinates to a . + /// + /// The position in screen coordinates. + /// The originating from the camera to the screen position in world coordinates. + Ray3D ScreenToWorldRay(Vector2D screenPosition); + + /// + /// Converts a position from world coordinates to screen coordinates. + /// + /// The position in world coordinates. + /// The position in screen coordinates. + Vector2D WorldToScreenPosition(Vector3D worldPosition); +} diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs new file mode 100644 index 0000000..6a9440f --- /dev/null +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs @@ -0,0 +1,131 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +using Engine.Core; + +namespace Engine.Integration.MonoGame; + +public class MonoGameCamera3D : BehaviourBase, ICamera3D, IFirstFrameUpdate, IPreDraw +{ + public Event OnViewChanged { get; } = new(); + public Event OnProjectionChanged { get; } = new(); + public Event OnViewportChanged { get; } = new(); + public Event OnFieldOfViewChanged { get; } = new(); + + private Matrix _view = Matrix.Identity; + private Matrix _projection = Matrix.Identity; + private Viewport _viewport = default; + private float _fieldOfView = 1f; + + public GraphicsDeviceManager Graphics { get; private set; } = null!; + public ITransform3D Transform { get; private set; } = null!; + + public Matrix View + { + get => _view; + set + { + if (_view == value) + return; + + Matrix previousView = _view; + _view = value; + OnViewChanged.Invoke(this, new(previousView)); + } + } + public Matrix Projection + { + get => _projection; + set + { + if (_projection == value) + return; + + Matrix previousProjection = _projection; + _projection = value; + OnProjectionChanged.Invoke(this, new(previousProjection)); + } + } + + public Vector3D Position + { + get => Transform.Position; + set => Transform.Position = value; + } + + public Viewport Viewport + { + get => _viewport; + set + { + if (_viewport.Equals(value)) + return; + + Viewport previousViewport = _viewport; + _viewport = value; + OnViewportChanged.Invoke(this, new(previousViewport)); + } + } + + public float FieldOfView + { + get => _fieldOfView; + set + { + float newValue = Math.Max(0.1f, value); + + if (_fieldOfView == newValue) + return; + + float previousFieldOfView = _fieldOfView; + _fieldOfView = newValue; + OnFieldOfViewChanged.Invoke(this, new(previousFieldOfView)); + } + } + + public Engine.Core.Quaternion Rotation + { + get => Transform.Rotation; + set => Transform.Rotation = value; + } + + // TODO This causes delay since OnPreDraw calls assuming this is called in in Update + public Ray3D ScreenToWorldRay(Vector2D screenPosition) + { + Vector3 nearPoint = new(screenPosition.X, screenPosition.Y, 0f); + Vector3 farPoint = new(screenPosition.X, screenPosition.Y, 1f); + + Vector3 worldNear = Viewport.Unproject(nearPoint, _projection, _view, Matrix.Identity); + Vector3 worldFar = Viewport.Unproject(farPoint, _projection, _view, Matrix.Identity); + + Vector3 direction = Vector3.Normalize(worldFar - worldNear); + return new(worldNear.ToVector3D(), direction.ToVector3D()); + } + + public Vector2D WorldToScreenPosition(Vector3D worldPosition) => Viewport.Project(worldPosition.ToVector3(), _projection, _view, Matrix.Identity).ToVector3D(); + + public void FirstActiveFrame() + { + Graphics = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour().Window.Graphics; + Viewport = Graphics.GraphicsDevice.Viewport; + } + + public void PreDraw() + { + Vector3 cameraPosition = Position.ToVector3(); + View = Matrix.CreateLookAt( + cameraPosition, + Transform.Forward.ToVector3() + cameraPosition, + Transform.Up.ToVector3() + ); + Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, Viewport.AspectRatio, 0.1f, 100f); + } + + protected sealed override void InitializeInternal() => Transform = BehaviourController.GetRequiredBehaviour(); + protected sealed override void FinalizeInternal() => Transform = null!; + + public readonly record struct ViewChangedArguments(Matrix PreviousView); + public readonly record struct ProjectionChangedArguments(Matrix PreviousProjection); + public readonly record struct ViewportChangedArguments(Viewport PreviousViewport); + public readonly record struct FieldOfViewChangedArguments(float PreviousFieldOfView); +}