From 761fbd247da03673fce4a36ac234484b18650e20 Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:25:27 +1000 Subject: [PATCH 01/11] Added base quaternion --- Hypercube.Shared.Math/Quaternion.cs | 156 +++++++++++++++++++++ Hypercube.Shared.Math/Vector/Vector2.cs | 8 +- Hypercube.Shared.Math/Vector/Vector2Int.cs | 8 +- Hypercube.Shared.Math/Vector/Vector3.cs | 8 +- Hypercube.Shared.Math/Vector/Vector4.cs | 28 ++++ Hypercube.UnitTests/Math/QuaternionTest.cs | 29 ++++ 6 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 Hypercube.Shared.Math/Quaternion.cs create mode 100644 Hypercube.UnitTests/Math/QuaternionTest.cs diff --git a/Hypercube.Shared.Math/Quaternion.cs b/Hypercube.Shared.Math/Quaternion.cs new file mode 100644 index 0000000..94a5299 --- /dev/null +++ b/Hypercube.Shared.Math/Quaternion.cs @@ -0,0 +1,156 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Hypercube.Shared.Math.Vector; + +namespace Hypercube.Shared.Math; + +[StructLayout(LayoutKind.Sequential)] +public readonly struct Quaternion : IEquatable +{ + public readonly Vector4 Vector; + + public float LengthSquared + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector.LengthSquared; + } + + public float Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector.Length; + } + + public Quaternion Normalized + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new (Vector.Normalized); + } + + public Vector3 Direction + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector.XYZ; + } + + public Angle Angle + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(Vector.W); + } + + public float X + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector.X; + } + + public float Y + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector.Y; + } + + public float Z + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector.Z; + } + + public float W + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Vector.W; + } + + public Quaternion(Vector4 vector) + { + Vector = vector; + } + + public Quaternion(Quaternion quaternion) : this(quaternion.Vector) + { + } + + public Quaternion(float x, float y, float z, float w) : this(new Vector4(x, y, z, w)) + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Quaternion WithX(float value) + { + return new Quaternion(Vector.WithX(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Quaternion WithY(float value) + { + return new Quaternion(Vector.WithY(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Quaternion WithZ(float value) + { + return new Quaternion(Vector.WithZ(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Quaternion WithW(float value) + { + return new Quaternion(Vector.WithW(value)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Quaternion other) + { + return Vector == other.Vector; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + return obj is Quaternion other && Equals(other); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + return Vector.GetHashCode(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion operator *(Quaternion a, Quaternion b) + { + return new Quaternion(a.Vector * b.Vector); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion operator *(Quaternion a, float b) + { + return new Quaternion(a.Vector * b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Quaternion a, Quaternion b) + { + return a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Quaternion a, Quaternion b) + { + return !a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion FromEuler(Vector3 vector3) + { + throw new NotImplementedException(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ToEuler(Quaternion quaternion) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Vector/Vector2.cs b/Hypercube.Shared.Math/Vector/Vector2.cs index 4742380..392d3b0 100644 --- a/Hypercube.Shared.Math/Vector/Vector2.cs +++ b/Hypercube.Shared.Math/Vector/Vector2.cs @@ -21,10 +21,16 @@ public float AspectRatio get => X / Y; } + public float LengthSquared + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => X * X + Y * Y; + } + public float Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => MathF.Sqrt(X * X + Y * Y); + get => MathF.Sqrt(LengthSquared); } public Vector2(float x, float y) diff --git a/Hypercube.Shared.Math/Vector/Vector2Int.cs b/Hypercube.Shared.Math/Vector/Vector2Int.cs index 554e67f..3b7b8ff 100644 --- a/Hypercube.Shared.Math/Vector/Vector2Int.cs +++ b/Hypercube.Shared.Math/Vector/Vector2Int.cs @@ -21,10 +21,16 @@ public float AspectRatio get => X / (float)Y; } + public float LengthSquared + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => X * X + Y * Y; + } + public float Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => MathF.Sqrt(X * X + Y * Y); + get => MathF.Sqrt(LengthSquared); } public Vector2Int(int x, int y) diff --git a/Hypercube.Shared.Math/Vector/Vector3.cs b/Hypercube.Shared.Math/Vector/Vector3.cs index e9f8463..98f9fd0 100644 --- a/Hypercube.Shared.Math/Vector/Vector3.cs +++ b/Hypercube.Shared.Math/Vector/Vector3.cs @@ -18,10 +18,16 @@ public readonly partial struct Vector3(float x, float y, float z) : IEquatable X * X + Y * Y + Z * Z; + } + public float Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => MathF.Sqrt(X * X + Y * Y + Z * Z); + get => MathF.Sqrt(LengthSquared); } public Vector3 Normalized diff --git a/Hypercube.Shared.Math/Vector/Vector4.cs b/Hypercube.Shared.Math/Vector/Vector4.cs index 224dcb1..390c189 100644 --- a/Hypercube.Shared.Math/Vector/Vector4.cs +++ b/Hypercube.Shared.Math/Vector/Vector4.cs @@ -20,6 +20,27 @@ namespace Hypercube.Shared.Math.Vector; public readonly float Z; public readonly float W; + public Vector2 XY => new(X, Y); + public Vector3 XYZ => new(X, Y, Z); + + public float LengthSquared + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => X * X + Y * Y + Z * Z + W * W; + } + + public float Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => MathF.Sqrt(LengthSquared); + } + + public Vector4 Normalized + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this / Length; + } + public Vector4(float x, float y, float z, float w) { X = x; @@ -47,6 +68,13 @@ public float Sum() return X + Y + Z + W; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Prod() + { + return X * Y * Z * W; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector4 WithX(float value) { diff --git a/Hypercube.UnitTests/Math/QuaternionTest.cs b/Hypercube.UnitTests/Math/QuaternionTest.cs new file mode 100644 index 0000000..f60ccb4 --- /dev/null +++ b/Hypercube.UnitTests/Math/QuaternionTest.cs @@ -0,0 +1,29 @@ +using Hypercube.Shared.Math; + +namespace Hypercube.UnitTests.Math; + +public class QuaternionTest +{ + [Test] + public void Equals() + { + var quaternionA = new Quaternion(1, 2, 3, 4); + var quaternionB = new Quaternion(10, 22, 40, 32); + + // ReSharper disable once EqualExpressionComparison + Assert.That(quaternionA == quaternionA); + Assert.That(quaternionA.Equals(quaternionA)); + Assert.That(quaternionA.Equals((object)quaternionA)); + + var quaternionAClone = new Quaternion(quaternionA); + Assert.That(quaternionA == quaternionAClone); + Assert.That(quaternionA.Equals(quaternionAClone)); + Assert.That(quaternionA.Equals((object)quaternionAClone)); + + Assert.That(quaternionA != quaternionB); + Assert.That(!quaternionA.Equals(quaternionB)); + Assert.That(!quaternionA.Equals((object)quaternionB)); + + Assert.Pass($"{nameof(Quaternion)} equals passed"); + } +} \ No newline at end of file From be927abee8dd39a8e378c86f37e36450184e91dc Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:19:38 +1000 Subject: [PATCH 02/11] Added to euler implementation --- Hypercube.Shared.Math/HyperMath.cs | 8 ++++ Hypercube.Shared.Math/Quaternion.cs | 49 ++++++++++++++++++++-- Hypercube.UnitTests/Math/QuaternionTest.cs | 14 +++++++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 Hypercube.Shared.Math/HyperMath.cs diff --git a/Hypercube.Shared.Math/HyperMath.cs b/Hypercube.Shared.Math/HyperMath.cs new file mode 100644 index 0000000..ab118a4 --- /dev/null +++ b/Hypercube.Shared.Math/HyperMath.cs @@ -0,0 +1,8 @@ +namespace Hypercube.Shared.Math; + +public static class HyperMath +{ + // ReSharper disable once InconsistentNaming + public const float PI = MathF.PI; + public const float PiOver2 = PI / 2; +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Quaternion.cs b/Hypercube.Shared.Math/Quaternion.cs index 94a5299..b092305 100644 --- a/Hypercube.Shared.Math/Quaternion.cs +++ b/Hypercube.Shared.Math/Quaternion.cs @@ -7,6 +7,8 @@ namespace Hypercube.Shared.Math; [StructLayout(LayoutKind.Sequential)] public readonly struct Quaternion : IEquatable { + private const float SingularityThreshold = 0.4999995f; + public readonly Vector4 Vector; public float LengthSquared @@ -100,6 +102,12 @@ public Quaternion WithW(float value) return new Quaternion(Vector.WithW(value)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector3 ToEuler() + { + return ToEuler(this); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(Quaternion other) { @@ -146,11 +154,46 @@ public override int GetHashCode() public static Quaternion FromEuler(Vector3 vector3) { throw new NotImplementedException(); - } - + } + + /// + /// Convert this instance to an Euler angle representation. + /// + /// Taken from OpenTK.Mathematics/Data/Quaterniond.cs + /// + /// + /// Euler angle in radians [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector3 ToEuler(Quaternion quaternion) { - throw new NotImplementedException(); + var sqx = quaternion.X * quaternion.X; + var sqy = quaternion.Y * quaternion.Y; + var sqz = quaternion.Z * quaternion.Z; + var sqw = quaternion.W * quaternion.W; + + var unit = sqx + sqy + sqz + sqw; // If normalised is one, otherwise is correction factor + var singularityTest = quaternion.X * quaternion.Z + quaternion.W * quaternion.Y; + + if (singularityTest > SingularityThreshold * unit) + // Singularity at north pole + return new Vector3( + 0, + HyperMath.PiOver2, + 2f * MathF.Atan2(quaternion.X, quaternion.W) + ); + + if (singularityTest < -SingularityThreshold * unit) + // Singularity at south pole + return new Vector3( + 0, + -HyperMath.PiOver2, + -2f * MathF.Atan2(quaternion.X, quaternion.W) + ); + + return new Vector3( + MathF.Atan2(2 * (quaternion.W * quaternion.X - quaternion.Y * quaternion.Z), sqw - sqx - sqy + sqz), + MathF.Asin(2 * singularityTest / unit), + MathF.Atan2(2 * (quaternion.W * quaternion.Z - quaternion.X * quaternion.Y), sqw + sqx - sqy - sqz) + ); } } \ No newline at end of file diff --git a/Hypercube.UnitTests/Math/QuaternionTest.cs b/Hypercube.UnitTests/Math/QuaternionTest.cs index f60ccb4..71c3a12 100644 --- a/Hypercube.UnitTests/Math/QuaternionTest.cs +++ b/Hypercube.UnitTests/Math/QuaternionTest.cs @@ -1,4 +1,5 @@ using Hypercube.Shared.Math; +using Hypercube.Shared.Math.Vector; namespace Hypercube.UnitTests.Math; @@ -26,4 +27,17 @@ public void Equals() Assert.Pass($"{nameof(Quaternion)} equals passed"); } + + /// + /// Can ba useful, cite to 3d convert: https://www.andre-gaschler.com/rotationconverter/ + /// + [Test] + public void ToEuler() + { + var eulerA = new Quaternion(1, 2, 3, 4).ToEuler(); + var eulerB = new Quaternion(0, 0.6767456f, 0.4308296f, 0.5969936f).ToEuler(); + + Assert.That(eulerA == new Vector3(-0.19739556f, 0.8232120f, 1.3734008f)); + Assert.That(eulerB == new Vector3(-1.42767680f, 0.9407929f, 2.0799970f)); + } } \ No newline at end of file From acbd961914f2118cc0b9d9ee9280439429b271e0 Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:00:53 +1000 Subject: [PATCH 03/11] Added from euler implementation --- Hypercube.Shared.Math/Quaternion.cs | 35 +++++++++++++++++++--- Hypercube.UnitTests/Math/QuaternionTest.cs | 29 ++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/Hypercube.Shared.Math/Quaternion.cs b/Hypercube.Shared.Math/Quaternion.cs index b092305..c2dfb36 100644 --- a/Hypercube.Shared.Math/Quaternion.cs +++ b/Hypercube.Shared.Math/Quaternion.cs @@ -125,7 +125,13 @@ public override int GetHashCode() { return Vector.GetHashCode(); } - + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() + { + return Vector.ToString(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Quaternion operator *(Quaternion a, Quaternion b) { @@ -149,17 +155,38 @@ public override int GetHashCode() { return !a.Equals(b); } - + + /// + /// Created new from given Euler angles in radians. + /// + /// Taken from OpenTK.Mathematics/Data/Quaternion.cs + /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Quaternion FromEuler(Vector3 vector3) { - throw new NotImplementedException(); + var axis = vector3 / 2f; + + var c1 = MathF.Cos(axis.X); + var c2 = MathF.Cos(axis.Y); + var c3 = MathF.Cos(axis.Z); + + var s1 = MathF.Sin(axis.X); + var s2 = MathF.Sin(axis.Y); + var s3 = MathF.Sin(axis.Z); + + return new Quaternion( + s1 * c2 * c3 + c1 * s2 * s3, + c1 * s2 * c3 - s1 * c2 * s3, + c1 * c2 * s3 + s1 * s2 * c3, + c1 * c2 * c3 - s1 * s2 * s3 + ); } /// /// Convert this instance to an Euler angle representation. /// - /// Taken from OpenTK.Mathematics/Data/Quaterniond.cs + /// Taken from OpenTK.Mathematics/Data/Quaternion.cs /// /// /// Euler angle in radians diff --git a/Hypercube.UnitTests/Math/QuaternionTest.cs b/Hypercube.UnitTests/Math/QuaternionTest.cs index 71c3a12..7aa1e37 100644 --- a/Hypercube.UnitTests/Math/QuaternionTest.cs +++ b/Hypercube.UnitTests/Math/QuaternionTest.cs @@ -39,5 +39,34 @@ public void ToEuler() Assert.That(eulerA == new Vector3(-0.19739556f, 0.8232120f, 1.3734008f)); Assert.That(eulerB == new Vector3(-1.42767680f, 0.9407929f, 2.0799970f)); + + Assert.Pass($"{nameof(Quaternion)} to euler passed"); + } + + [Test] + public void FromEulerUnit() + { + var quaternionUnitX = Quaternion.FromEuler(Vector3.UnitX); + var quaternionUnitY = Quaternion.FromEuler(Vector3.UnitY); + var quaternionUnitZ = Quaternion.FromEuler(Vector3.UnitZ); + + Assert.That(quaternionUnitX == new Quaternion(0.47942555f, 0, 0, 0.87758255f)); + Assert.That(quaternionUnitY == new Quaternion(0, 0.47942555f, 0, 0.87758255f)); + Assert.That(quaternionUnitZ == new Quaternion(0, 0, 0.47942555f, 0.87758255f)); + + Assert.Pass($"{nameof(Quaternion)} from euler unit passed"); + } + + [Test] + public void FromEulerConvert() + { + var quaternionA = new Quaternion(0.8232120f, 0.6767456f, 0.4308296f, 0.5969936f); + var eulerA = quaternionA.ToEuler(); + var fromA = Quaternion.FromEuler(eulerA); + + // The losses on this convert are fucked up. + Assert.That(fromA == new Quaternion(0.6355612f, 0.5224818f, 0.33262214f, 0.4609092f)); + + Assert.Pass($"{nameof(Quaternion)} from euler convert passed"); } } \ No newline at end of file From 9ad9aaa6136e8ad968a17979a716ec09e1bdfa6d Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:50:33 +1000 Subject: [PATCH 04/11] Cretead Transform3 --- .../Graphics/Viewports/Camera2D.cs | 37 ++++------ Hypercube.Shared.Math/Matrix/Matrix4X4.cs | 7 ++ Hypercube.Shared.Math/Quaternion.cs | 12 +++- Hypercube.Shared.Math/Transform/Transform.cs | 68 +++++++++++++++++++ 4 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 Hypercube.Shared.Math/Transform/Transform.cs diff --git a/Hypercube.Client/Graphics/Viewports/Camera2D.cs b/Hypercube.Client/Graphics/Viewports/Camera2D.cs index fde8ce8..56027bc 100644 --- a/Hypercube.Client/Graphics/Viewports/Camera2D.cs +++ b/Hypercube.Client/Graphics/Viewports/Camera2D.cs @@ -1,18 +1,21 @@ -using Hypercube.Shared.Math.Matrix; +using Hypercube.Shared.Math; +using Hypercube.Shared.Math.Matrix; +using Hypercube.Shared.Math.Transform; using Hypercube.Shared.Math.Vector; namespace Hypercube.Client.Graphics.Viewports; public class Camera2D : ICamera { - public Vector3 Position { get; private set; } - public Vector3 Rotation { get; private set; } - public Vector3 Scale { get; private set; } = Vector3.One; + private Transform3 _transform = new(); + + public Vector3 Position => _transform.Position; + public Vector3 Rotation => _transform.Rotation.ToEuler(); + public Vector3 Scale => _transform.Scale; private readonly float _zFar; private readonly float _zNear; private Vector2Int Size { get; set; } - private Vector2 HalfSize => Size / 2f; public Matrix4X4 Projection { get; private set; } @@ -20,45 +23,35 @@ public class Camera2D : ICamera public Camera2D(Vector2Int size, Vector2 position, float zNear, float zFar) { Size = size; - Position = new Vector3(position); _zNear = zNear; _zFar = zFar; + SetPosition(new Vector3(position)); + UpdateProjection(); } - public void SetSize(Vector2Int size) - { - Size = size; - UpdateProjection(); - } - public void SetPosition(Vector3 position) { - Position = position; + _transform.SetPosition(position); UpdateProjection(); } - + public void SetRotation(Vector3 rotation) { - Rotation = Rotation.WithZ(rotation.Z); + _transform.SetRotation(Quaternion.FromEuler(0, 0, rotation.Z)); UpdateProjection(); } public void SetScale(Vector3 scale) { - Scale = scale; + _transform.SetScale(scale); UpdateProjection(); } private void UpdateProjection() { var projection = Matrix4X4.CreateOrthographic(Size, _zNear, _zFar); - - var translate = Matrix4X4.CreateTranslation(Position); - var rotation = Matrix4X4.CreateRotationZ(Rotation.Z); - var scale = Matrix4X4.CreateScale(Scale); - - Projection = projection * translate * rotation * scale; + Projection = projection * _transform.Matrix; } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Matrix/Matrix4X4.cs b/Hypercube.Shared.Math/Matrix/Matrix4X4.cs index 17cef3d..5989091 100644 --- a/Hypercube.Shared.Math/Matrix/Matrix4X4.cs +++ b/Hypercube.Shared.Math/Matrix/Matrix4X4.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Hypercube.Shared.Math.Box; +using Hypercube.Shared.Math.Transform; using Hypercube.Shared.Math.Vector; namespace Hypercube.Shared.Math.Matrix; @@ -395,6 +396,12 @@ public static Matrix4X4 CreateScale(float x, float y, float z) return result; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4X4 CreateRotation(Quaternion quaternion) + { + return CreateRotation(quaternion.Direction, (float)quaternion.Angle.Theta); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix4X4 CreateRotation(Vector3 direction, float angle) diff --git a/Hypercube.Shared.Math/Quaternion.cs b/Hypercube.Shared.Math/Quaternion.cs index c2dfb36..a2ca47c 100644 --- a/Hypercube.Shared.Math/Quaternion.cs +++ b/Hypercube.Shared.Math/Quaternion.cs @@ -70,6 +70,10 @@ public Quaternion(Vector4 vector) Vector = vector; } + public Quaternion(Vector3 vector3) : this(FromEuler(vector3).Vector) + { + } + public Quaternion(Quaternion quaternion) : this(quaternion.Vector) { } @@ -155,7 +159,13 @@ public override string ToString() { return !a.Equals(b); } - + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion FromEuler(float x, float y, float z) + { + return FromEuler(new Vector3(x, y, z)); + } + /// /// Created new from given Euler angles in radians. /// diff --git a/Hypercube.Shared.Math/Transform/Transform.cs b/Hypercube.Shared.Math/Transform/Transform.cs new file mode 100644 index 0000000..90b792d --- /dev/null +++ b/Hypercube.Shared.Math/Transform/Transform.cs @@ -0,0 +1,68 @@ +using Hypercube.Shared.Math.Matrix; +using Hypercube.Shared.Math.Vector; + +namespace Hypercube.Shared.Math.Transform; + +public struct Transform3 +{ + public Matrix4X4 Matrix { get; private set; } + + public Vector3 Position { get; private set; } + public Quaternion Rotation { get; private set; } + public Vector3 Scale { get; private set; } + + public Transform3() + { + Position = Vector3.Zero; + Rotation = new Quaternion(Vector3.Zero); + Scale = Vector3.One; + UpdateMatrix(); + } + + public Transform3(Vector3 position, Quaternion rotation, Vector3 scale) + { + Position = position; + Rotation = rotation; + Scale = scale; + UpdateMatrix(); + } + + public Transform3 SetPosition(Vector3 position) + { + Position = position; + UpdateMatrix(); + + return this; + } + + public Transform3 SetRotation(Vector3 vector3) + { + Rotation = new Quaternion(vector3); + UpdateMatrix(); + + return this; + } + + public Transform3 SetRotation(Quaternion rotation) + { + Rotation = rotation; + UpdateMatrix(); + + return this; + } + + public Transform3 SetScale(Vector3 scale) + { + Scale = scale; + UpdateMatrix(); + + return this; + } + + private void UpdateMatrix() + { + Matrix = Matrix4X4.CreateTranslation(Position) * + Matrix4X4.CreateRotation(Rotation) * + Matrix4X4.CreateScale(Scale); + } +} \ No newline at end of file From e36e0aab6199ce92b6774005eb871bee9e7ac644 Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:16:07 +1000 Subject: [PATCH 05/11] Fixed rotation matrix for Quaternion --- .../Graphics/Viewports/Camera2D.cs | 7 ++-- .../Matrix/Matrix4X4.Compatibility.cs | 25 ++++++++++++++ Hypercube.Shared.Math/Matrix/Matrix4X4.cs | 34 ++++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/Hypercube.Client/Graphics/Viewports/Camera2D.cs b/Hypercube.Client/Graphics/Viewports/Camera2D.cs index 56027bc..6ca4c30 100644 --- a/Hypercube.Client/Graphics/Viewports/Camera2D.cs +++ b/Hypercube.Client/Graphics/Viewports/Camera2D.cs @@ -1,7 +1,10 @@ -using Hypercube.Shared.Math; +using System.Numerics; using Hypercube.Shared.Math.Matrix; using Hypercube.Shared.Math.Transform; using Hypercube.Shared.Math.Vector; +using Quaternion = Hypercube.Shared.Math.Quaternion; +using Vector2 = Hypercube.Shared.Math.Vector.Vector2; +using Vector3 = Hypercube.Shared.Math.Vector.Vector3; namespace Hypercube.Client.Graphics.Viewports; @@ -18,7 +21,7 @@ public class Camera2D : ICamera private Vector2Int Size { get; set; } private Vector2 HalfSize => Size / 2f; - public Matrix4X4 Projection { get; private set; } + public Hypercube.Shared.Math.Matrix.Matrix4X4 Projection { get; private set; } public Camera2D(Vector2Int size, Vector2 position, float zNear, float zFar) { diff --git a/Hypercube.Shared.Math/Matrix/Matrix4X4.Compatibility.cs b/Hypercube.Shared.Math/Matrix/Matrix4X4.Compatibility.cs index e66a499..4a02f0c 100644 --- a/Hypercube.Shared.Math/Matrix/Matrix4X4.Compatibility.cs +++ b/Hypercube.Shared.Math/Matrix/Matrix4X4.Compatibility.cs @@ -4,6 +4,30 @@ namespace Hypercube.Shared.Math.Matrix; public partial struct Matrix4X4 { + /* + * System.Numerics Compatibility + */ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator System.Numerics.Matrix4x4(Matrix4X4 matrix4X4) + { + return new System.Numerics.Matrix4x4( + matrix4X4.M00, matrix4X4.M01, matrix4X4.M02, matrix4X4.M03, + matrix4X4.M10, matrix4X4.M11, matrix4X4.M12, matrix4X4.M13, + matrix4X4.M20, matrix4X4.M21, matrix4X4.M22, matrix4X4.M23, + matrix4X4.M30, matrix4X4.M31, matrix4X4.M32, matrix4X4.M33); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Matrix4X4(System.Numerics.Matrix4x4 matrix4X4) + { + return new Matrix4X4( + matrix4X4.M11, matrix4X4.M12, matrix4X4.M13, matrix4X4.M14, + matrix4X4.M21, matrix4X4.M22, matrix4X4.M23, matrix4X4.M24, + matrix4X4.M31, matrix4X4.M32, matrix4X4.M33, matrix4X4.M34, + matrix4X4.M41, matrix4X4.M42, matrix4X4.M43, matrix4X4.M44); + } + /* * OpenTK Compatibility */ @@ -13,6 +37,7 @@ public static implicit operator OpenTK.Mathematics.Matrix4(Matrix4X4 matrix4X4) { return new OpenTK.Mathematics.Matrix4(matrix4X4.Row0, matrix4X4.Row1, matrix4X4.Row2, matrix4X4.Row3); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Matrix4X4(OpenTK.Mathematics.Matrix4 matrix4) { diff --git a/Hypercube.Shared.Math/Matrix/Matrix4X4.cs b/Hypercube.Shared.Math/Matrix/Matrix4X4.cs index 5989091..40671a6 100644 --- a/Hypercube.Shared.Math/Matrix/Matrix4X4.cs +++ b/Hypercube.Shared.Math/Matrix/Matrix4X4.cs @@ -400,7 +400,39 @@ public static Matrix4X4 CreateScale(float x, float y, float z) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Matrix4X4 CreateRotation(Quaternion quaternion) { - return CreateRotation(quaternion.Direction, (float)quaternion.Angle.Theta); + var xx = quaternion.X * quaternion.X; + var yy = quaternion.Y * quaternion.Y; + var zz = quaternion.Z * quaternion.Z; + + var xy = quaternion.X * quaternion.Y; + var wz = quaternion.Z * quaternion.W; + var xz = quaternion.Z * quaternion.X; + var wy = quaternion.Y * quaternion.W; + var yz = quaternion.Y * quaternion.Z; + var wx = quaternion.X * quaternion.W; + + var x = new Vector4( + 1.0f - 2.0f * (yy + zz), + 2.0f * (xy + wz), + 2.0f * (xz - wy), + 0 + ); + + var y = new Vector4( + 2.0f * (xy - wz), + 1.0f - 2.0f * (zz + xx), + 2.0f * (yz + wx), + 0 + ); + + var z = new Vector4( + 2.0f * (xz + wy), + 2.0f * (yz - wx), + 1.0f - 2.0f * (yy + xx), + 0 + ); + + return new Matrix4X4(x, y, z, Vector4.UnitW); } [MethodImpl(MethodImplOptions.AggressiveInlining)] From ccf19449c2d5e9e0e846eb6064f2337217d6a7af Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:19:12 +1000 Subject: [PATCH 06/11] Clean up camera using --- Hypercube.Client/Graphics/Viewports/Camera2D.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Hypercube.Client/Graphics/Viewports/Camera2D.cs b/Hypercube.Client/Graphics/Viewports/Camera2D.cs index 6ca4c30..41fbae7 100644 --- a/Hypercube.Client/Graphics/Viewports/Camera2D.cs +++ b/Hypercube.Client/Graphics/Viewports/Camera2D.cs @@ -1,10 +1,7 @@ -using System.Numerics; +using Hypercube.Shared.Math; using Hypercube.Shared.Math.Matrix; using Hypercube.Shared.Math.Transform; using Hypercube.Shared.Math.Vector; -using Quaternion = Hypercube.Shared.Math.Quaternion; -using Vector2 = Hypercube.Shared.Math.Vector.Vector2; -using Vector3 = Hypercube.Shared.Math.Vector.Vector3; namespace Hypercube.Client.Graphics.Viewports; From 51b54c6110e4d416fa766cccb14a54e6e2683318 Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:36:51 +1000 Subject: [PATCH 07/11] Added transform2 --- .../Graphics/Viewports/Camera2D.cs | 11 ++-- .../Graphics/Viewports/ICamera.cs | 1 + Hypercube.Shared.Math/Transform/ITransform.cs | 8 +++ .../Transform/Transform2.Compatibility.cs | 17 +++++ Hypercube.Shared.Math/Transform/Transform2.cs | 62 +++++++++++++++++++ .../Transform/Transform3.Compatibility.cs | 16 +++++ .../Transform/{Transform.cs => Transform3.cs} | 2 +- 7 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 Hypercube.Shared.Math/Transform/ITransform.cs create mode 100644 Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs create mode 100644 Hypercube.Shared.Math/Transform/Transform2.cs create mode 100644 Hypercube.Shared.Math/Transform/Transform3.Compatibility.cs rename Hypercube.Shared.Math/Transform/{Transform.cs => Transform3.cs} (97%) diff --git a/Hypercube.Client/Graphics/Viewports/Camera2D.cs b/Hypercube.Client/Graphics/Viewports/Camera2D.cs index 41fbae7..c8a0496 100644 --- a/Hypercube.Client/Graphics/Viewports/Camera2D.cs +++ b/Hypercube.Client/Graphics/Viewports/Camera2D.cs @@ -5,20 +5,19 @@ namespace Hypercube.Client.Graphics.Viewports; -public class Camera2D : ICamera +public sealed class Camera2D : ICamera { - private Transform3 _transform = new(); + public Matrix4X4 Projection { get; private set; } public Vector3 Position => _transform.Position; public Vector3 Rotation => _transform.Rotation.ToEuler(); public Vector3 Scale => _transform.Scale; + public Vector2Int Size { get; private set; } private readonly float _zFar; private readonly float _zNear; - private Vector2Int Size { get; set; } - - private Vector2 HalfSize => Size / 2f; - public Hypercube.Shared.Math.Matrix.Matrix4X4 Projection { get; private set; } + + private Transform3 _transform = new(); public Camera2D(Vector2Int size, Vector2 position, float zNear, float zFar) { diff --git a/Hypercube.Client/Graphics/Viewports/ICamera.cs b/Hypercube.Client/Graphics/Viewports/ICamera.cs index fd8d030..946949b 100644 --- a/Hypercube.Client/Graphics/Viewports/ICamera.cs +++ b/Hypercube.Client/Graphics/Viewports/ICamera.cs @@ -10,6 +10,7 @@ public interface ICamera Vector3 Position { get; } Vector3 Rotation { get; } Vector3 Scale { get; } + Vector2Int Size { get; } void SetPosition(Vector3 position); void SetRotation(Vector3 rotation); diff --git a/Hypercube.Shared.Math/Transform/ITransform.cs b/Hypercube.Shared.Math/Transform/ITransform.cs new file mode 100644 index 0000000..aa3a560 --- /dev/null +++ b/Hypercube.Shared.Math/Transform/ITransform.cs @@ -0,0 +1,8 @@ +using Hypercube.Shared.Math.Matrix; + +namespace Hypercube.Shared.Math.Transform; + +public interface ITransform +{ + Matrix4X4 Matrix { get; } +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs b/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs new file mode 100644 index 0000000..8d6230d --- /dev/null +++ b/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs @@ -0,0 +1,17 @@ +using System.Runtime.CompilerServices; +using Hypercube.Shared.Math.Vector; + +namespace Hypercube.Shared.Math.Transform; + +public partial struct Transform2 +{ + /* + * Self Compatibility + */ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Transform3(Transform2 transform2) + { + return new Transform3(transform2.Position, new Quaternion(Vector3.UnitZ * transform2.Rotation), transform2.Scale); + } +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Transform/Transform2.cs b/Hypercube.Shared.Math/Transform/Transform2.cs new file mode 100644 index 0000000..5dcf94a --- /dev/null +++ b/Hypercube.Shared.Math/Transform/Transform2.cs @@ -0,0 +1,62 @@ +using Hypercube.Shared.Math.Matrix; +using Hypercube.Shared.Math.Vector; + +namespace Hypercube.Shared.Math.Transform; + +public partial struct Transform2 : ITransform +{ + public Matrix4X4 Matrix { get; private set; } + + public Vector2 Position { get; private set; } + public Angle Rotation { get; private set; } + public Vector2 Scale { get; private set; } + + public Transform2() + { + Position = Vector2.Zero; + Rotation = Angle.Zero; + Scale = Vector2.One; + + UpdateMatrix(); + } + + public Transform2(Vector2 position, Angle rotation, Vector2 scale) + { + Position = position; + Rotation = rotation; + Scale = scale; + + UpdateMatrix(); + } + + public Transform2 SetPosition(Vector2 position) + { + Position = position; + UpdateMatrix(); + + return this; + } + + public Transform2 SetRotation(Angle rotation) + { + Rotation = rotation; + UpdateMatrix(); + + return this; + } + + public Transform2 SetScale(Vector2 scale) + { + Scale = scale; + UpdateMatrix(); + + return this; + } + + private void UpdateMatrix() + { + Matrix = Matrix4X4.CreateTranslation(Position) * + Matrix4X4.CreateRotationZ(Rotation) * + Matrix4X4.CreateScale(Scale); + } +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Transform/Transform3.Compatibility.cs b/Hypercube.Shared.Math/Transform/Transform3.Compatibility.cs new file mode 100644 index 0000000..70af559 --- /dev/null +++ b/Hypercube.Shared.Math/Transform/Transform3.Compatibility.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace Hypercube.Shared.Math.Transform; + +public partial struct Transform3 +{ + /* + * Self Compatibility + */ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Transform2(Transform3 transform3) + { + return new Transform2(transform3.Position, new Angle(transform3.Rotation.ToEuler().Z), transform3.Scale); + } +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Transform/Transform.cs b/Hypercube.Shared.Math/Transform/Transform3.cs similarity index 97% rename from Hypercube.Shared.Math/Transform/Transform.cs rename to Hypercube.Shared.Math/Transform/Transform3.cs index 90b792d..467aa98 100644 --- a/Hypercube.Shared.Math/Transform/Transform.cs +++ b/Hypercube.Shared.Math/Transform/Transform3.cs @@ -3,7 +3,7 @@ namespace Hypercube.Shared.Math.Transform; -public struct Transform3 +public partial struct Transform3 : ITransform { public Matrix4X4 Matrix { get; private set; } From 761496b900e31760c11a65fd212ae59631cc4e7f Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:33:37 +1000 Subject: [PATCH 08/11] Updated math angle --- Hypercube.Shared.Math/Angle.cs | 112 +++++++++++++++++- .../FloatingPointEqualsExtension.cs | 6 +- Hypercube.Shared.Math/HyperMath.cs | 14 ++- Hypercube.Shared.Math/HyperMathF.cs | 16 +++ Hypercube.Shared.Math/Quaternion.cs | 4 +- .../Transform/Transform2.Compatibility.cs | 2 +- Hypercube.Shared.Math/Transform/Transform2.cs | 2 +- Hypercube.Shared.Math/Vector/Vector2.cs | 10 +- Hypercube.Shared.Math/Vector/Vector2Int.cs | 6 + Hypercube.UnitTests/Math/AngleTest.cs | 53 +++++++++ Hypercube.UnitTests/Math/FloatingPointTest.cs | 7 +- 11 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 Hypercube.Shared.Math/HyperMathF.cs create mode 100644 Hypercube.UnitTests/Math/AngleTest.cs diff --git a/Hypercube.Shared.Math/Angle.cs b/Hypercube.Shared.Math/Angle.cs index e52a056..48f5f4b 100644 --- a/Hypercube.Shared.Math/Angle.cs +++ b/Hypercube.Shared.Math/Angle.cs @@ -1,16 +1,120 @@ using System.Runtime.CompilerServices; +using Hypercube.Shared.Math.Extensions; +using Hypercube.Shared.Math.Vector; namespace Hypercube.Shared.Math; -public readonly struct Angle(double theta) +public readonly struct Angle : IEquatable, IEquatable { public static readonly Angle Zero = new(0); + + public readonly double Theta; + + public double Degrees + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Theta * HyperMath.RadiansToDegrees; + } + + public Vector2 Vector + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new((float)System.Math.Cos(Theta), (float)System.Math.Sin(Theta)); + } + + public Angle(double theta) + { + Theta = theta; + } + + public Angle(Vector2 vector2) + { + vector2 = vector2.Normalized; + Theta = System.Math.Atan2(vector2.X, vector2.Y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Angle other) + { + return Theta.AboutEquals(other.Theta); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(double other) + { + return Theta.AboutEquals(other); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + return (obj is double theta && Equals(theta)) || + (obj is Angle angle && Equals(angle)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + return Theta.GetHashCode(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() + { + return $"{Degrees} deg"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Angle a, double b) + { + return a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Angle a, double b) + { + return !a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Angle a, Angle b) + { + return a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Angle a, Angle b) + { + return !a.Equals(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator double(Angle angle) + { + return angle.Theta; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Angle(double radians) + { + return new Angle(radians); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator Angle(float radians) + { + return new Angle(radians); + } - public readonly double Theta = theta; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Angle FromDegrees(double degrees) + { + return new Angle(degrees * HyperMath.DegreesToRadians); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator float(Angle angle) + public static Angle FromDegrees(float degrees) { - return (float)angle.Theta; + return new Angle(degrees * HyperMath.DegreesToRadians); } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Extensions/FloatingPointEqualsExtension.cs b/Hypercube.Shared.Math/Extensions/FloatingPointEqualsExtension.cs index ac59f56..14cb0ac 100644 --- a/Hypercube.Shared.Math/Extensions/FloatingPointEqualsExtension.cs +++ b/Hypercube.Shared.Math/Extensions/FloatingPointEqualsExtension.cs @@ -2,15 +2,15 @@ public static class FloatingPointEqualsExtension { - public static bool AboutEquals(this double a, double b) + public static bool AboutEquals(this double a, double b, double tolerance = 1E-15d) { var epsilon = System.Math.Max(System.Math.Abs(a), System.Math.Abs(b)) * 1E-15d; return System.Math.Abs(a - b) <= epsilon; } - public static bool AboutEquals(this float a, float b) + public static bool AboutEquals(this float a, float b, float tolerance = 1E-15f) { - var epsilon = System.Math.Max(System.Math.Abs(a), System.Math.Abs(b)) * 1E-15f; + var epsilon = System.Math.Max(System.Math.Abs(a), System.Math.Abs(b)) * tolerance ; return System.Math.Abs(a - b) <= epsilon; } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/HyperMath.cs b/Hypercube.Shared.Math/HyperMath.cs index ab118a4..de5bb5b 100644 --- a/Hypercube.Shared.Math/HyperMath.cs +++ b/Hypercube.Shared.Math/HyperMath.cs @@ -2,7 +2,15 @@ public static class HyperMath { - // ReSharper disable once InconsistentNaming - public const float PI = MathF.PI; - public const float PiOver2 = PI / 2; + public const double PI = System.Math.PI; + + public const double PIOver2 = PI / 2; + public const double PIOver4 = PI / 4; + public const double PIOver6 = PI / 6; + + public const double TwoPI = 2 * PI; + public const double ThreePiOver2 = 3 * PI / 2; + + public const double RadiansToDegrees = 180 / PI; + public const double DegreesToRadians = PI / 180; } \ No newline at end of file diff --git a/Hypercube.Shared.Math/HyperMathF.cs b/Hypercube.Shared.Math/HyperMathF.cs new file mode 100644 index 0000000..d11822e --- /dev/null +++ b/Hypercube.Shared.Math/HyperMathF.cs @@ -0,0 +1,16 @@ +namespace Hypercube.Shared.Math; + +public static class HyperMathF +{ + public const float PI = MathF.PI; + + public const float PIOver2 = PI / 2; + public const float PIOver4 = PI / 4; + public const float PIOver6 = PI / 6; + + public const float TwoPI = 2 * PI; + public const float ThreePiOver2 = 3 * PI / 2; + + public const float RadiansToDegrees = 180 / PI; + public const float DegreesToRadians = PI / 180; +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Quaternion.cs b/Hypercube.Shared.Math/Quaternion.cs index a2ca47c..49fc631 100644 --- a/Hypercube.Shared.Math/Quaternion.cs +++ b/Hypercube.Shared.Math/Quaternion.cs @@ -215,7 +215,7 @@ public static Vector3 ToEuler(Quaternion quaternion) // Singularity at north pole return new Vector3( 0, - HyperMath.PiOver2, + HyperMathF.PIOver2, 2f * MathF.Atan2(quaternion.X, quaternion.W) ); @@ -223,7 +223,7 @@ public static Vector3 ToEuler(Quaternion quaternion) // Singularity at south pole return new Vector3( 0, - -HyperMath.PiOver2, + -HyperMathF.PIOver2, -2f * MathF.Atan2(quaternion.X, quaternion.W) ); diff --git a/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs b/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs index 8d6230d..d906155 100644 --- a/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs +++ b/Hypercube.Shared.Math/Transform/Transform2.Compatibility.cs @@ -12,6 +12,6 @@ public partial struct Transform2 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator Transform3(Transform2 transform2) { - return new Transform3(transform2.Position, new Quaternion(Vector3.UnitZ * transform2.Rotation), transform2.Scale); + return new Transform3(transform2.Position, new Quaternion(Vector3.UnitZ * (float)transform2.Rotation), transform2.Scale); } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Transform/Transform2.cs b/Hypercube.Shared.Math/Transform/Transform2.cs index 5dcf94a..c5d4674 100644 --- a/Hypercube.Shared.Math/Transform/Transform2.cs +++ b/Hypercube.Shared.Math/Transform/Transform2.cs @@ -56,7 +56,7 @@ public Transform2 SetScale(Vector2 scale) private void UpdateMatrix() { Matrix = Matrix4X4.CreateTranslation(Position) * - Matrix4X4.CreateRotationZ(Rotation) * + Matrix4X4.CreateRotationZ((float)Rotation) * Matrix4X4.CreateScale(Scale); } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Vector/Vector2.cs b/Hypercube.Shared.Math/Vector/Vector2.cs index 392d3b0..da55088 100644 --- a/Hypercube.Shared.Math/Vector/Vector2.cs +++ b/Hypercube.Shared.Math/Vector/Vector2.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Hypercube.Shared.Math.Extensions; namespace Hypercube.Shared.Math.Vector; @@ -33,6 +34,12 @@ public float Length get => MathF.Sqrt(LengthSquared); } + public Vector2 Normalized + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this / Length; + } + public Vector2(float x, float y) { X = x; @@ -62,7 +69,8 @@ public Vector2 WithY(float value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(Vector2 other) { - return X.Equals(other.X) && Y.Equals(other.Y); + return X.AboutEquals(other.X) && + Y.AboutEquals(other.Y); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Hypercube.Shared.Math/Vector/Vector2Int.cs b/Hypercube.Shared.Math/Vector/Vector2Int.cs index 3b7b8ff..91c7a2b 100644 --- a/Hypercube.Shared.Math/Vector/Vector2Int.cs +++ b/Hypercube.Shared.Math/Vector/Vector2Int.cs @@ -32,6 +32,12 @@ public float Length [MethodImpl(MethodImplOptions.AggressiveInlining)] get => MathF.Sqrt(LengthSquared); } + + public Vector2Int Normalized + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this / Length; + } public Vector2Int(int x, int y) { diff --git a/Hypercube.UnitTests/Math/AngleTest.cs b/Hypercube.UnitTests/Math/AngleTest.cs new file mode 100644 index 0000000..8ebaaab --- /dev/null +++ b/Hypercube.UnitTests/Math/AngleTest.cs @@ -0,0 +1,53 @@ +using Hypercube.Shared.Math; +using Hypercube.Shared.Math.Vector; + +namespace Hypercube.UnitTests.Math; + +public static class AngleTest +{ + [Test] + public static void Degrees() + { + Assert.Multiple(() => + { + Assert.That(new Angle(HyperMath.PI).Degrees, Is.EqualTo(180d).Within(0.01d)); + Assert.That(new Angle(HyperMath.PIOver2).Degrees, Is.EqualTo(90d).Within(0.01d)); + Assert.That(new Angle(HyperMath.PIOver4).Degrees, Is.EqualTo(45d).Within(0.01d)); + Assert.That(new Angle(HyperMath.PIOver6).Degrees, Is.EqualTo(30d).Within(0.01d)); + + Assert.That(Angle.FromDegrees(180d), Is.EqualTo(new Angle(HyperMath.PI))); + Assert.That(Angle.FromDegrees(90d), Is.EqualTo(new Angle(HyperMath.PIOver2))); + Assert.That(Angle.FromDegrees(45d), Is.EqualTo(new Angle(HyperMath.PIOver4))); + Assert.That(Angle.FromDegrees(30d), Is.EqualTo(new Angle(HyperMath.PIOver6))); + }); + Assert.Pass($"{nameof(Angle)} degrees passed"); + } + + [Test] + public static void Vector() + { + Assert.Multiple(() => + { + Assert.That(Angle.Zero.GetRoundVector(), Is.EqualTo(Vector2.UnitX)); + Assert.That(new Angle(HyperMath.PI).GetRoundVector(), Is.EqualTo(-Vector2.UnitX)); + Assert.That(new Angle(-HyperMath.PI).GetRoundVector(), Is.EqualTo(-Vector2.UnitX)); + + Assert.That(new Angle(HyperMath.PIOver2).GetRoundVector(), Is.EqualTo(Vector2.UnitY)); + Assert.That(new Angle(HyperMath.ThreePiOver2).GetRoundVector(), Is.EqualTo(-Vector2.UnitY)); + Assert.That(new Angle(-HyperMath.PIOver2).GetRoundVector(), Is.EqualTo(-Vector2.UnitY)); + + Assert.That(new Angle(HyperMath.PIOver4).GetRoundVector(), Is.EqualTo(Vector2.One.Normalized)); + Assert.That(new Angle(HyperMath.ThreePiOver2 - HyperMath.PIOver4).GetRoundVector(), Is.EqualTo(-Vector2.One.Normalized)); + }); + + Assert.Pass($"{nameof(Angle)} vector passed"); + } + + private static Vector2 GetRoundVector(this Angle angle) + { + var vector = angle.Vector; + var x = MathF.Abs(vector.X) < 1e-15f ? 0 : vector.X; + var y = MathF.Abs(vector.Y) < 1e-15f ? 0 : vector.Y; + return new Vector2(x, y); + } +} \ No newline at end of file diff --git a/Hypercube.UnitTests/Math/FloatingPointTest.cs b/Hypercube.UnitTests/Math/FloatingPointTest.cs index 7b1d5f4..dd14565 100644 --- a/Hypercube.UnitTests/Math/FloatingPointTest.cs +++ b/Hypercube.UnitTests/Math/FloatingPointTest.cs @@ -7,8 +7,11 @@ public class FloatingPointTest [Test] public void Equals() { - Assert.That((0.1d + 0.2d).AboutEquals(0.3d)); - Assert.That((0.1f + 0.2f).AboutEquals(0.3f)); + Assert.Multiple(() => + { + Assert.That((0.1d + 0.2d).AboutEquals(0.3d)); + Assert.That((0.1f + 0.2f).AboutEquals(0.3f)); + }); Assert.Pass($"{nameof(FloatingPointEqualsExtension)} passed"); } From e981967fb7f8f51b229cd63f22f0200983aac813 Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:36:48 +1000 Subject: [PATCH 09/11] Clean up math unit tests --- Hypercube.UnitTests/Math/AngleTest.cs | 1 + Hypercube.UnitTests/Math/FloatingPointTest.cs | 4 +- Hypercube.UnitTests/Math/Matrix4X4Test.cs | 36 +++---------- Hypercube.UnitTests/Math/QuaternionTest.cs | 53 +++++++------------ 4 files changed, 29 insertions(+), 65 deletions(-) diff --git a/Hypercube.UnitTests/Math/AngleTest.cs b/Hypercube.UnitTests/Math/AngleTest.cs index 8ebaaab..fb85b3f 100644 --- a/Hypercube.UnitTests/Math/AngleTest.cs +++ b/Hypercube.UnitTests/Math/AngleTest.cs @@ -20,6 +20,7 @@ public static void Degrees() Assert.That(Angle.FromDegrees(45d), Is.EqualTo(new Angle(HyperMath.PIOver4))); Assert.That(Angle.FromDegrees(30d), Is.EqualTo(new Angle(HyperMath.PIOver6))); }); + Assert.Pass($"{nameof(Angle)} degrees passed"); } diff --git a/Hypercube.UnitTests/Math/FloatingPointTest.cs b/Hypercube.UnitTests/Math/FloatingPointTest.cs index dd14565..dcd0954 100644 --- a/Hypercube.UnitTests/Math/FloatingPointTest.cs +++ b/Hypercube.UnitTests/Math/FloatingPointTest.cs @@ -2,10 +2,10 @@ namespace Hypercube.UnitTests.Math; -public class FloatingPointTest +public static class FloatingPointTest { [Test] - public void Equals() + public static void Equals() { Assert.Multiple(() => { diff --git a/Hypercube.UnitTests/Math/Matrix4X4Test.cs b/Hypercube.UnitTests/Math/Matrix4X4Test.cs index 79c23bd..9b9515e 100644 --- a/Hypercube.UnitTests/Math/Matrix4X4Test.cs +++ b/Hypercube.UnitTests/Math/Matrix4X4Test.cs @@ -3,41 +3,21 @@ namespace Hypercube.UnitTests.Math; -public sealed class Matrix4X4Test +public static class Matrix4X4Test { [Test] - public void Equals() - { - var matrixA = new Matrix4X4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); - var matrixB = new Matrix4X4(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); - - // ReSharper disable once EqualExpressionComparison - Assert.That(matrixA == matrixA); - Assert.That(matrixA.Equals(matrixA)); - Assert.That(matrixA.Equals((object)matrixA)); - - var matrixAClone = new Matrix4X4(matrixA); - Assert.That(matrixA == matrixAClone); - Assert.That(matrixA.Equals(matrixAClone)); - Assert.That(matrixA.Equals((object)matrixAClone)); - - Assert.That(matrixA != matrixB); - Assert.That(!matrixA.Equals(matrixB)); - Assert.That(!matrixA.Equals((object)matrixB)); - - Assert.Pass($"{nameof(Matrix4X4)} equals passed"); - } - - [Test] - public void Multiplication() + public static void Multiplication() { var matrixA = new Matrix4X4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); var matrixB = new Matrix4X4(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); var vectorA = new Vector4(20, 60, 32, 7); - Assert.That(matrixA * matrixB == new Matrix4X4(80, 70, 60, 50, 240, 214, 188, 162, 400, 358, 316, 274, 560, 502, 444, 386)); - Assert.That(matrixA * vectorA == new Vector4(264, 740, 1216, 1692)); - Assert.That(matrixB * vectorA == new Vector4(1759, 1283, 807, 331)); + Assert.Multiple(() => + { + Assert.That(matrixA * matrixB, Is.EqualTo(new Matrix4X4(80, 70, 60, 50, 240, 214, 188, 162, 400, 358, 316, 274, 560, 502, 444, 386))); + Assert.That(matrixA * vectorA, Is.EqualTo(new Vector4(264, 740, 1216, 1692))); + Assert.That(matrixB * vectorA, Is.EqualTo(new Vector4(1759, 1283, 807, 331))); + }); Assert.Pass($"{nameof(Matrix4X4)} multiplication passed"); } diff --git a/Hypercube.UnitTests/Math/QuaternionTest.cs b/Hypercube.UnitTests/Math/QuaternionTest.cs index 7aa1e37..8d786e9 100644 --- a/Hypercube.UnitTests/Math/QuaternionTest.cs +++ b/Hypercube.UnitTests/Math/QuaternionTest.cs @@ -3,69 +3,52 @@ namespace Hypercube.UnitTests.Math; -public class QuaternionTest +public static class QuaternionTest { - [Test] - public void Equals() - { - var quaternionA = new Quaternion(1, 2, 3, 4); - var quaternionB = new Quaternion(10, 22, 40, 32); - - // ReSharper disable once EqualExpressionComparison - Assert.That(quaternionA == quaternionA); - Assert.That(quaternionA.Equals(quaternionA)); - Assert.That(quaternionA.Equals((object)quaternionA)); - - var quaternionAClone = new Quaternion(quaternionA); - Assert.That(quaternionA == quaternionAClone); - Assert.That(quaternionA.Equals(quaternionAClone)); - Assert.That(quaternionA.Equals((object)quaternionAClone)); - - Assert.That(quaternionA != quaternionB); - Assert.That(!quaternionA.Equals(quaternionB)); - Assert.That(!quaternionA.Equals((object)quaternionB)); - - Assert.Pass($"{nameof(Quaternion)} equals passed"); - } - /// /// Can ba useful, cite to 3d convert: https://www.andre-gaschler.com/rotationconverter/ /// [Test] - public void ToEuler() + public static void ToEuler() { var eulerA = new Quaternion(1, 2, 3, 4).ToEuler(); var eulerB = new Quaternion(0, 0.6767456f, 0.4308296f, 0.5969936f).ToEuler(); - Assert.That(eulerA == new Vector3(-0.19739556f, 0.8232120f, 1.3734008f)); - Assert.That(eulerB == new Vector3(-1.42767680f, 0.9407929f, 2.0799970f)); + Assert.Multiple(() => + { + Assert.That(eulerA, Is.EqualTo(new Vector3(-0.19739556f, 0.8232120f, 1.3734008f))); + Assert.That(eulerB, Is.EqualTo(new Vector3(-1.42767680f, 0.9407929f, 2.0799970f))); + }); Assert.Pass($"{nameof(Quaternion)} to euler passed"); } [Test] - public void FromEulerUnit() + public static void FromEulerUnit() { var quaternionUnitX = Quaternion.FromEuler(Vector3.UnitX); var quaternionUnitY = Quaternion.FromEuler(Vector3.UnitY); var quaternionUnitZ = Quaternion.FromEuler(Vector3.UnitZ); - - Assert.That(quaternionUnitX == new Quaternion(0.47942555f, 0, 0, 0.87758255f)); - Assert.That(quaternionUnitY == new Quaternion(0, 0.47942555f, 0, 0.87758255f)); - Assert.That(quaternionUnitZ == new Quaternion(0, 0, 0.47942555f, 0.87758255f)); + + Assert.Multiple(() => + { + Assert.That(quaternionUnitX, Is.EqualTo(new Quaternion(0.47942555f, 0, 0, 0.87758255f))); + Assert.That(quaternionUnitY, Is.EqualTo(new Quaternion(0, 0.47942555f, 0, 0.87758255f))); + Assert.That(quaternionUnitZ, Is.EqualTo(new Quaternion(0, 0, 0.47942555f, 0.87758255f))); + }); Assert.Pass($"{nameof(Quaternion)} from euler unit passed"); } [Test] - public void FromEulerConvert() + public static void FromEulerConvert() { var quaternionA = new Quaternion(0.8232120f, 0.6767456f, 0.4308296f, 0.5969936f); var eulerA = quaternionA.ToEuler(); var fromA = Quaternion.FromEuler(eulerA); - // The losses on this convert are fucked up. - Assert.That(fromA == new Quaternion(0.6355612f, 0.5224818f, 0.33262214f, 0.4609092f)); + // The losses on this convert are fucked up + Assert.That(fromA, Is.EqualTo(new Quaternion(0.6355612f, 0.5224818f, 0.33262214f, 0.4609092f))); Assert.Pass($"{nameof(Quaternion)} from euler convert passed"); } From 2f9db053725862f2f1ead5ecaa2520976a9e0280 Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:11:58 +1000 Subject: [PATCH 10/11] Added CreateTransform implementation for matrix4 --- Hypercube.Shared.Math/Matrix/Matrix4X4.cs | 44 +++++++++++++++++++ Hypercube.Shared.Math/Transform/Transform2.cs | 4 +- Hypercube.Shared.Math/Transform/Transform3.cs | 4 +- Hypercube.Shared.Math/Vector/Vector4.cs | 4 ++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/Hypercube.Shared.Math/Matrix/Matrix4X4.cs b/Hypercube.Shared.Math/Matrix/Matrix4X4.cs index 40671a6..6052a45 100644 --- a/Hypercube.Shared.Math/Matrix/Matrix4X4.cs +++ b/Hypercube.Shared.Math/Matrix/Matrix4X4.cs @@ -335,6 +335,50 @@ public static Matrix4X4 Transpose(Matrix4X4 matrix4X4) return new Matrix4X4(matrix4X4.Column0, matrix4X4.Column1, matrix4X4.Column2, matrix4X4.Column3); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4X4 CreateTransform(Transform3 transform3) + { + return CreateTransform(transform3.Position, transform3.Rotation, transform3.Scale); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4X4 CreateTransform(Vector3 position, Quaternion quaternion, Vector3 scale) + { + var xx = quaternion.X * quaternion.X; + var yy = quaternion.Y * quaternion.Y; + var zz = quaternion.Z * quaternion.Z; + + var xy = quaternion.X * quaternion.Y; + var wz = quaternion.Z * quaternion.W; + var xz = quaternion.Z * quaternion.X; + var wy = quaternion.Y * quaternion.W; + var yz = quaternion.Y * quaternion.Z; + var wx = quaternion.X * quaternion.W; + + var x3 = new Vector3( + 1.0f - 2.0f * (yy + zz), + 2.0f * (xy + wz), + 2.0f * (xz - wy) + ) * scale.X; + var x = new Vector4(x3, (x3 * position.X).Sum()); + + var y3 = new Vector3( + 2.0f * (xy - wz), + 1.0f - 2.0f * (zz + xx), + 2.0f * (yz + wx) + )* scale.Y; + var y = new Vector4(y3, (y3 * position.Y).Sum()); + + var z3 = new Vector3( + 2.0f * (xz + wy), + 2.0f * (yz - wx), + 1.0f - 2.0f * (yy + xx) + ) * scale.Z; + var z = new Vector4(z3, (z3 * position.Z).Sum()); + + return new Matrix4X4(x, y, z, Vector4.UnitW); + } + /// /// Creating scale matrix /// diff --git a/Hypercube.Shared.Math/Transform/Transform2.cs b/Hypercube.Shared.Math/Transform/Transform2.cs index c5d4674..4641721 100644 --- a/Hypercube.Shared.Math/Transform/Transform2.cs +++ b/Hypercube.Shared.Math/Transform/Transform2.cs @@ -55,8 +55,6 @@ public Transform2 SetScale(Vector2 scale) private void UpdateMatrix() { - Matrix = Matrix4X4.CreateTranslation(Position) * - Matrix4X4.CreateRotationZ((float)Rotation) * - Matrix4X4.CreateScale(Scale); + Matrix = Matrix4X4.CreateTransform(this); } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Transform/Transform3.cs b/Hypercube.Shared.Math/Transform/Transform3.cs index 467aa98..8f4bccf 100644 --- a/Hypercube.Shared.Math/Transform/Transform3.cs +++ b/Hypercube.Shared.Math/Transform/Transform3.cs @@ -61,8 +61,6 @@ public Transform3 SetScale(Vector3 scale) private void UpdateMatrix() { - Matrix = Matrix4X4.CreateTranslation(Position) * - Matrix4X4.CreateRotation(Rotation) * - Matrix4X4.CreateScale(Scale); + Matrix = Matrix4X4.CreateTransform(this); } } \ No newline at end of file diff --git a/Hypercube.Shared.Math/Vector/Vector4.cs b/Hypercube.Shared.Math/Vector/Vector4.cs index 390c189..1ef9437 100644 --- a/Hypercube.Shared.Math/Vector/Vector4.cs +++ b/Hypercube.Shared.Math/Vector/Vector4.cs @@ -56,6 +56,10 @@ public Vector4(float value) : this(value, value, value, value) public Vector4(Vector2 vector2, float z, float w) : this(vector2.X, vector2.Y, z, w) { } + + public Vector4(Vector3 vector3, float w) : this(vector3.X, vector3.Y, vector3.Z, w) + { + } public Vector4(Vector4 vector4, float w) : this(vector4.X, vector4.Y, vector4.Z, w) { From c89569d7a186d6020bad8f7e805dbf49fb2b0778 Mon Sep 17 00:00:00 2001 From: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:20:48 +1000 Subject: [PATCH 11/11] Clean up CreateTransform --- Hypercube.Shared.Math/Matrix/Matrix4X4.cs | 39 +++++++++++------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Hypercube.Shared.Math/Matrix/Matrix4X4.cs b/Hypercube.Shared.Math/Matrix/Matrix4X4.cs index 6052a45..f345b72 100644 --- a/Hypercube.Shared.Math/Matrix/Matrix4X4.cs +++ b/Hypercube.Shared.Math/Matrix/Matrix4X4.cs @@ -355,28 +355,25 @@ public static Matrix4X4 CreateTransform(Vector3 position, Quaternion quaternion, var yz = quaternion.Y * quaternion.Z; var wx = quaternion.X * quaternion.W; - var x3 = new Vector3( - 1.0f - 2.0f * (yy + zz), - 2.0f * (xy + wz), - 2.0f * (xz - wy) - ) * scale.X; - var x = new Vector4(x3, (x3 * position.X).Sum()); - - var y3 = new Vector3( - 2.0f * (xy - wz), - 1.0f - 2.0f * (zz + xx), - 2.0f * (yz + wx) - )* scale.Y; - var y = new Vector4(y3, (y3 * position.Y).Sum()); + var rx1 = (1.0f - 2.0f * (yy + zz)) * scale.X; + var rx2 = 2.0f * (xy + wz) * scale.X; + var rx3 = 2.0f * (xz - wy) * scale.X; + var rx4 = rx1 * position.X + rx2 * position.X + rx3 * position.X; + var ry1 = 2.0f * (xy - wz) * scale.Y; + var ry2 = (1.0f - 2.0f * (zz + xx)) * scale.Y; + var ry3 = 2.0f * (yz + wx) * scale.Y; + var ry4 = ry1 * position.Y + ry2 * position.Y + ry3 * position.Y; + var rz1 = 2.0f * (xz + wy) * scale.Z; + var rz2 = 2.0f * (yz - wx) * scale.Z; + var rz3 = (1.0f - 2.0f * (yy + xx)) * scale.Z; + var rz4 = rz1 * position.Z + rz2 * position.Z + rz3 * position.Z; - var z3 = new Vector3( - 2.0f * (xz + wy), - 2.0f * (yz - wx), - 1.0f - 2.0f * (yy + xx) - ) * scale.Z; - var z = new Vector4(z3, (z3 * position.Z).Sum()); - - return new Matrix4X4(x, y, z, Vector4.UnitW); + return new Matrix4X4( + rx1, rx2, rx3, rx4, + ry1, ry2, ry3, ry4, + rz1, rz2, rz3, rz4, + 0, 0, 0, 1 + ); } ///