using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Engine.Core; namespace Engine.Integration.MonoGame; public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFrameUpdate, IPreDraw { public Event OnViewChanged { get; } = new(); public Event OnProjectionChanged { get; } = new(); public Event OnViewportChanged { get; } = new(); public Event OnNearPlaneChanged { get; } = new(); public Event OnFarPlaneChanged { get; } = new(); public Event OnFieldOfViewChanged { get; } = new(); private Matrix _view = Matrix.Identity; private Matrix _projection = Matrix.Identity; private Viewport _viewport = default; private float _nearPlane = 0.01f; private float _farPlane = 100f; private float _fieldOfView = Math.DegreeToRadian * 70f; private bool isRecalculationNeeded = true; 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; SetForRecalculation(); OnViewportChanged.Invoke(this, new(previousViewport)); } } public float NearPlane { get => _nearPlane; set { float previousNearPlane = _nearPlane; _nearPlane = value.Max(0.0001f); SetForRecalculation(); OnNearPlaneChanged.Invoke(this, new(previousNearPlane)); } } public float FarPlane { get => _farPlane; set { float previousFarPlane = _farPlane; _farPlane = value.Max(NearPlane); SetForRecalculation(); OnFarPlaneChanged.Invoke(this, new(previousFarPlane)); } } public float FieldOfView { get => _fieldOfView * Math.RadianToDegree; set { value = value.Max(0.1f) * Math.DegreeToRadian; if (_fieldOfView == value) return; float previousFieldOfView = _fieldOfView; _fieldOfView = value; SetForRecalculation(); OnFieldOfViewChanged.Invoke(this, new(previousFieldOfView)); } } public 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 LastActiveFrame() => Transform.OnTransformUpdated.RemoveListener(SetDirtyOnTransformUpdate); public void FirstActiveFrame() { Transform = BehaviourController.GetRequiredBehaviour(); Graphics = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour().Window.Graphics; Viewport = Graphics.GraphicsDevice.Viewport; Transform.OnTransformUpdated.AddListener(SetDirtyOnTransformUpdate); } private void SetDirtyOnTransformUpdate(ITransform3D sender) => SetForRecalculation(); private void SetForRecalculation() => isRecalculationNeeded = true; public void PreDraw() { if (!isRecalculationNeeded) return; Vector3 cameraPosition = Position.ToVector3(); View = Matrix.CreateLookAt( cameraPosition, Transform.Forward.ToVector3() + cameraPosition, Transform.Up.ToVector3() ); Projection = Matrix.CreatePerspectiveFieldOfView(_fieldOfView, Viewport.AspectRatio, 0.1f, 100f); isRecalculationNeeded = false; } public readonly record struct ViewChangedArguments(Matrix PreviousView); public readonly record struct ProjectionChangedArguments(Matrix PreviousProjection); public readonly record struct ViewportChangedArguments(Viewport PreviousViewport); }