diff --git a/Hypercube.Client/Graphics/Viewports/Camera2D.cs b/Hypercube.Client/Graphics/Viewports/Camera2D.cs index fde8ce8..c8a0496 100644 --- a/Hypercube.Client/Graphics/Viewports/Camera2D.cs +++ b/Hypercube.Client/Graphics/Viewports/Camera2D.cs @@ -1,64 +1,56 @@ -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 sealed class Camera2D : ICamera { - public Vector3 Position { get; private set; } - public Vector3 Rotation { get; private set; } - public Vector3 Scale { get; private set; } = Vector3.One; + 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 Matrix4X4 Projection { get; private set; } + private Transform3 _transform = new(); 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.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/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 new file mode 100644 index 0000000..de5bb5b --- /dev/null +++ b/Hypercube.Shared.Math/HyperMath.cs @@ -0,0 +1,16 @@ +namespace Hypercube.Shared.Math; + +public static class HyperMath +{ + 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/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 17cef3d..f345b72 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; @@ -334,6 +335,47 @@ 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 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; + + return new Matrix4X4( + rx1, rx2, rx3, rx4, + ry1, ry2, ry3, ry4, + rz1, rz2, rz3, rz4, + 0, 0, 0, 1 + ); + } + /// /// Creating scale matrix /// @@ -395,6 +437,44 @@ public static Matrix4X4 CreateScale(float x, float y, float z) return result; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4X4 CreateRotation(Quaternion quaternion) + { + 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)] public static Matrix4X4 CreateRotation(Vector3 direction, float angle) diff --git a/Hypercube.Shared.Math/Quaternion.cs b/Hypercube.Shared.Math/Quaternion.cs new file mode 100644 index 0000000..49fc631 --- /dev/null +++ b/Hypercube.Shared.Math/Quaternion.cs @@ -0,0 +1,236 @@ +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 +{ + private const float SingularityThreshold = 0.4999995f; + + 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(Vector3 vector3) : this(FromEuler(vector3).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 Vector3 ToEuler() + { + return ToEuler(this); + } + + [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 override string ToString() + { + return Vector.ToString(); + } + + [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(float x, float y, float z) + { + return FromEuler(new Vector3(x, y, z)); + } + + /// + /// Created new from given Euler angles in radians. + /// + /// Taken from OpenTK.Mathematics/Data/Quaternion.cs + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Quaternion FromEuler(Vector3 vector3) + { + 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/Quaternion.cs + /// + /// + /// Euler angle in radians + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 ToEuler(Quaternion quaternion) + { + 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, + HyperMathF.PIOver2, + 2f * MathF.Atan2(quaternion.X, quaternion.W) + ); + + if (singularityTest < -SingularityThreshold * unit) + // Singularity at south pole + return new Vector3( + 0, + -HyperMathF.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.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..d906155 --- /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 * (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 new file mode 100644 index 0000000..4641721 --- /dev/null +++ b/Hypercube.Shared.Math/Transform/Transform2.cs @@ -0,0 +1,60 @@ +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.CreateTransform(this); + } +} \ 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/Transform3.cs b/Hypercube.Shared.Math/Transform/Transform3.cs new file mode 100644 index 0000000..8f4bccf --- /dev/null +++ b/Hypercube.Shared.Math/Transform/Transform3.cs @@ -0,0 +1,66 @@ +using Hypercube.Shared.Math.Matrix; +using Hypercube.Shared.Math.Vector; + +namespace Hypercube.Shared.Math.Transform; + +public partial struct Transform3 : ITransform +{ + 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.CreateTransform(this); + } +} \ No newline at end of file diff --git a/Hypercube.Shared.Math/Vector/Vector2.cs b/Hypercube.Shared.Math/Vector/Vector2.cs index 4742380..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; @@ -21,10 +22,22 @@ 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 Normalized + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this / Length; } public Vector2(float x, float y) @@ -56,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 554e67f..91c7a2b 100644 --- a/Hypercube.Shared.Math/Vector/Vector2Int.cs +++ b/Hypercube.Shared.Math/Vector/Vector2Int.cs @@ -21,10 +21,22 @@ 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 Normalized + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this / Length; } 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..1ef9437 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; @@ -35,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) { @@ -47,6 +72,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/AngleTest.cs b/Hypercube.UnitTests/Math/AngleTest.cs new file mode 100644 index 0000000..fb85b3f --- /dev/null +++ b/Hypercube.UnitTests/Math/AngleTest.cs @@ -0,0 +1,54 @@ +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..dcd0954 100644 --- a/Hypercube.UnitTests/Math/FloatingPointTest.cs +++ b/Hypercube.UnitTests/Math/FloatingPointTest.cs @@ -2,13 +2,16 @@ namespace Hypercube.UnitTests.Math; -public class FloatingPointTest +public static class FloatingPointTest { [Test] - public void Equals() + public static 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"); } 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 new file mode 100644 index 0000000..8d786e9 --- /dev/null +++ b/Hypercube.UnitTests/Math/QuaternionTest.cs @@ -0,0 +1,55 @@ +using Hypercube.Shared.Math; +using Hypercube.Shared.Math.Vector; + +namespace Hypercube.UnitTests.Math; + +public static class QuaternionTest +{ + /// + /// Can ba useful, cite to 3d convert: https://www.andre-gaschler.com/rotationconverter/ + /// + [Test] + 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.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 static void FromEulerUnit() + { + var quaternionUnitX = Quaternion.FromEuler(Vector3.UnitX); + var quaternionUnitY = Quaternion.FromEuler(Vector3.UnitY); + var quaternionUnitZ = Quaternion.FromEuler(Vector3.UnitZ); + + 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 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, Is.EqualTo(new Quaternion(0.6355612f, 0.5224818f, 0.33262214f, 0.4609092f))); + + Assert.Pass($"{nameof(Quaternion)} from euler convert passed"); + } +} \ No newline at end of file