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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-14 13:29:59 +02:00
commit d514efebba
29 changed files with 1609 additions and 75 deletions

View file

@ -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,

View file

@ -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);
}

View file

@ -2,17 +2,50 @@ using System;
namespace CultMath;
public record struct bool2(bool x, bool y)
public struct bool2 : IEquatable<bool2>
{
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})";
}

View file

@ -0,0 +1,55 @@
using System;
namespace CultMath;
public struct bool3 : IEquatable<bool3>
{
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})";
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 323ec670fc374c8ba4d87b4bf4c91f34

View file

@ -0,0 +1,59 @@
using System;
namespace CultMath;
public struct bool4 : IEquatable<bool4>
{
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})";
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d38b9f70d88c459584441b85ecbfd92a

View file

@ -2,17 +2,38 @@ using System;
namespace CultMath;
public record struct double2(double x, double y)
public struct double2 : IEquatable<double2>
{
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})");
}

View file

@ -2,14 +2,25 @@ using System;
namespace CultMath;
public record struct double3(double x, double y, double z)
public struct double3 : IEquatable<double3>
{
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})");
}

View file

@ -2,17 +2,48 @@ using System;
namespace CultMath;
public record struct float2(float x, float y)
public struct float2 : IEquatable<float2>
{
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})");
}

View file

@ -0,0 +1,69 @@
using System;
namespace CultMath;
/// <summary>
/// HLSL float2x2: row-major. Constructors take rows, <c>m[i]</c> is row i, and
/// <c>_mRC</c> names row R, column C exactly as HLSL does. Use <see cref="math.mul(float2x2, float2)"/>
/// for products; there is no <c>*</c> matrix product because HLSL's <c>*</c> is component-wise.
/// </summary>
public struct float2x2 : IEquatable<float2x2>
{
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));
}
}
}
/// <summary>
/// Counter-clockwise rotation for column vectors: <c>mul(Rotate(a), v)</c> equals
/// <c>math.rotate(v, a)</c>; <c>mul(v, Rotate(a))</c> rotates the other way.
/// </summary>
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})");
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c0dcec14c0b7432dbd8ba0bebb0a7c2f

View file

@ -2,11 +2,22 @@ using System;
namespace CultMath;
public record struct float3(float x, float y, float z)
public struct float3 : IEquatable<float3>
{
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})");
}

View file

@ -0,0 +1,125 @@
using System;
namespace CultMath;
/// <summary>
/// HLSL float3x3: row-major. Constructors take rows, <c>m[i]</c> is row i, and
/// <c>_mRC</c> names row R, column C exactly as HLSL does. Use <see cref="math.mul(float3x3, float3)"/>
/// for products; there is no <c>*</c> matrix product because HLSL's <c>*</c> is component-wise.
/// </summary>
public struct float3x3 : IEquatable<float3x3>
{
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));
}
}
}
/// <summary>Rotation about +X for column vectors (<c>mul(m, v)</c>).</summary>
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);
}
/// <summary>Rotation about +Y for column vectors (<c>mul(m, v)</c>).</summary>
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);
}
/// <summary>Rotation about +Z for column vectors (<c>mul(m, v)</c>).</summary>
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);
}
/// <summary>
/// Extrinsic Euler rotation in radians. The order names the axis applied first:
/// <see cref="math.RotationOrder.YXZ"/> rotates about Y, then X, then Z, so the
/// matrix is <c>mul(RotateZ, mul(RotateX, RotateY))</c> for column vectors.
/// </summary>
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})");
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9ca36a9bfff144a9b51478a6d7b19c8e

View file

@ -2,11 +2,24 @@ using System;
namespace CultMath;
public record struct float4(float x, float y, float z, float w)
public struct float4 : IEquatable<float4>
{
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})");
}

View file

@ -2,17 +2,38 @@ using System;
namespace CultMath;
public record struct int2(int x, int y)
public struct int2 : IEquatable<int2>
{
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})");
}

View file

@ -0,0 +1,64 @@
using System;
namespace CultMath;
public struct int3 : IEquatable<int3>
{
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})");
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 42ee98aace354db19b3ae87bdcbc1df9

View file

@ -0,0 +1,68 @@
using System;
namespace CultMath;
public struct int4 : IEquatable<int4>
{
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})");
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1a313709f76a430eb83901bb2d05c74e

View file

@ -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;
/// <summary>
/// Extrinsic Euler order for <see cref="CultMath.float3x3.Euler(CultMath.float3, RotationOrder)"/>:
/// the first letter is the axis applied first. Nested here so <c>using static CultMath.math;</c>
/// brings it into scope with the rest of the shader vocabulary.
/// </summary>
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)
{

View file

@ -1,9 +1,28 @@
using System;
namespace CultMath;
public record struct quaternion(float x, float y, float z, float w)
/// <summary>
/// Not an HLSL type: shaders carry rotations as float4. Equality therefore stays
/// scalar (<c>==</c> returns bool) instead of following the component-wise vector rule.
/// </summary>
public struct quaternion : IEquatable<quaternion>
{
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})");
}

View file

@ -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",

View file

@ -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<float3, string> { [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<int2> { new int2(3, 4) };
Assert.Contains(new int2(3, 4), cells);
Assert.Contains(new int3(1, 2, 3), new HashSet<int3> { 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);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7164d00a023e43578a31897347d8f106

View file

@ -0,0 +1,85 @@
using Xunit;
using static CultMath.math;
namespace CultMath.Tests;
/// <summary>
/// HLSL written against CultMath compiles as C# under <c>using static CultMath.math;</c>.
/// The only spelling C# forces is float literal suffixes (<c>0.5f</c>): an unsuffixed
/// literal is a double in C#.
/// </summary>
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);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 942fbcb9c28a4e559e5baf435dc24cf1

View file

@ -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]