From e89fd687789be7a8c1977e084f6e88ac5eb18620 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 4 Oct 2026 03:07:02 +0200 Subject: [PATCH] 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 --- packages/cultmath/shaders/CultMath.hlsl | 7 ++- packages/cultmath/src/CultMath/math.cs | 9 ++- .../CultMath.Tests/HlslSemanticsTests.cs | 3 +- .../tests/CultMath.Tests/NoiseGradTests.cs | 59 ++++++++++++++++++- .../Shaders/CultMath.hlsl | 7 ++- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index cbb13e4e..e40cf0b6 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -80,13 +80,14 @@ float3 cultmath_snoise_permute(float3 value) { return cultmath_snoise_mod289(((v float4 cultmath_snoise_permute(float4 value) { return cultmath_snoise_mod289(((value * 34.0) + 10.0) * value); } // Ashima Arts / Ian McEwan 3D simplex noise (MIT), same float32 evaluation -// order as the C# math.snoise(float3) mirror. +// order as the C# math.snoise(float3) mirror. Ties resolve in simplexnoise1234's +// total order; see math.snoise. float cultmath_snoise(float3 value) { 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 g = float3(step(x0.y, x0.x), step(x0.z, x0.y), 1.0 - step(x0.z, x0.x)); float3 l = 1.0 - g; float3 i1 = min(g.xyz, l.zxy); float3 i2 = max(g.xyz, l.zxy); @@ -172,7 +173,7 @@ float4 cultmath_snoise_grad(float3 value) 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 g = float3(step(x0.y, x0.x), step(x0.z, x0.y), 1.0 - step(x0.z, x0.x)); float3 l = 1.0 - g; float3 i1 = min(g.xyz, l.zxy); float3 i2 = max(g.xyz, l.zxy); diff --git a/packages/cultmath/src/CultMath/math.cs b/packages/cultmath/src/CultMath/math.cs index d6c85847..a5a30a09 100644 --- a/packages/cultmath/src/CultMath/math.cs +++ b/packages/cultmath/src/CultMath/math.cs @@ -599,7 +599,10 @@ public static partial class math // in shaders/CultMath.hlsl with the same float32 evaluation order. Follows stegu/webgl-noise // src/noise3D.glsl at 22434e04d7 (kernel radius^2 0.5, scale 105, permute (34x+10)x); see // THIRD-PARTY-NOTICES.md. Continuous across simplex-cell boundaries - // (NoiseGradTests.SnoiseAndItsGradientAreContinuousAcrossSimplexCellBoundaries). + // (NoiseGradTests.SnoiseAndItsGradientAreContinuousAcrossSimplexCellBoundaries). Ties in x0 resolve + // in Gustavson's simplexnoise1234 order (x over y, y over z, x over z), a total order, so the four + // corners are always a simplex; upstream's step is cyclic and degenerates where x0's components are + // equal (NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal). // SNOISE_LIPSCHITZ (math.Interval.cs) is measured from this kernel: a change here must re-run // NoiseBoundTests.MeasureLipschitz and re-pin it. public static float snoise(float3 value) @@ -608,7 +611,7 @@ public static partial class math 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 g = new float3(step(x0.y, x0.x), step(x0.z, x0.y), 1.0f - step(x0.z, x0.x)); var l = 1.0f - g; var lzxy = new float3(l.z, l.x, l.y); var i1 = min(g, lzxy); @@ -714,7 +717,7 @@ public static partial class math 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 g = new float3(step(x0.y, x0.x), step(x0.z, x0.y), 1.0f - step(x0.z, x0.x)); var l = 1.0f - g; var lzxy = new float3(l.z, l.x, l.y); var i1 = min(g, lzxy); diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs index c6bac266..27a79234 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSemanticsTests.cs @@ -444,7 +444,8 @@ public sealed class HlslSemanticsTests } [Theory] - [InlineData(0.0f, 0.0f, 0.0f, -0.435872972f)] + [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)] diff --git a/packages/cultmath/tests/CultMath.Tests/NoiseGradTests.cs b/packages/cultmath/tests/CultMath.Tests/NoiseGradTests.cs index 77ec4819..f55cf38e 100644 --- a/packages/cultmath/tests/CultMath.Tests/NoiseGradTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/NoiseGradTests.cs @@ -133,7 +133,7 @@ public sealed class NoiseGradTests { var i = math.floor(p + math.dot(p, new float3(1.0f / 3.0f, 1.0f / 3.0f, 1.0f / 3.0f))); var x0 = p - i + math.dot(i, new float3(1.0f / 6.0f, 1.0f / 6.0f, 1.0f / 6.0f)); - var g = math.step(new float3(x0.y, x0.z, x0.x), x0); + var g = new float3(math.step(x0.y, x0.x), math.step(x0.z, x0.y), 1.0f - math.step(x0.z, x0.x)); var l = 1.0f - g; var lzxy = new float3(l.z, l.x, l.y); return (i, math.min(g, lzxy), math.max(g, lzxy)); @@ -190,6 +190,63 @@ public sealed class NoiseGradTests Assert.True(maxGradientJump < 1.0e-4f, $"gradient jump {maxGradientJump} across a simplex boundary"); } + // The simplex diagonals are where x0's three components compare equal. The tie rule must be a total + // order there (math.snoise): with a cyclic or all-strict compare the four corners stop being a + // simplex and the value jumps by up to 0.77 on the line (t, t, t), lattice diagonals included. + [Fact] + public void SnoiseIsContinuousOnTheSimplexDiagonal() + { + const int vertices = 160; + const int perVertex = 8; + var random = new System.Random(0x71E5); + var points = new System.Collections.Generic.List(); + for (var k = 0; k < 400; k++) + { + var t = (float)(random.NextDouble() * 128.0 - 64.0); + points.Add(new float3(t, t, t)); + } + + points.Add(new float3(0.0f, 0.0f, 0.0f)); + for (var k = 0; k < vertices; k++) + { + var lx = random.Next(-64, 65); + var ly = random.Next(-64, 65); + var lz = random.Next(-64, 65); + var shift = (lx + ly + lz) / 6.0f; + var v = new float3(lx - shift, ly - shift, lz - shift); + for (var j = 0; j < perVertex; j++) + { + var s = j == 0 ? 0.0f : random.NextSingle(); + points.Add(new float3(v.x + s, v.y + s, v.z + s)); + } + } + + foreach (var p in points) + { + var value = math.snoise(p); + for (var axis = 0; axis < 3; axis++) + { + for (var direction = -1; direction <= 1; direction += 2) + { + var q = p; + for (var ulp = 1; ulp <= 2; ulp++) + { + var component = axis == 0 ? q.x : axis == 1 ? q.y : q.z; + component = direction < 0 ? MathF.BitDecrement(component) : MathF.BitIncrement(component); + q = axis == 0 ? new float3(component, q.y, q.z) : axis == 1 ? new float3(q.x, component, q.z) : new float3(q.x, q.y, component); + var jump = MathF.Abs(math.snoise(q) - value); + Assert.True(jump <= 1.0e-4f, $"snoise jumps {jump} within {ulp} ulp of {p}"); + } + } + } + + var grad = math.snoise_grad(p); + Assert.Equal(BitConverter.SingleToInt32Bits(value), BitConverter.SingleToInt32Bits(grad.w)); + var slope = math.length(new float3(grad.x, grad.y, grad.z)); + Assert.True(slope <= math.SNOISE_LIPSCHITZ, $"|snoise_grad| {slope} exceeds SNOISE_LIPSCHITZ at {p}"); + } + } + // ---- snoise_grad.w vs snoise ---- [Fact] diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index cbb13e4e..e40cf0b6 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -80,13 +80,14 @@ float3 cultmath_snoise_permute(float3 value) { return cultmath_snoise_mod289(((v float4 cultmath_snoise_permute(float4 value) { return cultmath_snoise_mod289(((value * 34.0) + 10.0) * value); } // Ashima Arts / Ian McEwan 3D simplex noise (MIT), same float32 evaluation -// order as the C# math.snoise(float3) mirror. +// order as the C# math.snoise(float3) mirror. Ties resolve in simplexnoise1234's +// total order; see math.snoise. float cultmath_snoise(float3 value) { 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 g = float3(step(x0.y, x0.x), step(x0.z, x0.y), 1.0 - step(x0.z, x0.x)); float3 l = 1.0 - g; float3 i1 = min(g.xyz, l.zxy); float3 i2 = max(g.xyz, l.zxy); @@ -172,7 +173,7 @@ float4 cultmath_snoise_grad(float3 value) 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 g = float3(step(x0.y, x0.x), step(x0.z, x0.y), 1.0 - step(x0.z, x0.x)); float3 l = 1.0 - g; float3 i1 = min(g.xyz, l.zxy); float3 i2 = max(g.xyz, l.zxy);