CultLib/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs
Meta Vens e89fd68778 snoise tie rule: x over y, y over z, x over z (a total order) in C# and both HLSL copies
The cyclic non-strict step(x0.yzx, x0.xyz) made the four corners a non-simplex
where x0's components are equal: the origin and every (t, t, t) read a different
function (snoise(0) was -0.4358730, jumps up to 0.77). One strict compare, z
against x, gives simplexnoise1234's order. Off the tie set old and new return
the same bits.

NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal fails against the old
rule (0.054 within 1 ulp of (-53.6, -53.6, -53.6)). The origin golden is 0 and a
(1.25, 1.25, 1.25) diagonal row joins it. SNOISE_LIPSCHITZ and the GLSL follow
in the next commits, so the ratio pin and CommittedGlslEqualsLowering are red here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:15:32 +02:00

506 lines
24 KiB
C#

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 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));
Assert.Equal(1.0f, math.step(1.0f, float.NaN));
Assert.Equal(1.0f, math.step(float.NaN, 1.0f));
Assert.Equal(1.0f, math.step(1.0f, 1.0f));
// 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)));
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)));
// dxc: normalize(x) is x * Rsqrt(Dot(x, x)); length and distance are Sqrt of the summed squares.
static bool AllNaN(float4 value) =>
float.IsNaN(value.x) && float.IsNaN(value.y) && float.IsNaN(value.z) && float.IsNaN(value.w);
var n2 = math.normalize(float2.zero);
Assert.True(float.IsNaN(n2.x) && float.IsNaN(n2.y));
var n3 = math.normalize(float3.zero);
Assert.True(float.IsNaN(n3.x) && float.IsNaN(n3.y) && float.IsNaN(n3.z));
Assert.True(AllNaN(math.normalize(float4.zero)));
var nanX = math.normalize(new float2(float.NaN, 2.41f));
Assert.True(float.IsNaN(nanX.x) && float.IsNaN(nanX.y));
Assert.True(AllNaN(math.normalize(new float4(1.0f, float.NaN, 2.41f, 1.0f))));
var skew = new float3(0.25f, 1.0f, -0.1f);
var inverseLength = 1.0f / MathF.Sqrt(skew.x * skew.x + skew.y * skew.y + skew.z * skew.z);
var normalized = math.normalize(skew);
Assert.Equal(Bits(skew.x * inverseLength), Bits(normalized.x));
Assert.Equal(Bits(skew.y * inverseLength), Bits(normalized.y));
Assert.Equal(Bits(skew.z * inverseLength), Bits(normalized.z));
Assert.Equal(Bits(0.0f), Bits(math.length(float2.zero)));
Assert.Equal(Bits(0.0f), Bits(math.length(float3.zero)));
Assert.Equal(Bits(0.0f), Bits(math.length(float4.zero)));
Assert.Equal(Bits(0.0f), Bits(math.distance(new float3(1.0f, 2.0f, 3.0f), new float3(1.0f, 2.0f, 3.0f))));
// asuint(float) reinterprets the IEEE-754 bits.
Assert.Equal(0x80000000u, math.asuint(-0.0f));
Assert.Equal(0x3FC00000u, math.asuint(1.5f));
Assert.Equal(0x7FC00000u, math.asuint(BitConverter.Int32BitsToSingle(0x7FC00000)));
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)));
}
[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 VectorConversionsFollowDxc()
{
// fptosi truncates toward zero; NaN and out-of-range inputs are undefined in DXIL and not pinned.
Assert.Equal(new int2(-1, 2), math.int2(new float2(-1.7f, 2.9f)));
Assert.Equal(new int3(0, 0, 3), new int3(new float3(-0.5f, 0.5f, 3.99f)));
Assert.Equal(new float4(1.0f, -2.0f, 3.0f, 0.0f), math.float4(new int4(1, -2, 3, 0)));
// Numeric to bool is `!= 0` (fcmp une for float, so NaN is true); bool to numeric is 0 or 1.
Assert.Equal(new bool2(true, false), math.bool2(new float2(float.NaN, -0.0f)));
Assert.Equal(new bool3(false, true, true), math.bool3(new int3(0, 5, -1)));
Assert.Equal(new float2(1.0f, 0.0f), math.float2(new bool2(true, false)));
Assert.Equal(new int4(0, 1, 1, 0), math.int4(new bool4(false, true, true, false)));
// intN(intN) is exact; it must not promote through float and lose 2^24 + 1.
Assert.Equal(new int2(16777217, -16777217), math.int2(new int2(16777217, -16777217)));
float2 splat = math.float2(0);
Assert.Equal(float2.zero, splat);
}
[Fact]
public void FracAndIntegerClampFollowHlsl()
{
// frac(double) keeps double precision; a float frac of 1e9 + 0.25 would return 0.
Assert.Equal(0.25, math.frac(1.0e9 + 0.25));
Assert.Equal(0.75, math.frac(-0.25));
Assert.Equal(0.5f, math.frac(2.5f));
// IMin(IMax(x, a), b): the result stays int, and inverted bounds return b.
int clamped = math.clamp(7, -1, 3);
Assert.Equal(3, clamped);
Assert.Equal(-1, math.clamp(-5, -1, 3));
Assert.Equal(0, math.clamp(1, 3, 0));
Assert.Equal(new int2(0, 3), math.clamp(new int2(-4, 9), 0, 3));
Assert.Equal(new int3(1, 2, 3), math.clamp(new int3(1, 2, 3), new int3(0, 0, 0), new int3(5, 5, 5)));
Assert.Equal(new int4(2, 2, 4, 4), math.clamp(new int4(1, 2, 5, 4), new int4(2), new int4(4)));
Assert.Equal(1.0f, math.clamp(2.0f, -1, 1));
}
[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.0f)]
[InlineData(1.25f, 1.25f, 1.25f, -0.245501652f)]
[InlineData(0.25f, -0.5f, 1.75f, 0.0492948629f)]
[InlineData(12.25f, -4.5f, 3.125f, 0.443114728f)]
[InlineData(-7.3f, 2.9f, 101.4f, -0.10221792f)]
public void Snoise3MatchesGoldenValues(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);
}
}
}