From d514efebba4ea24026bbf247a7c584936d0c1929 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 13:29:59 +0200 Subject: [PATCH] Make CultMath HLSL-shaped: writable components, bool-vector comparisons HLSL is now CultMath's semantic authority where it defines one. - float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with public mutable fields instead of record structs. Indexers and swizzles (float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx) have setters. - ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and GetHashCode stay value-based so vectors remain dictionary keys. quaternion is not an HLSL type and keeps a bool ==. - Scalar/vector arithmetic and comparisons get explicit overloads on both sides instead of leaning on implicit splat. - New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder). - New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all, select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2, double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor functions. - simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL mirror follows as cultmath_snoise(float2/float3). - Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order. Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to Unity.Mathematics reference values, and an HLSL-shaped source block compiles under using static CultMath.math. Co-Authored-By: Claude Opus 5 --- packages/cultmath/shaders/CultMath.hlsl | 61 +++- packages/cultmath/src/CultMath/Random.cs | 6 +- packages/cultmath/src/CultMath/bool2.cs | 45 ++- packages/cultmath/src/CultMath/bool3.cs | 55 +++ packages/cultmath/src/CultMath/bool3.cs.meta | 2 + packages/cultmath/src/CultMath/bool4.cs | 59 ++++ packages/cultmath/src/CultMath/bool4.cs.meta | 2 + packages/cultmath/src/CultMath/double2.cs | 45 ++- packages/cultmath/src/CultMath/double3.cs | 52 ++- packages/cultmath/src/CultMath/float2.cs | 78 +++- packages/cultmath/src/CultMath/float2x2.cs | 69 ++++ .../cultmath/src/CultMath/float2x2.cs.meta | 2 + packages/cultmath/src/CultMath/float3.cs | 140 +++++++- packages/cultmath/src/CultMath/float3x3.cs | 125 +++++++ .../cultmath/src/CultMath/float3x3.cs.meta | 2 + packages/cultmath/src/CultMath/float4.cs | 116 +++++- packages/cultmath/src/CultMath/int2.cs | 45 ++- packages/cultmath/src/CultMath/int3.cs | 64 ++++ packages/cultmath/src/CultMath/int3.cs.meta | 2 + packages/cultmath/src/CultMath/int4.cs | 68 ++++ packages/cultmath/src/CultMath/int4.cs.meta | 2 + packages/cultmath/src/CultMath/math.cs | 189 +++++++++- packages/cultmath/src/CultMath/quaternion.cs | 29 +- .../tests/CultMath.Tests/HlslMirrorTests.cs | 3 +- .../CultMath.Tests/HlslSemanticsTests.cs | 332 ++++++++++++++++++ .../CultMath.Tests/HlslSemanticsTests.cs.meta | 2 + .../HlslSourceCompatibilityTests.cs | 85 +++++ .../HlslSourceCompatibilityTests.cs.meta | 2 + .../tests/CultMath.Tests/MathTests.cs | 4 +- 29 files changed, 1610 insertions(+), 76 deletions(-) create mode 100644 packages/cultmath/src/CultMath/bool3.cs create mode 100644 packages/cultmath/src/CultMath/bool3.cs.meta create mode 100644 packages/cultmath/src/CultMath/bool4.cs create mode 100644 packages/cultmath/src/CultMath/bool4.cs.meta create mode 100644 packages/cultmath/src/CultMath/float2x2.cs create mode 100644 packages/cultmath/src/CultMath/float2x2.cs.meta create mode 100644 packages/cultmath/src/CultMath/float3x3.cs create mode 100644 packages/cultmath/src/CultMath/float3x3.cs.meta create mode 100644 packages/cultmath/src/CultMath/int3.cs create mode 100644 packages/cultmath/src/CultMath/int3.cs.meta create mode 100644 packages/cultmath/src/CultMath/int4.cs create mode 100644 packages/cultmath/src/CultMath/int4.cs.meta create mode 100644 packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs create mode 100644 packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs.meta create mode 100644 packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs create mode 100644 packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs.meta diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index 4c00fb69..f3c398d6 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -73,11 +73,64 @@ float cultmath_smootherstep(float value) return value * value * value * (value * (value * 6.0 - 15.0) + 10.0); } -float2 cultmath_simplex_mod289(float2 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } -float3 cultmath_simplex_mod289(float3 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } -float3 cultmath_simplex_permute(float3 value) { return cultmath_simplex_mod289(((value * 34.0) + 1.0) * value); } +float2 cultmath_snoise_mod289(float2 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } +float3 cultmath_snoise_mod289(float3 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } +float4 cultmath_snoise_mod289(float4 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } +float3 cultmath_snoise_permute(float3 value) { return cultmath_snoise_mod289(((value * 34.0) + 1.0) * value); } +float4 cultmath_snoise_permute(float4 value) { return cultmath_snoise_mod289(((value * 34.0) + 1.0) * value); } -float cultmath_simplex_noise(float2 value) +// Ashima Arts / Ian McEwan 3D simplex noise (MIT), same float32 evaluation +// order as the C# math.snoise(float3) mirror. +float cultmath_snoise(float3 value) +{ + const float2 c = float2(1.0 / 6.0, 1.0 / 3.0); + float3 i = floor(value + dot(value, c.yyy)); + float3 x0 = value - i + dot(i, c.xxx); + float3 g = step(x0.yzx, x0.xyz); + float3 l = 1.0 - g; + float3 i1 = min(g.xyz, l.zxy); + float3 i2 = max(g.xyz, l.zxy); + float3 x1 = x0 - i1 + c.xxx; + float3 x2 = x0 - i2 + c.yyy; + float3 x3 = x0 - 0.5; + + i = cultmath_snoise_mod289(i); + float4 p = cultmath_snoise_permute(cultmath_snoise_permute(cultmath_snoise_permute( + i.z + float4(0.0, i1.z, i2.z, 1.0)) + i.y + float4(0.0, i1.y, i2.y, 1.0)) + i.x + float4(0.0, i1.x, i2.x, 1.0)); + + const float n_ = 0.142857142857; + float3 ns = float3(2.0 * n_, 0.5 * n_ - 1.0, n_); + float4 j = p - 49.0 * floor(p * ns.z * ns.z); + float4 x_ = floor(j * ns.z); + float4 y_ = floor(j - 7.0 * x_); + float4 x = x_ * ns.x + ns.yyyy; + float4 y = y_ * ns.x + ns.yyyy; + float4 h = 1.0 - abs(x) - abs(y); + + float4 b0 = float4(x.xy, y.xy); + float4 b1 = float4(x.zw, y.zw); + float4 s0 = floor(b0) * 2.0 + 1.0; + float4 s1 = floor(b1) * 2.0 + 1.0; + float4 sh = -step(h, 0.0); + float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + float4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + + float3 p0 = float3(a0.xy, h.x); + float3 p1 = float3(a0.zw, h.y); + float3 p2 = float3(a1.xy, h.z); + float3 p3 = float3(a1.zw, h.w); + float4 norm = 1.79284291400159 - 0.85373472095314 * float4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + float4 m = max(0.6 - float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, float4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); +} + +float cultmath_snoise(float2 value) { const float4 c = float4( 0.211324865405187, diff --git a/packages/cultmath/src/CultMath/Random.cs b/packages/cultmath/src/CultMath/Random.cs index 9931a87f..6892e20c 100644 --- a/packages/cultmath/src/CultMath/Random.cs +++ b/packages/cultmath/src/CultMath/Random.cs @@ -58,5 +58,9 @@ public struct Random return new float2(math.cos(angle), math.sin(angle)); } - public float3 NextFloat3(float3 min, float3 max) => min + new float3(NextFloat(), NextFloat(), NextFloat()) * (max - min); + public float3 NextFloat3() => new(NextFloat(), NextFloat(), NextFloat()); + + public float3 NextFloat3(float3 max) => NextFloat3() * max; + + public float3 NextFloat3(float3 min, float3 max) => min + NextFloat3() * (max - min); } diff --git a/packages/cultmath/src/CultMath/bool2.cs b/packages/cultmath/src/CultMath/bool2.cs index 95ab29eb..31fd10b0 100644 --- a/packages/cultmath/src/CultMath/bool2.cs +++ b/packages/cultmath/src/CultMath/bool2.cs @@ -2,17 +2,50 @@ using System; namespace CultMath; -public record struct bool2(bool x, bool y) +public struct bool2 : IEquatable { public static readonly bool2 @false = new(false, false); public static readonly bool2 @true = new(true, true); - public bool this[int index] => index switch + public bool x; + public bool y; + + public bool2(bool x, bool y) { - 0 => x, - 1 => y, - _ => throw new ArgumentOutOfRangeException(nameof(index)), - }; + this.x = x; + this.y = y; + } + + public bool this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } public static implicit operator bool2(bool value) => new(value, value); + + public static bool2 operator !(bool2 value) => new(!value.x, !value.y); + public static bool2 operator &(bool2 left, bool2 right) => new(left.x & right.x, left.y & right.y); + public static bool2 operator |(bool2 left, bool2 right) => new(left.x | right.x, left.y | right.y); + public static bool2 operator ^(bool2 left, bool2 right) => new(left.x ^ right.x, left.y ^ right.y); + public static bool2 operator ==(bool2 left, bool2 right) => new(left.x == right.x, left.y == right.y); + public static bool2 operator !=(bool2 left, bool2 right) => new(left.x != right.x, left.y != right.y); + + public readonly bool Equals(bool2 other) => x == other.x && y == other.y; + public override readonly bool Equals(object? obj) => obj is bool2 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y); + public override readonly string ToString() => $"bool2({x}, {y})"; } diff --git a/packages/cultmath/src/CultMath/bool3.cs b/packages/cultmath/src/CultMath/bool3.cs new file mode 100644 index 00000000..15c834ed --- /dev/null +++ b/packages/cultmath/src/CultMath/bool3.cs @@ -0,0 +1,55 @@ +using System; + +namespace CultMath; + +public struct bool3 : IEquatable +{ + public static readonly bool3 @false = new(false, false, false); + public static readonly bool3 @true = new(true, true, true); + + public bool x; + public bool y; + public bool z; + + public bool3(bool x, bool y, bool z) + { + this.x = x; + this.y = y; + this.z = z; + } + + public bool this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + 2 => z, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + case 2: z = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } + + public static implicit operator bool3(bool value) => new(value, value, value); + + public static bool3 operator !(bool3 value) => new(!value.x, !value.y, !value.z); + public static bool3 operator &(bool3 left, bool3 right) => new(left.x & right.x, left.y & right.y, left.z & right.z); + public static bool3 operator |(bool3 left, bool3 right) => new(left.x | right.x, left.y | right.y, left.z | right.z); + public static bool3 operator ^(bool3 left, bool3 right) => new(left.x ^ right.x, left.y ^ right.y, left.z ^ right.z); + public static bool3 operator ==(bool3 left, bool3 right) => new(left.x == right.x, left.y == right.y, left.z == right.z); + public static bool3 operator !=(bool3 left, bool3 right) => new(left.x != right.x, left.y != right.y, left.z != right.z); + + public readonly bool Equals(bool3 other) => x == other.x && y == other.y && z == other.z; + public override readonly bool Equals(object? obj) => obj is bool3 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y, z); + public override readonly string ToString() => $"bool3({x}, {y}, {z})"; +} diff --git a/packages/cultmath/src/CultMath/bool3.cs.meta b/packages/cultmath/src/CultMath/bool3.cs.meta new file mode 100644 index 00000000..ada81b6d --- /dev/null +++ b/packages/cultmath/src/CultMath/bool3.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 323ec670fc374c8ba4d87b4bf4c91f34 diff --git a/packages/cultmath/src/CultMath/bool4.cs b/packages/cultmath/src/CultMath/bool4.cs new file mode 100644 index 00000000..09300432 --- /dev/null +++ b/packages/cultmath/src/CultMath/bool4.cs @@ -0,0 +1,59 @@ +using System; + +namespace CultMath; + +public struct bool4 : IEquatable +{ + public static readonly bool4 @false = new(false, false, false, false); + public static readonly bool4 @true = new(true, true, true, true); + + public bool x; + public bool y; + public bool z; + public bool w; + + public bool4(bool x, bool y, bool z, bool w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + public bool this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + 2 => z, + 3 => w, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + case 2: z = value; break; + case 3: w = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } + + public static implicit operator bool4(bool value) => new(value, value, value, value); + + public static bool4 operator !(bool4 value) => new(!value.x, !value.y, !value.z, !value.w); + public static bool4 operator &(bool4 left, bool4 right) => new(left.x & right.x, left.y & right.y, left.z & right.z, left.w & right.w); + public static bool4 operator |(bool4 left, bool4 right) => new(left.x | right.x, left.y | right.y, left.z | right.z, left.w | right.w); + public static bool4 operator ^(bool4 left, bool4 right) => new(left.x ^ right.x, left.y ^ right.y, left.z ^ right.z, left.w ^ right.w); + public static bool4 operator ==(bool4 left, bool4 right) => new(left.x == right.x, left.y == right.y, left.z == right.z, left.w == right.w); + public static bool4 operator !=(bool4 left, bool4 right) => new(left.x != right.x, left.y != right.y, left.z != right.z, left.w != right.w); + + public readonly bool Equals(bool4 other) => x == other.x && y == other.y && z == other.z && w == other.w; + public override readonly bool Equals(object? obj) => obj is bool4 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y, z, w); + public override readonly string ToString() => $"bool4({x}, {y}, {z}, {w})"; +} diff --git a/packages/cultmath/src/CultMath/bool4.cs.meta b/packages/cultmath/src/CultMath/bool4.cs.meta new file mode 100644 index 00000000..625bb46b --- /dev/null +++ b/packages/cultmath/src/CultMath/bool4.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d38b9f70d88c459584441b85ecbfd92a diff --git a/packages/cultmath/src/CultMath/double2.cs b/packages/cultmath/src/CultMath/double2.cs index 000c3cb0..e61d84ea 100644 --- a/packages/cultmath/src/CultMath/double2.cs +++ b/packages/cultmath/src/CultMath/double2.cs @@ -2,17 +2,38 @@ using System; namespace CultMath; -public record struct double2(double x, double y) +public struct double2 : IEquatable { public static readonly double2 zero = new(0.0, 0.0); public static readonly double2 one = new(1.0, 1.0); - public double this[int index] => index switch + public double x; + public double y; + + public double2(double x, double y) { - 0 => x, - 1 => y, - _ => throw new ArgumentOutOfRangeException(nameof(index)), - }; + this.x = x; + this.y = y; + } + + public double this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } public static implicit operator double2(double value) => new(value, value); @@ -22,4 +43,16 @@ public record struct double2(double x, double y) public static double2 operator -(double2 left, double2 right) => new(left.x - right.x, left.y - right.y); public static double2 operator *(double2 left, double2 right) => new(left.x * right.x, left.y * right.y); public static double2 operator /(double2 left, double2 right) => new(left.x / right.x, left.y / right.y); + + public static bool2 operator <(double2 left, double2 right) => new(left.x < right.x, left.y < right.y); + public static bool2 operator >(double2 left, double2 right) => new(left.x > right.x, left.y > right.y); + public static bool2 operator <=(double2 left, double2 right) => new(left.x <= right.x, left.y <= right.y); + public static bool2 operator >=(double2 left, double2 right) => new(left.x >= right.x, left.y >= right.y); + public static bool2 operator ==(double2 left, double2 right) => new(left.x == right.x, left.y == right.y); + public static bool2 operator !=(double2 left, double2 right) => new(left.x != right.x, left.y != right.y); + + public readonly bool Equals(double2 other) => x.Equals(other.x) && y.Equals(other.y); + public override readonly bool Equals(object? obj) => obj is double2 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y); + public override readonly string ToString() => FormattableString.Invariant($"double2({x:R}, {y:R})"); } diff --git a/packages/cultmath/src/CultMath/double3.cs b/packages/cultmath/src/CultMath/double3.cs index b1427505..98c79327 100644 --- a/packages/cultmath/src/CultMath/double3.cs +++ b/packages/cultmath/src/CultMath/double3.cs @@ -2,14 +2,25 @@ using System; namespace CultMath; -public record struct double3(double x, double y, double z) +public struct double3 : IEquatable { public static readonly double3 zero = new(0.0, 0.0, 0.0); public static readonly double3 one = new(1.0, 1.0, 1.0); + public double x; + public double y; + public double z; + + public double3(double x, double y, double z) + { + this.x = x; + this.y = y; + this.z = z; + } + public double2 xy { - get => new(x, y); + readonly get => new(x, y); set { x = value.x; @@ -17,13 +28,26 @@ public record struct double3(double x, double y, double z) } } - public double this[int index] => index switch + public double this[int index] { - 0 => x, - 1 => y, - 2 => z, - _ => throw new ArgumentOutOfRangeException(nameof(index)), - }; + readonly get => index switch + { + 0 => x, + 1 => y, + 2 => z, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + case 2: z = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } public static implicit operator double3(double value) => new(value, value, value); @@ -33,4 +57,16 @@ public record struct double3(double x, double y, double z) public static double3 operator -(double3 left, double3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z); public static double3 operator *(double3 left, double3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z); public static double3 operator /(double3 left, double3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z); + + public static bool3 operator <(double3 left, double3 right) => new(left.x < right.x, left.y < right.y, left.z < right.z); + public static bool3 operator >(double3 left, double3 right) => new(left.x > right.x, left.y > right.y, left.z > right.z); + public static bool3 operator <=(double3 left, double3 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z); + public static bool3 operator >=(double3 left, double3 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z); + public static bool3 operator ==(double3 left, double3 right) => new(left.x == right.x, left.y == right.y, left.z == right.z); + public static bool3 operator !=(double3 left, double3 right) => new(left.x != right.x, left.y != right.y, left.z != right.z); + + public readonly bool Equals(double3 other) => x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z); + public override readonly bool Equals(object? obj) => obj is double3 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y, z); + public override readonly string ToString() => FormattableString.Invariant($"double3({x:R}, {y:R}, {z:R})"); } diff --git a/packages/cultmath/src/CultMath/float2.cs b/packages/cultmath/src/CultMath/float2.cs index 5298494b..f1fb5ccd 100644 --- a/packages/cultmath/src/CultMath/float2.cs +++ b/packages/cultmath/src/CultMath/float2.cs @@ -2,17 +2,48 @@ using System; namespace CultMath; -public record struct float2(float x, float y) +public struct float2 : IEquatable { public static readonly float2 zero = new(0.0f, 0.0f); public static readonly float2 one = new(1.0f, 1.0f); - public float this[int index] => index switch + public float x; + public float y; + + public float2(float x, float y) { - 0 => x, - 1 => y, - _ => throw new ArgumentOutOfRangeException(nameof(index)), - }; + this.x = x; + this.y = y; + } + + public float2 yx + { + readonly get => new(y, x); + set + { + y = value.x; + x = value.y; + } + } + + public float this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } public static implicit operator float2(float value) => new(value, value); public static explicit operator System.Numerics.Vector2(float2 value) => new(value.x, value.y); @@ -20,9 +51,44 @@ public record struct float2(float x, float y) public static float2 operator +(float2 value) => value; public static float2 operator -(float2 value) => new(-value.x, -value.y); + public static float2 operator +(float2 left, float2 right) => new(left.x + right.x, left.y + right.y); + public static float2 operator +(float2 left, float right) => new(left.x + right, left.y + right); + public static float2 operator +(float left, float2 right) => new(left + right.x, left + right.y); public static float2 operator -(float2 left, float2 right) => new(left.x - right.x, left.y - right.y); + public static float2 operator -(float2 left, float right) => new(left.x - right, left.y - right); + public static float2 operator -(float left, float2 right) => new(left - right.x, left - right.y); public static float2 operator *(float2 left, float2 right) => new(left.x * right.x, left.y * right.y); + public static float2 operator *(float2 left, float right) => new(left.x * right, left.y * right); + public static float2 operator *(float left, float2 right) => new(left * right.x, left * right.y); public static float2 operator /(float2 left, float2 right) => new(left.x / right.x, left.y / right.y); + public static float2 operator /(float2 left, float right) => new(left.x / right, left.y / right); + public static float2 operator /(float left, float2 right) => new(left / right.x, left / right.y); public static float2 operator %(float2 left, float2 right) => new(left.x % right.x, left.y % right.y); + public static float2 operator %(float2 left, float right) => new(left.x % right, left.y % right); + public static float2 operator %(float left, float2 right) => new(left % right.x, left % right.y); + + public static bool2 operator <(float2 left, float2 right) => new(left.x < right.x, left.y < right.y); + public static bool2 operator <(float2 left, float right) => new(left.x < right, left.y < right); + public static bool2 operator <(float left, float2 right) => new(left < right.x, left < right.y); + public static bool2 operator >(float2 left, float2 right) => new(left.x > right.x, left.y > right.y); + public static bool2 operator >(float2 left, float right) => new(left.x > right, left.y > right); + public static bool2 operator >(float left, float2 right) => new(left > right.x, left > right.y); + public static bool2 operator <=(float2 left, float2 right) => new(left.x <= right.x, left.y <= right.y); + public static bool2 operator <=(float2 left, float right) => new(left.x <= right, left.y <= right); + public static bool2 operator <=(float left, float2 right) => new(left <= right.x, left <= right.y); + public static bool2 operator >=(float2 left, float2 right) => new(left.x >= right.x, left.y >= right.y); + public static bool2 operator >=(float2 left, float right) => new(left.x >= right, left.y >= right); + public static bool2 operator >=(float left, float2 right) => new(left >= right.x, left >= right.y); + public static bool2 operator ==(float2 left, float2 right) => new(left.x == right.x, left.y == right.y); + public static bool2 operator ==(float2 left, float right) => new(left.x == right, left.y == right); + public static bool2 operator ==(float left, float2 right) => new(left == right.x, left == right.y); + public static bool2 operator !=(float2 left, float2 right) => new(left.x != right.x, left.y != right.y); + public static bool2 operator !=(float2 left, float right) => new(left.x != right, left.y != right); + public static bool2 operator !=(float left, float2 right) => new(left != right.x, left != right.y); + + public readonly bool Equals(float2 other) => x.Equals(other.x) && y.Equals(other.y); + public override readonly bool Equals(object? obj) => obj is float2 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y); + public override readonly string ToString() => FormattableString.Invariant($"float2({x:R}, {y:R})"); } diff --git a/packages/cultmath/src/CultMath/float2x2.cs b/packages/cultmath/src/CultMath/float2x2.cs new file mode 100644 index 00000000..60f59fb2 --- /dev/null +++ b/packages/cultmath/src/CultMath/float2x2.cs @@ -0,0 +1,69 @@ +using System; + +namespace CultMath; + +/// +/// HLSL float2x2: row-major. Constructors take rows, m[i] is row i, and +/// _mRC names row R, column C exactly as HLSL does. Use +/// for products; there is no * matrix product because HLSL's * is component-wise. +/// +public struct float2x2 : IEquatable +{ + public static readonly float2x2 identity = new(1.0f, 0.0f, 0.0f, 1.0f); + public static readonly float2x2 zero = new(0.0f, 0.0f, 0.0f, 0.0f); + + public float _m00; + public float _m01; + public float _m10; + public float _m11; + + public float2x2(float m00, float m01, float m10, float m11) + { + _m00 = m00; + _m01 = m01; + _m10 = m10; + _m11 = m11; + } + + public float2x2(float2 row0, float2 row1) + : this(row0.x, row0.y, row1.x, row1.y) + { + } + + public float2 this[int row] + { + readonly get => row switch + { + 0 => new float2(_m00, _m01), + 1 => new float2(_m10, _m11), + _ => throw new ArgumentOutOfRangeException(nameof(row)), + }; + set + { + switch (row) + { + case 0: _m00 = value.x; _m01 = value.y; break; + case 1: _m10 = value.x; _m11 = value.y; break; + default: throw new ArgumentOutOfRangeException(nameof(row)); + } + } + } + + /// + /// Counter-clockwise rotation for column vectors: mul(Rotate(a), v) equals + /// math.rotate(v, a); mul(v, Rotate(a)) rotates the other way. + /// + public static float2x2 Rotate(float radians) + { + var s = MathF.Sin(radians); + var c = MathF.Cos(radians); + return new float2x2(c, -s, s, c); + } + + public readonly bool Equals(float2x2 other) => + _m00.Equals(other._m00) && _m01.Equals(other._m01) && _m10.Equals(other._m10) && _m11.Equals(other._m11); + + public override readonly bool Equals(object? obj) => obj is float2x2 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(_m00, _m01, _m10, _m11); + public override readonly string ToString() => FormattableString.Invariant($"float2x2({_m00:R}, {_m01:R}, {_m10:R}, {_m11:R})"); +} diff --git a/packages/cultmath/src/CultMath/float2x2.cs.meta b/packages/cultmath/src/CultMath/float2x2.cs.meta new file mode 100644 index 00000000..56840df7 --- /dev/null +++ b/packages/cultmath/src/CultMath/float2x2.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c0dcec14c0b7432dbd8ba0bebb0a7c2f diff --git a/packages/cultmath/src/CultMath/float3.cs b/packages/cultmath/src/CultMath/float3.cs index 4a4efeba..b4dfa854 100644 --- a/packages/cultmath/src/CultMath/float3.cs +++ b/packages/cultmath/src/CultMath/float3.cs @@ -2,11 +2,22 @@ using System; namespace CultMath; -public record struct float3(float x, float y, float z) +public struct float3 : IEquatable { public static readonly float3 zero = new(0.0f, 0.0f, 0.0f); public static readonly float3 one = new(1.0f, 1.0f, 1.0f); + public float x; + public float y; + public float z; + + public float3(float x, float y, float z) + { + this.x = x; + this.y = y; + this.z = z; + } + public float3(float2 xy, float z) : this(xy.x, xy.y, z) { @@ -14,7 +25,7 @@ public record struct float3(float x, float y, float z) public float2 xy { - get => new(x, y); + readonly get => new(x, y); set { x = value.x; @@ -24,7 +35,7 @@ public record struct float3(float x, float y, float z) public float2 xz { - get => new(x, z); + readonly get => new(x, z); set { x = value.x; @@ -32,9 +43,19 @@ public record struct float3(float x, float y, float z) } } + public float2 yx + { + readonly get => new(y, x); + set + { + y = value.x; + x = value.y; + } + } + public float2 yz { - get => new(y, z); + readonly get => new(y, z); set { y = value.x; @@ -44,7 +65,7 @@ public record struct float3(float x, float y, float z) public float2 zy { - get => new(z, y); + readonly get => new(z, y); set { z = value.x; @@ -52,28 +73,115 @@ public record struct float3(float x, float y, float z) } } - public float3 yzx => new(y, z, x); - public float3 zyx => new(z, y, x); - - public float this[int index] => index switch + public float3 xyz { - 0 => x, - 1 => y, - 2 => z, - _ => throw new ArgumentOutOfRangeException(nameof(index)), - }; + readonly get => new(x, y, z); + set + { + x = value.x; + y = value.y; + z = value.z; + } + } + + public float3 xzy + { + readonly get => new(x, z, y); + set + { + x = value.x; + z = value.y; + y = value.z; + } + } + + public float3 yzx + { + readonly get => new(y, z, x); + set + { + y = value.x; + z = value.y; + x = value.z; + } + } + + public float3 zyx + { + readonly get => new(z, y, x); + set + { + z = value.x; + y = value.y; + x = value.z; + } + } + + public float this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + 2 => z, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + case 2: z = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } public static implicit operator float3(float value) => new(value, value, value); public static explicit operator System.Numerics.Vector3(float3 value) => new(value.x, value.y, value.z); public static explicit operator float3(System.Numerics.Vector3 value) => new(value.X, value.Y, value.Z); - public override readonly string ToString() => $"float3({x:R}, {y:R}, {z:R})"; - public static float3 operator +(float3 value) => value; public static float3 operator -(float3 value) => new(-value.x, -value.y, -value.z); + public static float3 operator +(float3 left, float3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z); + public static float3 operator +(float3 left, float right) => new(left.x + right, left.y + right, left.z + right); + public static float3 operator +(float left, float3 right) => new(left + right.x, left + right.y, left + right.z); public static float3 operator -(float3 left, float3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z); + public static float3 operator -(float3 left, float right) => new(left.x - right, left.y - right, left.z - right); + public static float3 operator -(float left, float3 right) => new(left - right.x, left - right.y, left - right.z); public static float3 operator *(float3 left, float3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z); + public static float3 operator *(float3 left, float right) => new(left.x * right, left.y * right, left.z * right); + public static float3 operator *(float left, float3 right) => new(left * right.x, left * right.y, left * right.z); public static float3 operator /(float3 left, float3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z); + public static float3 operator /(float3 left, float right) => new(left.x / right, left.y / right, left.z / right); + public static float3 operator /(float left, float3 right) => new(left / right.x, left / right.y, left / right.z); public static float3 operator %(float3 left, float3 right) => new(left.x % right.x, left.y % right.y, left.z % right.z); + public static float3 operator %(float3 left, float right) => new(left.x % right, left.y % right, left.z % right); + public static float3 operator %(float left, float3 right) => new(left % right.x, left % right.y, left % right.z); + + public static bool3 operator <(float3 left, float3 right) => new(left.x < right.x, left.y < right.y, left.z < right.z); + public static bool3 operator <(float3 left, float right) => new(left.x < right, left.y < right, left.z < right); + public static bool3 operator <(float left, float3 right) => new(left < right.x, left < right.y, left < right.z); + public static bool3 operator >(float3 left, float3 right) => new(left.x > right.x, left.y > right.y, left.z > right.z); + public static bool3 operator >(float3 left, float right) => new(left.x > right, left.y > right, left.z > right); + public static bool3 operator >(float left, float3 right) => new(left > right.x, left > right.y, left > right.z); + public static bool3 operator <=(float3 left, float3 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z); + public static bool3 operator <=(float3 left, float right) => new(left.x <= right, left.y <= right, left.z <= right); + public static bool3 operator <=(float left, float3 right) => new(left <= right.x, left <= right.y, left <= right.z); + public static bool3 operator >=(float3 left, float3 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z); + public static bool3 operator >=(float3 left, float right) => new(left.x >= right, left.y >= right, left.z >= right); + public static bool3 operator >=(float left, float3 right) => new(left >= right.x, left >= right.y, left >= right.z); + public static bool3 operator ==(float3 left, float3 right) => new(left.x == right.x, left.y == right.y, left.z == right.z); + public static bool3 operator ==(float3 left, float right) => new(left.x == right, left.y == right, left.z == right); + public static bool3 operator ==(float left, float3 right) => new(left == right.x, left == right.y, left == right.z); + public static bool3 operator !=(float3 left, float3 right) => new(left.x != right.x, left.y != right.y, left.z != right.z); + public static bool3 operator !=(float3 left, float right) => new(left.x != right, left.y != right, left.z != right); + public static bool3 operator !=(float left, float3 right) => new(left != right.x, left != right.y, left != right.z); + + public readonly bool Equals(float3 other) => x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z); + public override readonly bool Equals(object? obj) => obj is float3 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y, z); + public override readonly string ToString() => FormattableString.Invariant($"float3({x:R}, {y:R}, {z:R})"); } diff --git a/packages/cultmath/src/CultMath/float3x3.cs b/packages/cultmath/src/CultMath/float3x3.cs new file mode 100644 index 00000000..41e39936 --- /dev/null +++ b/packages/cultmath/src/CultMath/float3x3.cs @@ -0,0 +1,125 @@ +using System; + +namespace CultMath; + +/// +/// HLSL float3x3: row-major. Constructors take rows, m[i] is row i, and +/// _mRC names row R, column C exactly as HLSL does. Use +/// for products; there is no * matrix product because HLSL's * is component-wise. +/// +public struct float3x3 : IEquatable +{ + public static readonly float3x3 identity = new(1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f); + public static readonly float3x3 zero = new(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); + + public float _m00; + public float _m01; + public float _m02; + public float _m10; + public float _m11; + public float _m12; + public float _m20; + public float _m21; + public float _m22; + + public float3x3(float m00, float m01, float m02, float m10, float m11, float m12, float m20, float m21, float m22) + { + _m00 = m00; + _m01 = m01; + _m02 = m02; + _m10 = m10; + _m11 = m11; + _m12 = m12; + _m20 = m20; + _m21 = m21; + _m22 = m22; + } + + public float3x3(float3 row0, float3 row1, float3 row2) + : this(row0.x, row0.y, row0.z, row1.x, row1.y, row1.z, row2.x, row2.y, row2.z) + { + } + + public float3 this[int row] + { + readonly get => row switch + { + 0 => new float3(_m00, _m01, _m02), + 1 => new float3(_m10, _m11, _m12), + 2 => new float3(_m20, _m21, _m22), + _ => throw new ArgumentOutOfRangeException(nameof(row)), + }; + set + { + switch (row) + { + case 0: _m00 = value.x; _m01 = value.y; _m02 = value.z; break; + case 1: _m10 = value.x; _m11 = value.y; _m12 = value.z; break; + case 2: _m20 = value.x; _m21 = value.y; _m22 = value.z; break; + default: throw new ArgumentOutOfRangeException(nameof(row)); + } + } + } + + /// Rotation about +X for column vectors (mul(m, v)). + public static float3x3 RotateX(float radians) + { + var s = MathF.Sin(radians); + var c = MathF.Cos(radians); + return new float3x3(1.0f, 0.0f, 0.0f, 0.0f, c, -s, 0.0f, s, c); + } + + /// Rotation about +Y for column vectors (mul(m, v)). + public static float3x3 RotateY(float radians) + { + var s = MathF.Sin(radians); + var c = MathF.Cos(radians); + return new float3x3(c, 0.0f, s, 0.0f, 1.0f, 0.0f, -s, 0.0f, c); + } + + /// Rotation about +Z for column vectors (mul(m, v)). + public static float3x3 RotateZ(float radians) + { + var s = MathF.Sin(radians); + var c = MathF.Cos(radians); + return new float3x3(c, -s, 0.0f, s, c, 0.0f, 0.0f, 0.0f, 1.0f); + } + + /// + /// Extrinsic Euler rotation in radians. The order names the axis applied first: + /// rotates about Y, then X, then Z, so the + /// matrix is mul(RotateZ, mul(RotateX, RotateY)) for column vectors. + /// + public static float3x3 Euler(float3 xyz, math.RotationOrder order = math.RotationOrder.Default) + { + var x = RotateX(xyz.x); + var y = RotateY(xyz.y); + var z = RotateZ(xyz.z); + return order switch + { + math.RotationOrder.XYZ => math.mul(z, math.mul(y, x)), + math.RotationOrder.XZY => math.mul(y, math.mul(z, x)), + math.RotationOrder.YXZ => math.mul(z, math.mul(x, y)), + math.RotationOrder.YZX => math.mul(x, math.mul(z, y)), + math.RotationOrder.ZXY => math.mul(y, math.mul(x, z)), + math.RotationOrder.ZYX => math.mul(x, math.mul(y, z)), + _ => throw new ArgumentOutOfRangeException(nameof(order)), + }; + } + + public static float3x3 Euler(float x, float y, float z, math.RotationOrder order = math.RotationOrder.Default) => + Euler(new float3(x, y, z), order); + + public readonly bool Equals(float3x3 other) => + _m00.Equals(other._m00) && _m01.Equals(other._m01) && _m02.Equals(other._m02) && + _m10.Equals(other._m10) && _m11.Equals(other._m11) && _m12.Equals(other._m12) && + _m20.Equals(other._m20) && _m21.Equals(other._m21) && _m22.Equals(other._m22); + + public override readonly bool Equals(object? obj) => obj is float3x3 other && Equals(other); + + public override readonly int GetHashCode() => + HashCode.Combine(HashCode.Combine(_m00, _m01, _m02), HashCode.Combine(_m10, _m11, _m12), HashCode.Combine(_m20, _m21, _m22)); + + public override readonly string ToString() => + FormattableString.Invariant($"float3x3({_m00:R}, {_m01:R}, {_m02:R}, {_m10:R}, {_m11:R}, {_m12:R}, {_m20:R}, {_m21:R}, {_m22:R})"); +} diff --git a/packages/cultmath/src/CultMath/float3x3.cs.meta b/packages/cultmath/src/CultMath/float3x3.cs.meta new file mode 100644 index 00000000..99b25ed3 --- /dev/null +++ b/packages/cultmath/src/CultMath/float3x3.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ca36a9bfff144a9b51478a6d7b19c8e diff --git a/packages/cultmath/src/CultMath/float4.cs b/packages/cultmath/src/CultMath/float4.cs index b6eed097..d73175d8 100644 --- a/packages/cultmath/src/CultMath/float4.cs +++ b/packages/cultmath/src/CultMath/float4.cs @@ -2,11 +2,24 @@ using System; namespace CultMath; -public record struct float4(float x, float y, float z, float w) +public struct float4 : IEquatable { public static readonly float4 zero = new(0.0f, 0.0f, 0.0f, 0.0f); public static readonly float4 one = new(1.0f, 1.0f, 1.0f, 1.0f); + public float x; + public float y; + public float z; + public float w; + + public float4(float x, float y, float z, float w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + public float4(float2 xy, float z, float w) : this(xy.x, xy.y, z, w) { @@ -24,25 +37,69 @@ public record struct float4(float x, float y, float z, float w) public float2 xy { - get => new(x, y); + readonly get => new(x, y); set { x = value.x; y = value.y; } } - public float3 xyw => new(x, y, w); - public float3 yzx => new(y, z, x); - public float3 xyz => new(x, y, z); - public float this[int index] => index switch + public float3 xyz { - 0 => x, - 1 => y, - 2 => z, - 3 => w, - _ => throw new ArgumentOutOfRangeException(nameof(index)), - }; + readonly get => new(x, y, z); + set + { + x = value.x; + y = value.y; + z = value.z; + } + } + + public float3 xyw + { + readonly get => new(x, y, w); + set + { + x = value.x; + y = value.y; + w = value.z; + } + } + + public float3 yzx + { + readonly get => new(y, z, x); + set + { + y = value.x; + z = value.y; + x = value.z; + } + } + + public float this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + 2 => z, + 3 => w, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + case 2: z = value; break; + case 3: w = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } public static implicit operator float4(float value) => new(value, value, value, value); public static explicit operator System.Numerics.Vector4(float4 value) => new(value.x, value.y, value.z, value.w); @@ -50,9 +107,44 @@ public record struct float4(float x, float y, float z, float w) public static float4 operator +(float4 value) => value; public static float4 operator -(float4 value) => new(-value.x, -value.y, -value.z, -value.w); + public static float4 operator +(float4 left, float4 right) => new(left.x + right.x, left.y + right.y, left.z + right.z, left.w + right.w); + public static float4 operator +(float4 left, float right) => new(left.x + right, left.y + right, left.z + right, left.w + right); + public static float4 operator +(float left, float4 right) => new(left + right.x, left + right.y, left + right.z, left + right.w); public static float4 operator -(float4 left, float4 right) => new(left.x - right.x, left.y - right.y, left.z - right.z, left.w - right.w); + public static float4 operator -(float4 left, float right) => new(left.x - right, left.y - right, left.z - right, left.w - right); + public static float4 operator -(float left, float4 right) => new(left - right.x, left - right.y, left - right.z, left - right.w); public static float4 operator *(float4 left, float4 right) => new(left.x * right.x, left.y * right.y, left.z * right.z, left.w * right.w); + public static float4 operator *(float4 left, float right) => new(left.x * right, left.y * right, left.z * right, left.w * right); + public static float4 operator *(float left, float4 right) => new(left * right.x, left * right.y, left * right.z, left * right.w); public static float4 operator /(float4 left, float4 right) => new(left.x / right.x, left.y / right.y, left.z / right.z, left.w / right.w); + public static float4 operator /(float4 left, float right) => new(left.x / right, left.y / right, left.z / right, left.w / right); + public static float4 operator /(float left, float4 right) => new(left / right.x, left / right.y, left / right.z, left / right.w); public static float4 operator %(float4 left, float4 right) => new(left.x % right.x, left.y % right.y, left.z % right.z, left.w % right.w); + public static float4 operator %(float4 left, float right) => new(left.x % right, left.y % right, left.z % right, left.w % right); + public static float4 operator %(float left, float4 right) => new(left % right.x, left % right.y, left % right.z, left % right.w); + + public static bool4 operator <(float4 left, float4 right) => new(left.x < right.x, left.y < right.y, left.z < right.z, left.w < right.w); + public static bool4 operator <(float4 left, float right) => new(left.x < right, left.y < right, left.z < right, left.w < right); + public static bool4 operator <(float left, float4 right) => new(left < right.x, left < right.y, left < right.z, left < right.w); + public static bool4 operator >(float4 left, float4 right) => new(left.x > right.x, left.y > right.y, left.z > right.z, left.w > right.w); + public static bool4 operator >(float4 left, float right) => new(left.x > right, left.y > right, left.z > right, left.w > right); + public static bool4 operator >(float left, float4 right) => new(left > right.x, left > right.y, left > right.z, left > right.w); + public static bool4 operator <=(float4 left, float4 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z, left.w <= right.w); + public static bool4 operator <=(float4 left, float right) => new(left.x <= right, left.y <= right, left.z <= right, left.w <= right); + public static bool4 operator <=(float left, float4 right) => new(left <= right.x, left <= right.y, left <= right.z, left <= right.w); + public static bool4 operator >=(float4 left, float4 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z, left.w >= right.w); + public static bool4 operator >=(float4 left, float right) => new(left.x >= right, left.y >= right, left.z >= right, left.w >= right); + public static bool4 operator >=(float left, float4 right) => new(left >= right.x, left >= right.y, left >= right.z, left >= right.w); + public static bool4 operator ==(float4 left, float4 right) => new(left.x == right.x, left.y == right.y, left.z == right.z, left.w == right.w); + public static bool4 operator ==(float4 left, float right) => new(left.x == right, left.y == right, left.z == right, left.w == right); + public static bool4 operator ==(float left, float4 right) => new(left == right.x, left == right.y, left == right.z, left == right.w); + public static bool4 operator !=(float4 left, float4 right) => new(left.x != right.x, left.y != right.y, left.z != right.z, left.w != right.w); + public static bool4 operator !=(float4 left, float right) => new(left.x != right, left.y != right, left.z != right, left.w != right); + public static bool4 operator !=(float left, float4 right) => new(left != right.x, left != right.y, left != right.z, left != right.w); + + public readonly bool Equals(float4 other) => x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z) && w.Equals(other.w); + public override readonly bool Equals(object? obj) => obj is float4 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y, z, w); + public override readonly string ToString() => FormattableString.Invariant($"float4({x:R}, {y:R}, {z:R}, {w:R})"); } diff --git a/packages/cultmath/src/CultMath/int2.cs b/packages/cultmath/src/CultMath/int2.cs index 7c2f350b..06b4c4f6 100644 --- a/packages/cultmath/src/CultMath/int2.cs +++ b/packages/cultmath/src/CultMath/int2.cs @@ -2,17 +2,38 @@ using System; namespace CultMath; -public record struct int2(int x, int y) +public struct int2 : IEquatable { public static readonly int2 zero = new(0, 0); public static readonly int2 one = new(1, 1); - public int this[int index] => index switch + public int x; + public int y; + + public int2(int x, int y) { - 0 => x, - 1 => y, - _ => throw new ArgumentOutOfRangeException(nameof(index)), - }; + this.x = x; + this.y = y; + } + + public int this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } public static implicit operator int2(int value) => new(value, value); public static implicit operator float2(int2 value) => new(value.x, value.y); @@ -26,4 +47,16 @@ public record struct int2(int x, int y) public static int2 operator *(int2 left, int2 right) => new(left.x * right.x, left.y * right.y); public static int2 operator /(int2 left, int2 right) => new(left.x / right.x, left.y / right.y); public static int2 operator %(int2 left, int2 right) => new(left.x % right.x, left.y % right.y); + + public static bool2 operator <(int2 left, int2 right) => new(left.x < right.x, left.y < right.y); + public static bool2 operator >(int2 left, int2 right) => new(left.x > right.x, left.y > right.y); + public static bool2 operator <=(int2 left, int2 right) => new(left.x <= right.x, left.y <= right.y); + public static bool2 operator >=(int2 left, int2 right) => new(left.x >= right.x, left.y >= right.y); + public static bool2 operator ==(int2 left, int2 right) => new(left.x == right.x, left.y == right.y); + public static bool2 operator !=(int2 left, int2 right) => new(left.x != right.x, left.y != right.y); + + public readonly bool Equals(int2 other) => x == other.x && y == other.y; + public override readonly bool Equals(object? obj) => obj is int2 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y); + public override readonly string ToString() => FormattableString.Invariant($"int2({x}, {y})"); } diff --git a/packages/cultmath/src/CultMath/int3.cs b/packages/cultmath/src/CultMath/int3.cs new file mode 100644 index 00000000..60cbff29 --- /dev/null +++ b/packages/cultmath/src/CultMath/int3.cs @@ -0,0 +1,64 @@ +using System; + +namespace CultMath; + +public struct int3 : IEquatable +{ + public static readonly int3 zero = new(0, 0, 0); + public static readonly int3 one = new(1, 1, 1); + + public int x; + public int y; + public int z; + + public int3(int x, int y, int z) + { + this.x = x; + this.y = y; + this.z = z; + } + + public int this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + 2 => z, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + case 2: z = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } + + public static implicit operator int3(int value) => new(value, value, value); + public static implicit operator float3(int3 value) => new(value.x, value.y, value.z); + + public static int3 operator +(int3 value) => value; + public static int3 operator -(int3 value) => new(-value.x, -value.y, -value.z); + public static int3 operator +(int3 left, int3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z); + public static int3 operator -(int3 left, int3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z); + public static int3 operator *(int3 left, int3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z); + public static int3 operator /(int3 left, int3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z); + public static int3 operator %(int3 left, int3 right) => new(left.x % right.x, left.y % right.y, left.z % right.z); + + public static bool3 operator <(int3 left, int3 right) => new(left.x < right.x, left.y < right.y, left.z < right.z); + public static bool3 operator >(int3 left, int3 right) => new(left.x > right.x, left.y > right.y, left.z > right.z); + public static bool3 operator <=(int3 left, int3 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z); + public static bool3 operator >=(int3 left, int3 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z); + public static bool3 operator ==(int3 left, int3 right) => new(left.x == right.x, left.y == right.y, left.z == right.z); + public static bool3 operator !=(int3 left, int3 right) => new(left.x != right.x, left.y != right.y, left.z != right.z); + + public readonly bool Equals(int3 other) => x == other.x && y == other.y && z == other.z; + public override readonly bool Equals(object? obj) => obj is int3 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y, z); + public override readonly string ToString() => FormattableString.Invariant($"int3({x}, {y}, {z})"); +} diff --git a/packages/cultmath/src/CultMath/int3.cs.meta b/packages/cultmath/src/CultMath/int3.cs.meta new file mode 100644 index 00000000..d0400303 --- /dev/null +++ b/packages/cultmath/src/CultMath/int3.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 42ee98aace354db19b3ae87bdcbc1df9 diff --git a/packages/cultmath/src/CultMath/int4.cs b/packages/cultmath/src/CultMath/int4.cs new file mode 100644 index 00000000..4c301203 --- /dev/null +++ b/packages/cultmath/src/CultMath/int4.cs @@ -0,0 +1,68 @@ +using System; + +namespace CultMath; + +public struct int4 : IEquatable +{ + public static readonly int4 zero = new(0, 0, 0, 0); + public static readonly int4 one = new(1, 1, 1, 1); + + public int x; + public int y; + public int z; + public int w; + + public int4(int x, int y, int z, int w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + + public int this[int index] + { + readonly get => index switch + { + 0 => x, + 1 => y, + 2 => z, + 3 => w, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + set + { + switch (index) + { + case 0: x = value; break; + case 1: y = value; break; + case 2: z = value; break; + case 3: w = value; break; + default: throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } + + public static implicit operator int4(int value) => new(value, value, value, value); + public static implicit operator float4(int4 value) => new(value.x, value.y, value.z, value.w); + + public static int4 operator +(int4 value) => value; + public static int4 operator -(int4 value) => new(-value.x, -value.y, -value.z, -value.w); + public static int4 operator +(int4 left, int4 right) => new(left.x + right.x, left.y + right.y, left.z + right.z, left.w + right.w); + public static int4 operator -(int4 left, int4 right) => new(left.x - right.x, left.y - right.y, left.z - right.z, left.w - right.w); + public static int4 operator *(int4 left, int4 right) => new(left.x * right.x, left.y * right.y, left.z * right.z, left.w * right.w); + public static int4 operator /(int4 left, int4 right) => new(left.x / right.x, left.y / right.y, left.z / right.z, left.w / right.w); + public static int4 operator %(int4 left, int4 right) => new(left.x % right.x, left.y % right.y, left.z % right.z, left.w % right.w); + + public static bool4 operator <(int4 left, int4 right) => new(left.x < right.x, left.y < right.y, left.z < right.z, left.w < right.w); + public static bool4 operator >(int4 left, int4 right) => new(left.x > right.x, left.y > right.y, left.z > right.z, left.w > right.w); + public static bool4 operator <=(int4 left, int4 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z, left.w <= right.w); + public static bool4 operator >=(int4 left, int4 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z, left.w >= right.w); + public static bool4 operator ==(int4 left, int4 right) => new(left.x == right.x, left.y == right.y, left.z == right.z, left.w == right.w); + public static bool4 operator !=(int4 left, int4 right) => new(left.x != right.x, left.y != right.y, left.z != right.z, left.w != right.w); + + public readonly bool Equals(int4 other) => x == other.x && y == other.y && z == other.z && w == other.w; + public override readonly bool Equals(object? obj) => obj is int4 other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y, z, w); + public override readonly string ToString() => FormattableString.Invariant($"int4({x}, {y}, {z}, {w})"); +} diff --git a/packages/cultmath/src/CultMath/int4.cs.meta b/packages/cultmath/src/CultMath/int4.cs.meta new file mode 100644 index 00000000..48d0eaba --- /dev/null +++ b/packages/cultmath/src/CultMath/int4.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1a313709f76a430eb83901bb2d05c74e diff --git a/packages/cultmath/src/CultMath/math.cs b/packages/cultmath/src/CultMath/math.cs index d0a56e76..3bed477b 100644 --- a/packages/cultmath/src/CultMath/math.cs +++ b/packages/cultmath/src/CultMath/math.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace CultMath; @@ -8,6 +8,22 @@ public static class math public const float TAU = MathF.PI * 2.0f; public const float HALF_PI = MathF.PI * 0.5f; + /// + /// Extrinsic Euler order for : + /// the first letter is the axis applied first. Nested here so using static CultMath.math; + /// brings it into scope with the rest of the shader vocabulary. + /// + public enum RotationOrder : byte + { + XYZ, + XZY, + YXZ, + YZX, + ZXY, + ZYX, + Default = ZXY, + } + public static float2 float2(float x, float y) => new(x, y); public static float2 float2(float value) => new(value, value); public static bool2 bool2(bool x, bool y) => new(x, y); @@ -22,6 +38,23 @@ public static class math public static float4 float4(float3 xyz, float w) => new(xyz, w); public static float4 float4(float x, float3 yzw) => new(x, yzw); public static float4 float4(float value) => new(value, value, value, value); + public static bool3 bool3(bool x, bool y, bool z) => new(x, y, z); + public static bool3 bool3(bool value) => new(value, value, value); + public static bool4 bool4(bool x, bool y, bool z, bool w) => new(x, y, z, w); + public static bool4 bool4(bool value) => new(value, value, value, value); + public static int3 int3(int x, int y, int z) => new(x, y, z); + public static int3 int3(int value) => new(value, value, value); + public static int4 int4(int x, int y, int z, int w) => new(x, y, z, w); + public static int4 int4(int value) => new(value, value, value, value); + + // Matrix constructor functions take rows, as HLSL does. Under `using static CultMath.math;` + // these method groups hide the type names in member access, so static members need a + // qualified type: `CultMath.float3x3.Euler(...)`, `CultMath.float2x2.Rotate(...)`. + public static float2x2 float2x2(float m00, float m01, float m10, float m11) => new(m00, m01, m10, m11); + public static float2x2 float2x2(float2 row0, float2 row1) => new(row0, row1); + public static float3x3 float3x3(float m00, float m01, float m02, float m10, float m11, float m12, float m20, float m21, float m22) => + new(m00, m01, m02, m10, m11, m12, m20, m21, m22); + public static float3x3 float3x3(float3 row0, float3 row1, float3 row2) => new(row0, row1, row2); public static float radians(float degrees) => degrees * (PI / 180.0f); public static float degrees(float radians) => radians * (180.0f / PI); public static float sin(float value) => MathF.Sin(value); @@ -324,9 +357,149 @@ public static class math public static float hash(float2 value) => hash(dot(value, new float2(127.1f, 311.7f))); public static float hash(float3 value) => hash(dot(value, new float3(127.1f, 311.7f, 74.7f))); + public static float log(float value) => MathF.Log(value); + public static float2 log(float2 value) => new(log(value.x), log(value.y)); + public static float3 log(float3 value) => new(log(value.x), log(value.y), log(value.z)); + public static float4 log(float4 value) => new(log(value.x), log(value.y), log(value.z), log(value.w)); + + public static float2 pow(float2 value, float2 power) => new(pow(value.x, power.x), pow(value.y, power.y)); + public static float3 pow(float3 value, float3 power) => new(pow(value.x, power.x), pow(value.y, power.y), pow(value.z, power.z)); + public static float4 pow(float4 value, float4 power) => new(pow(value.x, power.x), pow(value.y, power.y), pow(value.z, power.z), pow(value.w, power.w)); + + public static float2 sqrt(float2 value) => new(sqrt(value.x), sqrt(value.y)); + public static float3 sqrt(float3 value) => new(sqrt(value.x), sqrt(value.y), sqrt(value.z)); + public static float4 sqrt(float4 value) => new(sqrt(value.x), sqrt(value.y), sqrt(value.z), sqrt(value.w)); + + public static float2 sin(float2 value) => new(sin(value.x), sin(value.y)); + public static float3 sin(float3 value) => new(sin(value.x), sin(value.y), sin(value.z)); + public static float4 sin(float4 value) => new(sin(value.x), sin(value.y), sin(value.z), sin(value.w)); + + public static float2 cos(float2 value) => new(cos(value.x), cos(value.y)); + public static float3 cos(float3 value) => new(cos(value.x), cos(value.y), cos(value.z)); + public static float4 cos(float4 value) => new(cos(value.x), cos(value.y), cos(value.z), cos(value.w)); + + public static float2 acos(float2 value) => new(acos(value.x), acos(value.y)); + public static float3 acos(float3 value) => new(acos(value.x), acos(value.y), acos(value.z)); + public static float4 acos(float4 value) => new(acos(value.x), acos(value.y), acos(value.z), acos(value.w)); + + public static float2 atan2(float2 y, float2 x) => new(atan2(y.x, x.x), atan2(y.y, x.y)); + public static float3 atan2(float3 y, float3 x) => new(atan2(y.x, x.x), atan2(y.y, x.y), atan2(y.z, x.z)); + public static float4 atan2(float4 y, float4 x) => new(atan2(y.x, x.x), atan2(y.y, x.y), atan2(y.z, x.z), atan2(y.w, x.w)); + + public static double min(double left, double right) => Math.Min(left, right); + public static double max(double left, double right) => Math.Max(left, right); + + public static bool any(bool2 value) => value.x || value.y; + public static bool any(bool3 value) => value.x || value.y || value.z; + public static bool any(bool4 value) => value.x || value.y || value.z || value.w; + public static bool all(bool2 value) => value.x && value.y; + public static bool all(bool3 value) => value.x && value.y && value.z; + public static bool all(bool4 value) => value.x && value.y && value.z && value.w; + + // HLSL 2021 argument order: select(condition, whenTrue, whenFalse). + // Unity.Mathematics' select(falseValue, trueValue, condition) is the reverse. + public static float select(bool condition, float whenTrue, float whenFalse) => condition ? whenTrue : whenFalse; + public static float2 select(bool2 condition, float2 whenTrue, float2 whenFalse) => + new(condition.x ? whenTrue.x : whenFalse.x, condition.y ? whenTrue.y : whenFalse.y); + public static float3 select(bool3 condition, float3 whenTrue, float3 whenFalse) => + new(condition.x ? whenTrue.x : whenFalse.x, condition.y ? whenTrue.y : whenFalse.y, condition.z ? whenTrue.z : whenFalse.z); + public static float4 select(bool4 condition, float4 whenTrue, float4 whenFalse) => + new(condition.x ? whenTrue.x : whenFalse.x, condition.y ? whenTrue.y : whenFalse.y, condition.z ? whenTrue.z : whenFalse.z, condition.w ? whenTrue.w : whenFalse.w); + + // HLSL mul: mul(m, v) treats v as a column vector, mul(v, m) as a row vector. + public static float mul(float left, float right) => left * right; + + public static float2 mul(float2x2 m, float2 v) => + new(m._m00 * v.x + m._m01 * v.y, m._m10 * v.x + m._m11 * v.y); + + public static float2 mul(float2 v, float2x2 m) => + new(v.x * m._m00 + v.y * m._m10, v.x * m._m01 + v.y * m._m11); + + public static float2x2 mul(float2x2 a, float2x2 b) => + new( + a._m00 * b._m00 + a._m01 * b._m10, a._m00 * b._m01 + a._m01 * b._m11, + a._m10 * b._m00 + a._m11 * b._m10, a._m10 * b._m01 + a._m11 * b._m11); + + public static float3 mul(float3x3 m, float3 v) => + new( + m._m00 * v.x + m._m01 * v.y + m._m02 * v.z, + m._m10 * v.x + m._m11 * v.y + m._m12 * v.z, + m._m20 * v.x + m._m21 * v.y + m._m22 * v.z); + + public static float3 mul(float3 v, float3x3 m) => + new( + v.x * m._m00 + v.y * m._m10 + v.z * m._m20, + v.x * m._m01 + v.y * m._m11 + v.z * m._m21, + v.x * m._m02 + v.y * m._m12 + v.z * m._m22); + + public static float3x3 mul(float3x3 a, float3x3 b) => + new( + a._m00 * b._m00 + a._m01 * b._m10 + a._m02 * b._m20, + a._m00 * b._m01 + a._m01 * b._m11 + a._m02 * b._m21, + a._m00 * b._m02 + a._m01 * b._m12 + a._m02 * b._m22, + a._m10 * b._m00 + a._m11 * b._m10 + a._m12 * b._m20, + a._m10 * b._m01 + a._m11 * b._m11 + a._m12 * b._m21, + a._m10 * b._m02 + a._m11 * b._m12 + a._m12 * b._m22, + a._m20 * b._m00 + a._m21 * b._m10 + a._m22 * b._m20, + a._m20 * b._m01 + a._m21 * b._m11 + a._m22 * b._m21, + a._m20 * b._m02 + a._m21 * b._m12 + a._m22 * b._m22); + + // Ashima Arts / Ian McEwan 3D simplex noise (MIT), mirrored by cultmath_snoise(float3) + // in shaders/CultMath.hlsl with the same float32 evaluation order. + public static float snoise(float3 value) + { + const float cx = 1.0f / 6.0f; + const float cy = 1.0f / 3.0f; + var i = floor(value + dot(value, new float3(cy, cy, cy))); + var x0 = value - i + dot(i, new float3(cx, cx, cx)); + var g = step(new float3(x0.y, x0.z, x0.x), x0); + var l = 1.0f - g; + var lzxy = new float3(l.z, l.x, l.y); + var i1 = min(g, lzxy); + var i2 = max(g, lzxy); + var x1 = x0 - i1 + cx; + var x2 = x0 - i2 + cy; + var x3 = x0 - 0.5f; + + i = snoise_mod289(i); + var p = snoise_permute(snoise_permute(snoise_permute( + i.z + new float4(0.0f, i1.z, i2.z, 1.0f)) + i.y + new float4(0.0f, i1.y, i2.y, 1.0f)) + i.x + new float4(0.0f, i1.x, i2.x, 1.0f)); + + const float n = 0.142857142857f; + var ns = new float3(2.0f * n, 0.5f * n - 1.0f, n); + var j = p - 49.0f * floor(p * ns.z * ns.z); + var xs = floor(j * ns.z); + var ys = floor(j - 7.0f * xs); + var x = xs * ns.x + ns.y; + var y = ys * ns.x + ns.y; + var h = 1.0f - abs(x) - abs(y); + + var b0 = new float4(x.x, x.y, y.x, y.y); + var b1 = new float4(x.z, x.w, y.z, y.w); + var s0 = floor(b0) * 2.0f + 1.0f; + var s1 = floor(b1) * 2.0f + 1.0f; + var sh = -step(h, 0.0f); + var a0 = new float4(b0.x, b0.z, b0.y, b0.w) + new float4(s0.x, s0.z, s0.y, s0.w) * new float4(sh.x, sh.x, sh.y, sh.y); + var a1 = new float4(b1.x, b1.z, b1.y, b1.w) + new float4(s1.x, s1.z, s1.y, s1.w) * new float4(sh.z, sh.z, sh.w, sh.w); + + var p0 = new float3(a0.x, a0.y, h.x); + var p1 = new float3(a0.z, a0.w, h.y); + var p2 = new float3(a1.x, a1.y, h.z); + var p3 = new float3(a1.z, a1.w, h.w); + var norm = 1.79284291400159f - 0.85373472095314f * new float4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + var m = max(0.6f - new float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0f); + m = m * m; + return 42.0f * dot(m * m, new float4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); + } + // Ashima Arts / Ian McEwan 2D simplex noise, kept component-explicit so // the C# and HLSL mirrors preserve the same float32 evaluation order. - public static float simplex_noise(float2 value) + public static float snoise(float2 value) { var c = new float4( 0.211324865405187f, @@ -342,8 +515,8 @@ public static class math x0.x + c.z, x0.y + c.z); - i = simplex_mod289(i); - var p = simplex_permute(simplex_permute(i.y + new float3(0.0f, i1.y, 1.0f)) + i.x + new float3(0.0f, i1.x, 1.0f)); + i = snoise_mod289(i); + var p = snoise_permute(snoise_permute(i.y + new float3(0.0f, i1.y, 1.0f)) + i.x + new float3(0.0f, i1.x, 1.0f)); var m = max(0.5f - new float3( dot(x0, x0), x12.x * x12.x + x12.y * x12.y, @@ -364,9 +537,11 @@ public static class math return 130.0f * dot(m, g); } - private static float2 simplex_mod289(float2 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f; - private static float3 simplex_mod289(float3 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f; - private static float3 simplex_permute(float3 value) => simplex_mod289(((value * 34.0f) + 1.0f) * value); + private static float2 snoise_mod289(float2 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f; + private static float3 snoise_mod289(float3 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f; + private static float4 snoise_mod289(float4 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f; + private static float3 snoise_permute(float3 value) => snoise_mod289(((value * 34.0f) + 1.0f) * value); + private static float4 snoise_permute(float4 value) => snoise_mod289(((value * 34.0f) + 1.0f) * value); public static float value_noise(float2 position) { diff --git a/packages/cultmath/src/CultMath/quaternion.cs b/packages/cultmath/src/CultMath/quaternion.cs index fe8dda8e..e8c4a48c 100644 --- a/packages/cultmath/src/CultMath/quaternion.cs +++ b/packages/cultmath/src/CultMath/quaternion.cs @@ -1,9 +1,28 @@ +using System; + namespace CultMath; -public record struct quaternion(float x, float y, float z, float w) +/// +/// Not an HLSL type: shaders carry rotations as float4. Equality therefore stays +/// scalar (== returns bool) instead of following the component-wise vector rule. +/// +public struct quaternion : IEquatable { public static readonly quaternion identity = new(0.0f, 0.0f, 0.0f, 1.0f); + public float x; + public float y; + public float z; + public float w; + + public quaternion(float x, float y, float z, float w) + { + this.x = x; + this.y = y; + this.z = z; + this.w = w; + } + public static quaternion LookRotation(float3 forward, float3 up) { forward = math.normalize(forward); @@ -65,4 +84,12 @@ public record struct quaternion(float x, float y, float z, float w) public static implicit operator quaternion(float4 value) => new(value.x, value.y, value.z, value.w); public static implicit operator float4(quaternion value) => new(value.x, value.y, value.z, value.w); + + public static bool operator ==(quaternion left, quaternion right) => left.Equals(right); + public static bool operator !=(quaternion left, quaternion right) => !left.Equals(right); + + public readonly bool Equals(quaternion other) => x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z) && w.Equals(other.w); + public override readonly bool Equals(object? obj) => obj is quaternion other && Equals(other); + public override readonly int GetHashCode() => HashCode.Combine(x, y, z, w); + public override readonly string ToString() => FormattableString.Invariant($"quaternion({x:R}, {y:R}, {z:R}, {w:R})"); } diff --git a/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs index 0034a283..445ad255 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs @@ -42,7 +42,8 @@ public sealed class HlslMirrorTests "cultmath_cubic_bezier", "cultmath_hash", "cultmath_value_noise", - "cultmath_simplex_noise", + "cultmath_snoise(float2", + "cultmath_snoise(float3", "cultmath_value_noise_bicubic", "cultmath_value_noise_texture", "cultmath_value_noise_texture_bicubic", diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs new file mode 100644 index 00000000..bdb41cbf --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs @@ -0,0 +1,332 @@ +using System.Text.Json; +using Xunit; + +namespace CultMath.Tests; + +public sealed class HlslSemanticsTests +{ + [Fact] + public void ComponentsAndIndexersAreWritable() + { + var value = new float4(1.0f, 2.0f, 3.0f, 4.0f); + value.w = 9.0f; + value[0] = -1.0f; + + Assert.Equal(new float4(-1.0f, 2.0f, 3.0f, 9.0f), value); + + var cell = new int3(1, 2, 3); + cell[2] = 7; + Assert.Equal(new int3(1, 2, 7), cell); + + ref var component = ref value.y; + component = 20.0f; + Assert.Equal(20.0f, value.y); + } + + [Fact] + public void Float2SwizzlesReadAndWrite() + { + var value = new float2(1.0f, 2.0f); + Assert.Equal(new float2(2.0f, 1.0f), value.yx); + + value.yx = new float2(5.0f, 6.0f); + Assert.Equal(new float2(6.0f, 5.0f), value); + } + + [Fact] + public void Float3SwizzlesReadAndWrite() + { + var source = new float3(1.0f, 2.0f, 3.0f); + Assert.Equal(new float2(1.0f, 2.0f), source.xy); + Assert.Equal(new float2(1.0f, 3.0f), source.xz); + Assert.Equal(new float2(2.0f, 1.0f), source.yx); + Assert.Equal(new float2(2.0f, 3.0f), source.yz); + Assert.Equal(new float2(3.0f, 2.0f), source.zy); + Assert.Equal(new float3(1.0f, 2.0f, 3.0f), source.xyz); + Assert.Equal(new float3(1.0f, 3.0f, 2.0f), source.xzy); + Assert.Equal(new float3(2.0f, 3.0f, 1.0f), source.yzx); + Assert.Equal(new float3(3.0f, 2.0f, 1.0f), source.zyx); + + Assert.Equal(new float3(8.0f, 9.0f, 3.0f), Write(source, (ref float3 v) => v.xy = new float2(8.0f, 9.0f))); + Assert.Equal(new float3(8.0f, 2.0f, 9.0f), Write(source, (ref float3 v) => v.xz = new float2(8.0f, 9.0f))); + Assert.Equal(new float3(9.0f, 8.0f, 3.0f), Write(source, (ref float3 v) => v.yx = new float2(8.0f, 9.0f))); + Assert.Equal(new float3(1.0f, 8.0f, 9.0f), Write(source, (ref float3 v) => v.yz = new float2(8.0f, 9.0f))); + Assert.Equal(new float3(1.0f, 9.0f, 8.0f), Write(source, (ref float3 v) => v.zy = new float2(8.0f, 9.0f))); + Assert.Equal(new float3(7.0f, 8.0f, 9.0f), Write(source, (ref float3 v) => v.xyz = new float3(7.0f, 8.0f, 9.0f))); + Assert.Equal(new float3(7.0f, 9.0f, 8.0f), Write(source, (ref float3 v) => v.xzy = new float3(7.0f, 8.0f, 9.0f))); + Assert.Equal(new float3(9.0f, 7.0f, 8.0f), Write(source, (ref float3 v) => v.yzx = new float3(7.0f, 8.0f, 9.0f))); + Assert.Equal(new float3(9.0f, 8.0f, 7.0f), Write(source, (ref float3 v) => v.zyx = new float3(7.0f, 8.0f, 9.0f))); + } + + [Fact] + public void Float4SwizzlesReadAndWrite() + { + var source = new float4(1.0f, 2.0f, 3.0f, 4.0f); + Assert.Equal(new float2(1.0f, 2.0f), source.xy); + Assert.Equal(new float3(1.0f, 2.0f, 3.0f), source.xyz); + Assert.Equal(new float3(1.0f, 2.0f, 4.0f), source.xyw); + Assert.Equal(new float3(2.0f, 3.0f, 1.0f), source.yzx); + + var a = source; + a.xy = new float2(8.0f, 9.0f); + Assert.Equal(new float4(8.0f, 9.0f, 3.0f, 4.0f), a); + + var b = source; + b.xyz = new float3(7.0f, 8.0f, 9.0f); + Assert.Equal(new float4(7.0f, 8.0f, 9.0f, 4.0f), b); + + var c = source; + c.xyw = new float3(7.0f, 8.0f, 9.0f); + Assert.Equal(new float4(7.0f, 8.0f, 3.0f, 9.0f), c); + + var d = source; + d.yzx = new float3(7.0f, 8.0f, 9.0f); + Assert.Equal(new float4(9.0f, 7.0f, 8.0f, 4.0f), d); + } + + [Fact] + public void ComparisonOperatorsReturnComponentWiseBoolVectors() + { + var a = new float3(1.0f, 2.0f, 3.0f); + var b = new float3(3.0f, 2.0f, 1.0f); + + Assert.Equal(new bool3(true, false, false), a < b); + Assert.Equal(new bool3(false, false, true), a > b); + Assert.Equal(new bool3(true, true, false), a <= b); + Assert.Equal(new bool3(false, true, true), a >= b); + Assert.Equal(new bool3(false, true, false), a == b); + Assert.Equal(new bool3(true, false, true), a != b); + + Assert.Equal(new bool3(true, false, false), a < 2.0f); + Assert.Equal(new bool3(false, false, true), 2.0f < a); + Assert.Equal(new bool2(true, false), new float2(0.0f, 1.0f) == 0.0f); + Assert.Equal(new bool4(false, true, true, true), new float4(0.0f, 1.0f, 2.0f, 3.0f) >= 1.0f); + Assert.Equal(new bool2(false, true), new int2(1, 2) > new int2(1, 1)); + Assert.Equal(new bool3(true, false, true), new int3(1, 2, 3) != new int3(0, 2, 4)); + Assert.Equal(new bool4(true, true, false, false), new int4(1, 2, 3, 4) <= new int4(1, 2, 2, 2)); + Assert.Equal(new bool2(true, false), new double2(0.5, 1.0) < new double2(1.0, 1.0)); + Assert.Equal(new bool3(false, false, true), new double3(1.0, 1.0, 2.0) > new double3(1.0, 1.0, 1.0)); + } + + [Fact] + public void ScalarOperatorsApplyPerComponentOnBothSides() + { + var value = new float3(1.0f, 2.0f, 4.0f); + + Assert.Equal(new float3(3.0f, 2.0f, 0.0f), 4.0f - value); + Assert.Equal(new float3(0.0f, 1.0f, 3.0f), value - 1.0f); + Assert.Equal(new float3(2.0f, 3.0f, 5.0f), 1.0f + value); + Assert.Equal(new float3(4.0f, 2.0f, 1.0f), 4.0f / value); + Assert.Equal(new float3(0.5f, 1.0f, 2.0f), value / 2.0f); + Assert.Equal(new float3(2.0f, 4.0f, 8.0f), 2.0f * value); + Assert.Equal(new float3(1.0f, 0.0f, 0.0f), value % 2.0f); + Assert.Equal(new float2(1.5f, 2.5f), new float2(1.0f, 2.0f) + 0.5f); + Assert.Equal(new float4(0.0f, 1.0f, 2.0f, 3.0f), new float4(1.0f, 2.0f, 3.0f, 4.0f) - 1.0f); + } + + [Fact] + public void EqualsAndHashCodeStayValueBasedForDictionaryKeys() + { + var map = new Dictionary { [new float3(1.0f, 2.0f, 3.0f)] = "hit" }; + Assert.Equal("hit", map[new float3(1.0f, 2.0f, 3.0f)]); + Assert.False(map.ContainsKey(new float3(1.0f, 2.0f, 3.5f))); + + var cells = new HashSet { new int2(3, 4) }; + Assert.Contains(new int2(3, 4), cells); + Assert.Contains(new int3(1, 2, 3), new HashSet { new int3(1, 2, 3) }); + + var nan = new float2(float.NaN, 0.0f); + var sameNan = nan; + Assert.True(nan.Equals(sameNan)); + Assert.False((nan == sameNan).x); + + Assert.True(new quaternion(0.0f, 0.0f, 0.0f, 1.0f) == quaternion.identity); + Assert.Equal(float2x2.identity, math.float2x2(1.0f, 0.0f, 0.0f, 1.0f)); + Assert.Equal("float3(1, 2.5, -3)", new float3(1.0f, 2.5f, -3.0f).ToString()); + } + + [Fact] + public void AnyAllAndSelectFollowHlsl() + { + Assert.True(math.any(new bool2(false, true))); + Assert.False(math.any(bool3.@false)); + Assert.True(math.all(bool4.@true)); + Assert.False(math.all(new bool3(true, false, true))); + + var mask = new bool3(true, false, true); + Assert.Equal(new float3(1.0f, 20.0f, 3.0f), math.select(mask, new float3(1.0f, 2.0f, 3.0f), new float3(10.0f, 20.0f, 30.0f))); + Assert.Equal(7.0f, math.select(true, 7.0f, 8.0f)); + Assert.Equal(new float2(0.0f, 5.0f), math.select(new bool2(false, true), math.float2(5.0f), math.float2(0.0f))); + Assert.Equal(new float4(1.0f, 0.0f, 1.0f, 0.0f), math.select(new bool4(true, false, true, false), 1.0f, 0.0f)); + + Assert.Equal(new bool2(false, true), !new bool2(true, false)); + Assert.Equal(new bool3(true, false, false), new bool3(true, true, false) & new bool3(true, false, true)); + Assert.Equal(new bool3(true, true, true), new bool3(true, true, false) | new bool3(true, false, true)); + } + + [Fact] + public void AddedIntrinsicsAreComponentWise() + { + Assert.Equal(MathF.Log(3.0f), math.log(3.0f)); + Assert.Equal(new float3(0.0f, MathF.Log(2.0f), MathF.Log(4.0f)), math.log(new float3(1.0f, 2.0f, 4.0f))); + Assert.Equal(new float2(8.0f, 9.0f), math.pow(new float2(2.0f, 3.0f), new float2(3.0f, 2.0f))); + Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), math.sqrt(new float4(1.0f, 4.0f, 9.0f, 16.0f))); + var sines = math.sin(new float3(0.0f, math.HALF_PI, math.PI)); + Assert.Equal(0.0f, sines.x, precision: 5); + Assert.Equal(1.0f, sines.y, precision: 5); + Assert.Equal(0.0f, sines.z, precision: 5); + var cosines = math.cos(new float2(0.0f, math.PI)); + Assert.Equal(1.0f, cosines.x, precision: 5); + Assert.Equal(-1.0f, cosines.y, precision: 5); + var arccosines = math.acos(new float3(1.0f, 0.0f, -1.0f)); + Assert.Equal(0.0f, arccosines.x, precision: 5); + Assert.Equal(math.HALF_PI, arccosines.y, precision: 5); + Assert.Equal(math.PI, arccosines.z, precision: 5); + Assert.Equal(math.atan2(1.0f, -1.0f), math.atan2(new float2(1.0f, 0.0f), new float2(-1.0f, 1.0f)).x); + Assert.Equal(0.0f, math.atan2(new float4(1.0f, 0.0f, 1.0f, 0.0f), new float4(1.0f, 1.0f, 1.0f, 1.0f)).y); + Assert.Equal(2.5, math.min(2.5, 3.5)); + Assert.Equal(3.5, math.max(2.5, 3.5)); + } + + [Fact] + public void IntVectorsPromoteToFloatVectors() + { + float3 promoted3 = new int3(1, 2, 3); + float4 promoted4 = new int4(1, 2, 3, 4); + + Assert.Equal(new float3(1.0f, 2.0f, 3.0f), promoted3); + Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), promoted4); + Assert.Equal(new int3(2, 4, 6), new int3(1, 2, 3) * 2); + Assert.Equal(new int4(1, 0, 1, 0), new int4(3, 4, 5, 6) % 2); + Assert.Equal(new int3(1, 2, 3), math.int3(1, 2, 3)); + Assert.Equal(new int4(5, 5, 5, 5), math.int4(5)); + } + + [Fact] + public void MatrixMulFollowsHlslRowMajorConvention() + { + var m = math.float2x2(1.0f, 2.0f, 3.0f, 4.0f); + var v = new float2(5.0f, 6.0f); + + Assert.Equal(new float2(1.0f, 2.0f), m[0]); + Assert.Equal(new float2(17.0f, 39.0f), math.mul(m, v)); + Assert.Equal(new float2(23.0f, 34.0f), math.mul(v, m)); + Assert.Equal(math.float2x2(7.0f, 10.0f, 15.0f, 22.0f), math.mul(m, m)); + + var m3 = math.float3x3(new float3(1.0f, 2.0f, 3.0f), new float3(4.0f, 5.0f, 6.0f), new float3(7.0f, 8.0f, 9.0f)); + var v3 = new float3(1.0f, 0.0f, -1.0f); + Assert.Equal(new float3(-2.0f, -2.0f, -2.0f), math.mul(m3, v3)); + Assert.Equal(new float3(-6.0f, -6.0f, -6.0f), math.mul(v3, m3)); + Assert.Equal(math.float3x3(30.0f, 36.0f, 42.0f, 66.0f, 81.0f, 96.0f, 102.0f, 126.0f, 150.0f), math.mul(m3, m3)); + Assert.Equal(m3, math.mul(m3, float3x3.identity)); + + m3[1] = new float3(0.0f, 1.0f, 0.0f); + Assert.Equal(1.0f, m3._m11); + Assert.Equal(12.0f, math.mul(3.0f, 4.0f)); + } + + [Fact] + public void Float2x2RotateMatchesRotateAndUnityMathematics() + { + var rotation = float2x2.Rotate(0.5f); + var column = math.mul(rotation, new float2(1.0f, 0.0f)); + var row = math.mul(new float2(1.0f, 0.0f), rotation); + var reference = math.rotate(new float2(1.0f, 0.0f), 0.5f); + + Assert.Equal(reference.x, column.x, precision: 6); + Assert.Equal(reference.y, column.y, precision: 6); + + // Unity.Mathematics 1.x reference values: mul(float2(1, 0), float2x2.Rotate(0.5f)). + Assert.Equal(0.87758255f, row.x, precision: 6); + Assert.Equal(-0.47942555f, row.y, precision: 6); + } + + [Fact] + public void Float3x3EulerMatchesUnityMathematicsReference() + { + // Unity.Mathematics 1.x reference values captured from float3x3.Euler. + AssertRows( + float3x3.Euler(new float3(0.4f, -0.7f, 1.3f), math.RotationOrder.YXZ), + 0.44632244f, -0.8874959f, 0.114662126f, + 0.6698625f, 0.24638279f, -0.700414f, + 0.59336376f, 0.38941833f, 0.70446634f); + AssertRows( + float3x3.Euler(new float3(0.4f, -0.7f, 1.3f)), + -0.037133574f, -0.8040775f, -0.59336376f, + 0.8874959f, 0.24638279f, -0.38941833f, + 0.45931715f, -0.5410684f, 0.70446634f); + + // Aetheria ActionGameManager view direction: mul(float3(0, 0, 1), Euler(float3(yawPitch.yx, 0), YXZ)). + var yawPitch = new float2(0.3f, -1.1f); + var view = math.mul(new float3(0.0f, 0.0f, 1.0f), float3x3.Euler(new float3(yawPitch.yx, 0.0f), math.RotationOrder.YXZ)); + Assert.Equal(-0.13404681f, view.x, precision: 5); + Assert.Equal(-0.8912074f, view.y, precision: 5); + Assert.Equal(0.4333369f, view.z, precision: 5); + + var composed = math.mul(float3x3.RotateZ(0.2f), math.mul(float3x3.RotateY(0.3f), float3x3.RotateX(0.1f))); + AssertRows(float3x3.Euler(0.1f, 0.3f, 0.2f, math.RotationOrder.XYZ), + composed._m00, composed._m01, composed._m02, + composed._m10, composed._m11, composed._m12, + composed._m20, composed._m21, composed._m22); + } + + [Theory] + [InlineData(0.0f, 0.0f, 0.0f, -0.41219875f)] + [InlineData(0.25f, -0.5f, 1.75f, 0.5250269f)] + [InlineData(12.25f, -4.5f, 3.125f, 0.008141451f)] + [InlineData(-7.3f, 2.9f, 101.4f, 0.578015f)] + public void Snoise3MatchesUnityMathematicsReference(float x, float y, float z, float expected) + { + Assert.Equal(expected, math.snoise(new float3(x, y, z)), precision: 5); + } + + [Fact] + public void RandomFloat3OverloadsShareOneDrawOrder() + { + var a = new Random(99); + var b = new Random(99); + var c = new Random(99); + + var unit = a.NextFloat3(); + Assert.Equal(new float3(b.NextFloat(), b.NextFloat(), b.NextFloat()), unit); + Assert.Equal(unit * new float3(2.0f, 4.0f, 8.0f), c.NextFloat3(new float3(2.0f, 4.0f, 8.0f))); + + var d = new Random(99); + Assert.Equal(math.float3(-1.0f) + unit * 2.0f, d.NextFloat3(math.float3(-1.0f), math.float3(1.0f))); + } + + [Fact] + public void CoreAssemblyStaysEngineFree() + { + var references = typeof(math).Assembly.GetReferencedAssemblies(); + Assert.DoesNotContain(references, reference => reference.Name!.StartsWith("UnityEngine", StringComparison.Ordinal)); + + var root = new DirectoryInfo(AppContext.BaseDirectory); + while (root is not null && !File.Exists(Path.Combine(root.FullName, "packages", "cultmath", "src", "CultMath", "CultMath.asmdef"))) + { + root = root.Parent; + } + + Assert.NotNull(root); + var asmdefPath = Path.Combine(root!.FullName, "packages", "cultmath", "src", "CultMath", "CultMath.asmdef"); + using var asmdef = JsonDocument.Parse(File.ReadAllText(asmdefPath)); + Assert.True(asmdef.RootElement.GetProperty("noEngineReferences").GetBoolean()); + } + + private static float3 Write(float3 value, WriteAction action) + { + action(ref value); + return value; + } + + private delegate void WriteAction(ref float3 value); + + private static void AssertRows(float3x3 m, params float[] expected) + { + var actual = new[] { m._m00, m._m01, m._m02, m._m10, m._m11, m._m12, m._m20, m._m21, m._m22 }; + for (var index = 0; index < expected.Length; index++) + { + Assert.Equal(expected[index], actual[index], precision: 5); + } + } +} diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs.meta b/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs.meta new file mode 100644 index 00000000..8158a66d --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7164d00a023e43578a31897347d8f106 diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs new file mode 100644 index 00000000..575952c3 --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -0,0 +1,85 @@ +using Xunit; +using static CultMath.math; + +namespace CultMath.Tests; + +/// +/// HLSL written against CultMath compiles as C# under using static CultMath.math;. +/// The only spelling C# forces is float literal suffixes (0.5f): an unsuffixed +/// literal is a double in C#. +/// +public sealed class HlslSourceCompatibilityTests +{ + // ---- HLSL-shaped source: keep this block free of C#-only constructs. ---- + static float3 shade(float3 position, float3 normal, float2 wind, float time) + { + float3 p = position; + p.xz = p.xz + wind * time; + p.y = saturate(p.y); + + float3x3 basis = float3x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1)); + float3 n = normalize(mul(basis, normal)); + float ndotl = saturate(dot(n, normalize(float3(0.3f, 1, 0.2f)))); + float3 color = lerp(float3(0.1f, 0.1f, 0.2f), float3(1, 0.9f, 0.7f), ndotl); + + if (any(p < 0.0f)) + color *= 0.5f; + if (all(abs(n) <= 1.0f)) + color.yzx = color.yzx * float3(1, 1, 1); + + color = select(p > 10.0f, float3(0), color); + float2 uv = frac(p.xz * 0.25f); + color.xy = lerp(color.xy, uv, 0.1f); + return color; + } + + static float2 steer(float2 direction, float torque) + { + float2x2 turn = float2x2(cos(torque), -sin(torque), sin(torque), cos(torque)); + float2 turned = mul(direction, turn); + return normalize(turned) * pow(length(turned), 1.0f); + } + + static float fbm(float3 p, int octaves) + { + float sum = 0.0f; + float amp = 0.5f; + float freq = 1.0f; + for (int i = 0; i < octaves; i++) + { + sum += abs(snoise(p * freq)) * amp; + freq *= 2.0f; + amp *= 0.5f; + } + return sum; + } + // ---- end HLSL-shaped source ---- + + [Fact] + public void ShaderStyleMathCompilesAndEvaluates() + { + var color = shade(new float3(1.0f, 0.5f, 2.0f), new float3(0.0f, 1.0f, 0.0f), new float2(1.0f, 0.0f), 0.0f); + + Assert.True(color.x > 0.1f && color.x <= 1.0f); + Assert.True(float.IsFinite(color.y) && float.IsFinite(color.z)); + + var far = shade(new float3(20.0f, 20.0f, 20.0f), new float3(0.0f, 1.0f, 0.0f), float2(0.0f), 0.0f); + Assert.Equal(0.0f, far.z); + } + + [Fact] + public void AetheriaStyleRotationAndNoiseCompile() + { + var steered = steer(new float2(1.0f, 0.0f), 0.5f); + var viaRotate = mul(new float2(1.0f, 0.0f), CultMath.float2x2.Rotate(0.5f)); + + Assert.Equal(viaRotate.x, steered.x, precision: 5); + Assert.Equal(viaRotate.y, steered.y, precision: 5); + + var view = mul(float3(0, 0, 1), CultMath.float3x3.Euler(float3(float2(0.3f, -1.1f).yx, 0), RotationOrder.YXZ)); + Assert.Equal(1.0f, length(view), precision: 5); + + var noise = fbm(float3(1.5f, -2.25f, 0.75f), 4); + Assert.InRange(noise, 0.0f, 1.0f); + } +} diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs.meta b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs.meta new file mode 100644 index 00000000..0d673e1e --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 942fbcb9c28a4e559e5baf435dc24cf1 diff --git a/packages/cultmath/tests/CultMath.Tests/MathTests.cs b/packages/cultmath/tests/CultMath.Tests/MathTests.cs index 652ba23a..956bea08 100644 --- a/packages/cultmath/tests/CultMath.Tests/MathTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/MathTests.cs @@ -33,7 +33,7 @@ public sealed class MathTests } [Fact] - public void VectorsExposeCommonReadOnlySwizzles() + public void VectorsExposeCommonSwizzles() { Assert.Equal(new float3(2.0f, 3.0f, 1.0f), new float3(1.0f, 2.0f, 3.0f).yzx); Assert.Equal(new float3(3.0f, 2.0f, 1.0f), new float3(1.0f, 2.0f, 3.0f).zyx); @@ -277,7 +277,7 @@ public sealed class MathTests [InlineData(536.5106f, 536.5106f, -0.6901103f)] public void SimplexNoiseMatchesUnityMathematics(float x, float y, float expected) { - Assert.Equal(expected, math.simplex_noise(new float2(x, y)), precision: 6); + Assert.Equal(expected, math.snoise(new float2(x, y)), precision: 6); } [Fact]