Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
|
|
|
using System.Text.Json;
|
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>
2026-09-14 13:29:59 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
|
|
|
[Fact]
|
|
|
|
|
public void GeneratedSwizzlesCoverRepeatsColorNamesAndAllVectorFamilies()
|
|
|
|
|
{
|
|
|
|
|
var v = new float4(1.0f, 2.0f, 3.0f, 4.0f);
|
|
|
|
|
Assert.Equal(new float4(4.0f, 4.0f, 1.0f, 2.0f), v.wwxy);
|
|
|
|
|
Assert.Equal(new float3(3.0f, 2.0f, 1.0f), v.bgr);
|
|
|
|
|
Assert.Equal(4.0f, v.a);
|
|
|
|
|
Assert.Equal(new float4(2.0f, 2.0f, 2.0f, 2.0f), new float2(1.0f, 2.0f).yyyy);
|
|
|
|
|
|
|
|
|
|
v.wx = new float2(9.0f, 8.0f);
|
|
|
|
|
Assert.Equal(new float4(8.0f, 2.0f, 3.0f, 9.0f), v);
|
|
|
|
|
v.r = 0.5f;
|
|
|
|
|
v.gb += new float2(1.0f, 1.0f);
|
|
|
|
|
Assert.Equal(new float4(0.5f, 3.0f, 4.0f, 9.0f), v);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(new int3(3, 1, 1), new int3(1, 2, 3).zxx);
|
|
|
|
|
Assert.Equal(new bool2(false, true), new bool4(true, false, false, true).zw);
|
|
|
|
|
Assert.Equal(new double2(2.0, 1.0), new double3(1.0, 2.0, 3.0).yx);
|
|
|
|
|
|
|
|
|
|
var cell = new int4(1, 2, 3, 4);
|
|
|
|
|
cell.xyz = new int3(7, 8, 9);
|
|
|
|
|
Assert.Equal(new int4(7, 8, 9, 4), cell);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void MixedConstructorsMatchHlsl()
|
|
|
|
|
{
|
|
|
|
|
var xy = new float2(1.0f, 2.0f);
|
|
|
|
|
var zw = new float2(3.0f, 4.0f);
|
|
|
|
|
var expected = new float4(1.0f, 2.0f, 3.0f, 4.0f);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(expected, math.float4(xy, zw));
|
|
|
|
|
Assert.Equal(expected, math.float4(1.0f, new float3(2.0f, 3.0f, 4.0f)));
|
|
|
|
|
Assert.Equal(expected, math.float4(new float3(1.0f, 2.0f, 3.0f), 4.0f));
|
|
|
|
|
Assert.Equal(expected, math.float4(xy, 3.0f, 4.0f));
|
|
|
|
|
Assert.Equal(expected, math.float4(1.0f, 2.0f, zw));
|
|
|
|
|
Assert.Equal(expected, math.float4(1.0f, new float2(2.0f, 3.0f), 4.0f));
|
|
|
|
|
Assert.Equal(expected, new float4(xy, zw));
|
|
|
|
|
Assert.Equal(new float3(1.0f, 2.0f, 3.0f), math.float3(1.0f, zw - 1.0f));
|
|
|
|
|
Assert.Equal(new int4(1, 2, 3, 4), math.int4(new int2(1, 2), new int2(3, 4)));
|
|
|
|
|
Assert.Equal(new bool3(true, false, true), math.bool3(true, new bool2(false, true)));
|
|
|
|
|
Assert.Equal(new double3(1.0, 2.0, 3.0), new double3(new double2(1.0, 2.0), 3.0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void MatrixRowElementsAreWritable()
|
|
|
|
|
{
|
|
|
|
|
var m = float3x3.identity;
|
|
|
|
|
m[1][2] = 5.0f;
|
|
|
|
|
m[2].x = 7.0f;
|
|
|
|
|
m._m00 = 2.0f;
|
|
|
|
|
|
|
|
|
|
Assert.Equal(5.0f, m._m12);
|
|
|
|
|
Assert.Equal(7.0f, m._m20);
|
|
|
|
|
Assert.Equal(math.float3x3(2.0f, 0.0f, 0.0f, 0.0f, 1.0f, 5.0f, 7.0f, 0.0f, 1.0f), m);
|
|
|
|
|
Assert.Equal(float3x3.identity, math.float3x3(1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f));
|
|
|
|
|
|
|
|
|
|
var r = float2x2.identity;
|
|
|
|
|
r[0][1] = 3.0f;
|
|
|
|
|
Assert.Equal(math.float2x2(1.0f, 3.0f, 0.0f, 1.0f), r);
|
|
|
|
|
Assert.Equal(new float2(0.0f, 1.0f), float2x2.identity[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void IntrinsicEdgeCasesFollowHlsl()
|
|
|
|
|
{
|
|
|
|
|
Assert.True(math.any(new float3(0.0f, 0.0f, -2.0f)));
|
|
|
|
|
Assert.False(math.any(float4.zero));
|
|
|
|
|
Assert.True(math.all(new int2(1, -1)));
|
|
|
|
|
Assert.False(math.all(new int3(1, 0, 1)));
|
|
|
|
|
Assert.True(math.any(new float2(float.NaN, 0.0f)));
|
|
|
|
|
|
|
|
|
|
Assert.Equal(new int3(1, 2, 3), math.min(new int3(1, 5, 3), new int3(4, 2, 6)));
|
|
|
|
|
Assert.Equal(new int2(4, 5), math.max(new int2(1, 5), new int2(4, 2)));
|
|
|
|
|
Assert.Equal(new int4(1, 2, 0, 3), math.abs(new int4(-1, 2, 0, -3)));
|
|
|
|
|
Assert.Equal(7, math.abs(-7));
|
|
|
|
|
|
2026-09-14 14:09:49 +02:00
|
|
|
Assert.Equal(1.0f, math.step(1.0f, float.NaN));
|
|
|
|
|
Assert.Equal(1.0f, math.step(float.NaN, 1.0f));
|
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
|
|
|
Assert.Equal(1.0f, math.step(1.0f, 1.0f));
|
|
|
|
|
|
Match float min, max, clamp, and saturate to DXIL FMin/FMax/Saturate
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.
Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:19 +02:00
|
|
|
// DXIL FMin/FMax/Saturate: a NaN operand returns the other; max(a, b) is a >= b ? a : b.
|
|
|
|
|
Assert.Equal(1.0f, math.min(float.NaN, 1.0f));
|
|
|
|
|
Assert.Equal(1.0f, math.min(1.0f, float.NaN));
|
|
|
|
|
Assert.Equal(1.0f, math.max(float.NaN, 1.0f));
|
|
|
|
|
Assert.Equal(1.0f, math.max(1.0f, float.NaN));
|
|
|
|
|
Assert.True(float.IsNaN(math.max(float.NaN, float.NaN)));
|
|
|
|
|
Assert.Equal(2.0f, math.clamp(float.NaN, 2.0f, 3.0f));
|
|
|
|
|
Assert.Equal(new float3(0.0f, 1.0f, 0.0f), math.saturate(new float3(float.NaN, 2.0f, -1.0f)));
|
|
|
|
|
Assert.Equal(new float2(-1.0f, 4.0f), math.max(new float2(float.NaN, 4.0f), new float2(-1.0f, float.NaN)));
|
|
|
|
|
static int Bits(float value) => BitConverter.SingleToInt32Bits(value);
|
|
|
|
|
Assert.Equal(Bits(-0.0f), Bits(math.max(-0.0f, 0.0f)));
|
|
|
|
|
Assert.Equal(Bits(0.0f), Bits(math.min(-0.0f, 0.0f)));
|
|
|
|
|
Assert.Equal(Bits(0.0f), Bits(math.saturate(-0.0f)));
|
2026-09-14 14:47:10 +02:00
|
|
|
Assert.Equal(1.0, math.min(double.NaN, 1.0));
|
|
|
|
|
Assert.Equal(1.0, math.min(1.0, double.NaN));
|
|
|
|
|
Assert.Equal(1.0, math.max(double.NaN, 1.0));
|
|
|
|
|
Assert.Equal(1.0, math.max(1.0, double.NaN));
|
|
|
|
|
Assert.True(double.IsNaN(math.max(double.NaN, double.NaN)));
|
|
|
|
|
static long DoubleBits(double value) => BitConverter.DoubleToInt64Bits(value);
|
|
|
|
|
Assert.Equal(DoubleBits(-0.0), DoubleBits(math.max(-0.0, 0.0)));
|
|
|
|
|
Assert.Equal(DoubleBits(0.0), DoubleBits(math.min(-0.0, 0.0)));
|
Match float min, max, clamp, and saturate to DXIL FMin/FMax/Saturate
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.
Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:19 +02:00
|
|
|
|
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
|
|
|
Assert.Equal(0, math.sign(float.NaN));
|
|
|
|
|
Assert.Equal(-1, math.sign(-0.5f));
|
|
|
|
|
Assert.Equal(new int3(-1, 0, 1), math.sign(new float3(-2.0f, 0.0f, 4.0f)));
|
|
|
|
|
Assert.Equal(new int2(1, -1), math.sign(new int2(9, -9)));
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-09-14 13:29:59 +02:00
|
|
|
[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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|