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 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; set { if (field == value) return; Matrix previousView = field; field = value; OnViewChanged.Invoke(this, new(previousView)); } } = Matrix.Identity; public Matrix Projection { get; set { if (field == value) return; Matrix previousProjection = field; field = value; OnProjectionChanged.Invoke(this, new(previousProjection)); } } = Matrix.Identity; public Viewport Viewport { get; set { if (field.Equals(value)) return; Viewport previousViewport = field; field = value; SetForRecalculation(); OnViewportChanged.Invoke(this, new(previousViewport)); } } public float NearPlane { get; set { float previousNearPlane = field; field = value.Max(0.0001f); SetForRecalculation(); OnNearPlaneChanged.Invoke(this, new(previousNearPlane)); } } = 0.01f; public float FarPlane { get; set { float previousFarPlane = field; field = value.Max(NearPlane); SetForRecalculation(); OnFarPlaneChanged.Invoke(this, new(previousFarPlane)); } } = 100f; 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)); } } // 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; CalculateView(); CalculateProjection(); isRecalculationNeeded = false; } private void CalculateView() { Vector3 forward = Vector3.Normalize(Transform.Forward.ToVector3()); Vector3 up = Vector3.Normalize(Transform.Up.ToVector3()); Vector3 right = Vector3.Normalize(Transform.Right.ToVector3()); Vector3 position = Transform.Position.ToVector3(); View = new Matrix( right.X, up.X, forward.X, 0, right.Y, up.Y, forward.Y, 0, right.Z, up.Z, forward.Z, 0, -Vector3.Dot(right, position), -Vector3.Dot(up, position), -Vector3.Dot(forward, position), 1 ); } private void CalculateProjection() { float yScale = 1f / (float)Math.Tan(fieldOfView / 2f); float xScale = yScale / Viewport.AspectRatio; Projection = new Matrix( xScale, 0, 0, 0, 0, yScale, 0, 0, 0, 0, FarPlane / (FarPlane - NearPlane), 1, 0, 0, -NearPlane * FarPlane / (FarPlane - NearPlane), 0 ); } public readonly record struct ViewChangedArguments(Matrix PreviousView); public readonly record struct ProjectionChangedArguments(Matrix PreviousProjection); public readonly record struct ViewportChangedArguments(Viewport PreviousViewport); }