diff --git a/Engine.Core/Abstract/IBehaviour3D.cs b/Engine.Core/Abstract/IBehaviour3D.cs
new file mode 100644
index 0000000..75985d6
--- /dev/null
+++ b/Engine.Core/Abstract/IBehaviour3D.cs
@@ -0,0 +1,6 @@
+namespace Engine.Core;
+
+public interface IBehaviour3D : IBehaviour
+{
+ ITransform3D Transform { get; }
+}
diff --git a/Engine.Core/Abstract/ITransform3D.cs b/Engine.Core/Abstract/ITransform3D.cs
new file mode 100644
index 0000000..33a7a78
--- /dev/null
+++ b/Engine.Core/Abstract/ITransform3D.cs
@@ -0,0 +1,105 @@
+namespace Engine.Core;
+
+///
+/// Represents the transformation properties of an object such as position, scale, and rotation in 3D space.
+///
+public interface ITransform3D : IBehaviour
+{
+ ///
+ /// Event triggered when the of the changes.
+ ///
+ Event OnPositionChanged { get; }
+
+ ///
+ /// Event triggered when the of the changes.
+ ///
+ Event OnScaleChanged { get; }
+
+ ///
+ /// Event triggered when the of the changes.
+ ///
+ Event OnRotationChanged { get; }
+
+ ///
+ /// Event triggered when any of the properties of the gets updated.
+ ///
+ Event OnTransformUpdated { get; }
+
+ ///
+ /// The pointing upwards in world space.
+ ///
+ Vector3D Up { get; }
+
+ ///
+ /// The pointing upwards in world space.
+ ///
+ Vector3D Down { get; }
+
+ ///
+ /// The pointing upwards in world space.
+ ///
+ Vector3D Left { get; }
+
+ ///
+ /// The pointing upwards in world space.
+ ///
+ Vector3D Right { get; }
+
+ ///
+ /// The pointing forwards in world space.
+ ///
+ Vector3D Forward { get; }
+
+ ///
+ /// The pointing backwards in world space.
+ ///
+ Vector3D Backward { get; }
+
+ ///
+ /// The world position of the in 3D space.
+ ///
+ Vector3D Position { get; set; }
+
+ ///
+ /// The world scale of the .
+ ///
+ Vector3D Scale { get; set; }
+
+ ///
+ /// The world rotation of the in degrees.
+ ///
+ Quaternion Rotation { get; set; }
+
+ ///
+ /// The local position of the in 3D space.
+ ///
+ Vector3D LocalPosition { get; set; }
+
+ ///
+ /// The local scale of the .
+ ///
+ Vector3D LocalScale { get; set; }
+
+ ///
+ /// The local rotation of the in degrees.
+ ///
+ Quaternion LocalRotation { get; set; }
+
+ ///
+ /// Arguments for the event triggered when the 's rotation changes.
+ ///
+ /// The previous of the .
+ readonly record struct PositionChangedArguments(Vector3D PreviousPosition);
+
+ ///
+ /// Arguments for the event triggered when the 's rotation changes.
+ ///
+ /// The previous of the .
+ readonly record struct ScaleChangedArguments(Vector3D PreviousScale);
+
+ ///
+ /// Arguments for the event triggered when the 's rotation changes.
+ ///
+ /// The previous of the .
+ readonly record struct RotationChangedArguments(Quaternion PreviousRotation);
+}
diff --git a/Engine.Core/Extensions/TransformExtensions.cs b/Engine.Core/Extensions/TransformExtensions.cs
index 8300449..7ac43a3 100644
--- a/Engine.Core/Extensions/TransformExtensions.cs
+++ b/Engine.Core/Extensions/TransformExtensions.cs
@@ -14,4 +14,17 @@ public static class TransformExtensions
if (localScale.HasValue) transform.LocalScale = localScale.Value;
return transform;
}
+
+ public static ITransform3D SetTransform(this ITransform3D transform,
+ Vector3D? position = null, Quaternion? rotation = null, Vector3D? scale = null,
+ Vector3D? localPosition = null, Quaternion? localRotation = null, Vector3D? localScale = null)
+ {
+ if (position.HasValue) transform.Position = position.Value;
+ if (rotation.HasValue) transform.Rotation = rotation.Value;
+ if (scale.HasValue) transform.Scale = scale.Value;
+ if (localPosition.HasValue) transform.LocalPosition = localPosition.Value;
+ if (localRotation.HasValue) transform.LocalRotation = localRotation.Value;
+ if (localScale.HasValue) transform.LocalScale = localScale.Value;
+ return transform;
+ }
}
diff --git a/Engine.Core/Transform3D.cs b/Engine.Core/Transform3D.cs
new file mode 100644
index 0000000..7340b19
--- /dev/null
+++ b/Engine.Core/Transform3D.cs
@@ -0,0 +1,285 @@
+using Engine.Core.Serialization;
+
+namespace Engine.Core;
+
+[System.Diagnostics.DebuggerDisplay("Name: {UniverseObject.Name, nq} Position: {Position.ToString(), nq}, Scale: {Scale.ToString(), nq}, Rotation: {Rotation}")]
+public class Transform3D : Behaviour, ITransform3D
+{
+ public Event OnPositionChanged { get; } = new();
+ public Event OnScaleChanged { get; } = new();
+ public Event OnRotationChanged { get; } = new();
+ public Event OnTransformUpdated { get; } = new();
+
+ private Vector3D _position = Vector3D.Zero;
+ private Vector3D _scale = Vector3D.One;
+ private Quaternion _rotation = Quaternion.Identity;
+
+ [Serialize] private Vector3D _localPosition = Vector3D.Zero;
+ [Serialize] private Vector3D _localScale = Vector3D.One;
+ [Serialize] private Quaternion _localRotation = Quaternion.Identity;
+
+ private ITransform3D? parentTransform = null;
+
+ public Vector3D Up => Quaternion.RotateVector(Vector3D.Up, Rotation);
+ public Vector3D Down => Quaternion.RotateVector(Vector3D.Down, Rotation);
+ public Vector3D Left => Quaternion.RotateVector(Vector3D.Left, Rotation);
+ public Vector3D Right => Quaternion.RotateVector(Vector3D.Right, Rotation);
+ public Vector3D Forward => Quaternion.RotateVector(Vector3D.Forward, Rotation);
+ public Vector3D Backward => Quaternion.RotateVector(Vector3D.Backward, Rotation);
+
+ public Vector3D Position
+ {
+ get => _position;
+ set
+ {
+ if (value == _position)
+ return;
+
+ Vector3D previousPosition = _position;
+ _position = value;
+
+ UpdateLocalPosition();
+ OnPositionChanged.Invoke(this, new(previousPosition));
+ OnTransformUpdated.Invoke(this);
+ }
+ }
+
+ public Vector3D Scale
+ {
+ get => _scale;
+ set
+ {
+ if (value == _scale)
+ return;
+
+ Vector3D previousScale = _scale;
+ _scale = value;
+
+ UpdateLocalScale();
+ OnScaleChanged.Invoke(this, new(previousScale));
+ OnTransformUpdated.Invoke(this);
+ }
+ }
+
+ public Quaternion Rotation
+ {
+ get => _rotation;
+ set
+ {
+ if (value == _rotation)
+ return;
+
+ Quaternion previousRotation = _rotation;
+ _rotation = value;
+
+ UpdateLocalRotation();
+ OnRotationChanged.Invoke(this, new(previousRotation));
+ OnTransformUpdated.Invoke(this);
+ }
+ }
+
+ public Vector3D LocalPosition
+ {
+ get => _localPosition;
+ set
+ {
+ if (value == _localPosition)
+ return;
+
+ Vector3D previousPosition = _position;
+ _localPosition = value;
+
+ UpdatePosition();
+ OnPositionChanged.Invoke(this, new(previousPosition));
+ OnTransformUpdated.Invoke(this);
+ }
+ }
+
+ public Vector3D LocalScale
+ {
+ get => _localScale;
+ set
+ {
+ if (value == _localScale)
+ return;
+
+ Vector3D previousScale = _scale;
+ Vector3D previousPosition = _position;
+ _localScale = value;
+
+ UpdateScale();
+ UpdatePosition();
+ OnScaleChanged.Invoke(this, new(previousScale));
+ OnPositionChanged.Invoke(this, new(previousPosition));
+ OnTransformUpdated.Invoke(this);
+ }
+ }
+
+ public Quaternion LocalRotation
+ {
+ get => _localRotation;
+ set
+ {
+ if (value == _localRotation)
+ return;
+
+ Quaternion previousRotation = _rotation;
+ _localRotation = value;
+
+ UpdateRotation();
+ OnRotationChanged.Invoke(this, new(previousRotation));
+ OnTransformUpdated.Invoke(this);
+ }
+ }
+
+ private void RecalculatePosition(ITransform3D _, ITransform3D.PositionChangedArguments args)
+ {
+ if (parentTransform is null)
+ return;
+
+ UpdatePosition();
+
+ OnPositionChanged.Invoke(this, args);
+ OnTransformUpdated.Invoke(this);
+ }
+
+ private void RecalculateScale(ITransform3D _, ITransform3D.ScaleChangedArguments args)
+ {
+ if (parentTransform is null)
+ return;
+
+ Vector3D previousPosition = _position;
+
+ UpdateScale();
+ UpdatePosition();
+
+ OnScaleChanged.Invoke(this, args);
+ OnPositionChanged.Invoke(this, new(previousPosition));
+ OnTransformUpdated.Invoke(this);
+ }
+
+ private void RecalculateRotation(ITransform3D _, ITransform3D.RotationChangedArguments args)
+ {
+ if (parentTransform is null)
+ return;
+
+ Vector3D previousPosition = Position;
+
+ UpdateRotation();
+ UpdatePosition();
+
+ OnRotationChanged.Invoke(this, args);
+ OnPositionChanged.Invoke(this, new(previousPosition));
+ OnTransformUpdated.Invoke(this);
+ }
+
+ private void UpdateLocalPosition()
+ {
+ if (parentTransform is null)
+ _localPosition = Position;
+ else
+ _localPosition = parentTransform.Position.FromTo(Position).Scale(parentTransform.Scale);
+ }
+
+ private void UpdateLocalScale()
+ {
+ if (parentTransform is null)
+ _localScale = Scale;
+ else
+ _localScale = Scale.Scale(new(1f / parentTransform.Scale.X, 1f / parentTransform.Scale.Y, 1f / parentTransform.Scale.Z));
+ }
+
+ private void UpdateLocalRotation()
+ {
+ if (parentTransform is null)
+ _localRotation = Rotation;
+ else
+ _localRotation = Rotation * parentTransform.Rotation.Invert();
+ }
+
+ private void UpdatePosition()
+ {
+ if (parentTransform is null)
+ _position = LocalPosition;
+ else
+ _position = parentTransform.Position + Quaternion.RotateVector(LocalPosition.Scale(new(parentTransform.Scale.X, parentTransform.Scale.Y, parentTransform.Scale.Z)), parentTransform.Rotation);
+ }
+
+ private void UpdateScale()
+ {
+ if (parentTransform is null)
+ _scale = LocalScale;
+ else
+ _scale = (parentTransform?.Scale ?? Vector3D.One).Scale(_localScale);
+ }
+
+ private void UpdateRotation()
+ {
+ if (parentTransform is null)
+ _rotation = LocalRotation;
+ else
+ _rotation = parentTransform.Rotation * LocalRotation;
+ }
+
+ protected override void InitializeInternal()
+ {
+ UpdateReferences(UniverseObject.Parent);
+ UniverseObject.OnParentChanged.AddListener(OnParentChanged);
+ }
+
+ protected override void FinalizeInternal()
+ {
+ UniverseObject.OnParentChanged.RemoveListener(OnParentChanged);
+ }
+
+ private void UpdateReferences(IUniverseObject? parent)
+ {
+ ITransform3D? previousParent = parentTransform;
+ if (previousParent is not null)
+ {
+ previousParent.OnPositionChanged.RemoveListener(RecalculatePosition);
+ previousParent.OnScaleChanged.RemoveListener(RecalculateScale);
+ previousParent.OnRotationChanged.RemoveListener(RecalculateRotation);
+ previousParent.BehaviourController.UniverseObject.OnParentChanged.RemoveListener(OnParentChanged);
+ previousParent.BehaviourController.OnBehaviourAdded.RemoveListener(LookForTransform3D);
+ }
+
+ parentTransform = parent?.BehaviourController.GetBehaviour();
+
+ if (parentTransform is not null)
+ {
+ parentTransform.OnPositionChanged.AddListener(RecalculatePosition);
+ parentTransform.OnScaleChanged.AddListener(RecalculateScale);
+ parentTransform.OnRotationChanged.AddListener(RecalculateRotation);
+ parentTransform.BehaviourController.UniverseObject.OnParentChanged.AddListener(OnParentChanged);
+
+ UpdatePosition();
+ UpdateScale();
+ UpdateRotation();
+ }
+ else
+ UniverseObject.Parent?.BehaviourController.OnBehaviourAdded.AddListener(LookForTransform3D);
+
+ UpdateLocalPosition();
+ UpdateLocalScale();
+ UpdateLocalRotation();
+
+ OnPositionChanged.Invoke(this, new(Position));
+ OnScaleChanged.Invoke(this, new(Scale));
+ OnRotationChanged.Invoke(this, new(Rotation));
+ OnTransformUpdated.Invoke(this);
+ }
+
+ private void LookForTransform3D(IBehaviourController sender, IBehaviourController.BehaviourAddedArguments args)
+ {
+ if (args.BehaviourAdded is not ITransform3D)
+ return;
+
+ UpdateReferences(UniverseObject.Parent);
+ }
+
+ private void OnParentChanged(IUniverseObject sender, IUniverseObject.ParentChangedArguments args)
+ {
+ UpdateReferences(args.CurrentParent);
+ }
+}