Files
Syntriax.Engine/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs

197 lines
6.3 KiB
C#

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<MonoGameCamera3D, ViewChangedArguments> OnViewChanged { get; } = new();
public Event<MonoGameCamera3D, ProjectionChangedArguments> OnProjectionChanged { get; } = new();
public Event<MonoGameCamera3D, ViewportChangedArguments> OnViewportChanged { get; } = new();
public Event<ICamera3D, ICamera3D.NearPlaneChangedArguments> OnNearPlaneChanged { get; } = new();
public Event<ICamera3D, ICamera3D.FarPlaneChangedArguments> OnFarPlaneChanged { get; } = new();
public Event<ICamera3D, ICamera3D.FieldOfViewChangedArguments> 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<ITransform3D>();
Graphics = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour<MonoGameWindowContainer>().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 = 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);
}