diff --git a/packages/cultmath/shaders/CultMath.Affine.hlsl b/packages/cultmath/shaders/CultMath.Affine.hlsl
index f165148b..8727081e 100644
--- a/packages/cultmath/shaders/CultMath.Affine.hlsl
+++ b/packages/cultmath/shaders/CultMath.Affine.hlsl
@@ -12,6 +12,10 @@
// math.Affine.cs (SNOISE_HESSIAN).
static const float CULTMATH_SNOISE_HESSIAN = 56.050385;
+// An upper bound on the operator norm of the third-derivative tensor of cultmath_snoise(float3), the
+// next order after CULTMATH_SNOISE_HESSIAN; provenance in math.Span.cs (SNOISE_THIRD).
+static const float CULTMATH_SNOISE_THIRD = 1.0;
+
float3 cultmath_af_point(float value) { return float3(value, 0.0, 0.0); }
float3 cultmath_af_symbol(float x0, float a) { return float3(x0, a, 0.0); }
diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl
index c29fd603..18cd1fc9 100644
--- a/packages/cultmath/shaders/CultMath.hlsl
+++ b/packages/cultmath/shaders/CultMath.hlsl
@@ -228,6 +228,96 @@ float4 cultmath_snoise_grad(float3 value)
return float4(grad, value2);
}
+// Analytic Hessian of cultmath_snoise(float3); see the C# math.snoise_hessian comment for the
+// derivation. Every local up through p0..p3 and x0..x3 is cultmath_snoise_grad's own. A float3x3 built
+// from nine scalars reads as rows here and as columns in GLSL's mat3: the matrix is symmetric.
+float3x3 cultmath_snoise_hessian(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 = 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);
+ 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, float4(0.0, 0.0, 0.0, 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 m0 = max(0.5 - float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
+ float4 m2 = m0 * m0;
+ float4 m3 = m2 * m0;
+ float4 px = float4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3));
+ float4 wide = 48.0 * m2 * px;
+ float4 narrow = 8.0 * m3;
+
+ float4 cxx = float4(x0.x, x1.x, x2.x, x3.x);
+ float4 cxy = float4(x0.y, x1.y, x2.y, x3.y);
+ float4 cxz = float4(x0.z, x1.z, x2.z, x3.z);
+ float4 cpx = float4(p0.x, p1.x, p2.x, p3.x);
+ float4 cpy = float4(p0.y, p1.y, p2.y, p3.y);
+ float4 cpz = float4(p0.z, p1.z, p2.z, p3.z);
+
+ float hxx = 105.0 * (dot(wide * cxx, cxx) - dot(narrow, 2.0 * cpx * cxx + px));
+ float hyy = 105.0 * (dot(wide * cxy, cxy) - dot(narrow, 2.0 * cpy * cxy + px));
+ float hzz = 105.0 * (dot(wide * cxz, cxz) - dot(narrow, 2.0 * cpz * cxz + px));
+ float hxy = 105.0 * (dot(wide * cxx, cxy) - dot(narrow, cpx * cxy + cpy * cxx));
+ float hxz = 105.0 * (dot(wide * cxx, cxz) - dot(narrow, cpx * cxz + cpz * cxx));
+ float hyz = 105.0 * (dot(wide * cxy, cxz) - dot(narrow, cpy * cxz + cpz * cxy));
+ return float3x3(hxx, hxy, hxz, hxy, hyy, hyz, hxz, hyz, hzz);
+}
+
+// The integral over [t0, t1] of clamp(a + b t, lo, hi); see the C# math.integral_clamp_linear comment.
+float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float t0, float t1)
+{
+ if (b == 0.0)
+ return clamp(a, lo, hi) * (t1 - t0);
+
+ lo = min(lo, hi);
+ float direction = t1 < t0 ? -1.0 : 1.0;
+ float start = min(t0, t1);
+ float stop = max(t0, t1);
+ float atLo = (lo - a) / b;
+ float atHi = (hi - a) / b;
+ float enter = clamp(min(atLo, atHi), start, stop);
+ float leave = clamp(max(atLo, atHi), start, stop);
+ float before = b > 0.0 ? lo : hi;
+ float after = b > 0.0 ? hi : lo;
+ float total = before * (enter - start) + (a + b * (0.5 * (enter + leave))) * (leave - enter) + after * (stop - leave);
+ return direction * total;
+}
+
// Octave sum of cultmath_snoise_grad (invariant 8); see the C# math.fbm_grad comment for the
// per-octave frequency/amplitude derivation and why octaves is clamped.
float4 cultmath_fbm_grad(float3 p, int octaves, float lacunarity, float gain)
diff --git a/packages/cultmath/src/CultMath/math.Affine.cs b/packages/cultmath/src/CultMath/math.Affine.cs
index 3c00e397..8c4ce94a 100644
--- a/packages/cultmath/src/CultMath/math.Affine.cs
+++ b/packages/cultmath/src/CultMath/math.Affine.cs
@@ -31,7 +31,7 @@ public static partial class math
// curvature is realised if the constant is under it. The margin over the function: a scan of the
// whole 289^3 hash period found a realised maximum of 50.58, and bounding each corner's gradient
// independently over every hash configuration gives 52.22 (Soul pass s1). A change to the snoise
- // kernel must re-run MeasureHessian and re-pin it.
+ // kernel must re-run MeasureHessian and re-pin it. SNOISE_THIRD (math.Span.cs) is the next-order pin.
public const float SNOISE_HESSIAN = 56.050385f;
/// The point form (x, 0, 0): the value x at every eps. Exact.
diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs
new file mode 100644
index 00000000..4adc1139
--- /dev/null
+++ b/packages/cultmath/src/CultMath/math.Span.cs
@@ -0,0 +1,122 @@
+namespace CultMath;
+
+public static partial class math
+{
+ // An upper bound on the operator norm of the third-derivative tensor of snoise(float3):
+ // max over unit u of |D^3 snoise(p)[u, u, u]|, the next order after SNOISE_LIPSCHITZ (gradient)
+ // and SNOISE_HESSIAN (curvature). Provenance: NoiseBoundTests.MeasureThird (slow, explicit) takes
+ // the largest such norm over 1e6 seeded points of [-256, 256]^3 (central differences of
+ // snoise_hessian at h = 1e-3, then an ascent over unit vectors for the tensor's operator norm),
+ // refines the 1e4 largest by ascent over the point, and multiplies the maximum by 1.10; it prints
+ // the witness start. NoiseBoundTests.ThirdConstantPinsSampled pins the value against a fast
+ // re-measure and the ascent from that witness. Measured on the tied kernel (snoise_grad's order),
+ // not on any pre-tie splice. Empirical with a margin, not a proof. A change to the snoise kernel
+ // must re-run MeasureThird and re-pin it.
+ public const float SNOISE_THIRD = 1.0f;
+
+ // Analytic Hessian of snoise(float3): the symmetric matrix of second partial derivatives, same
+ // lattice, hash and tie order as snoise_grad, whose kernel it differentiates once more. Every local
+ // up through p0..p3 and x0..x3 is snoise_grad's own derivation verbatim. Per corner, with
+ // m = max(0.5 - dot(x,x), 0) and s = dot(p,x), the corner's gradient is -8 m^3 s x + m^4 p, so
+ // d/dx_j of its i-th component is 48 m^2 s x_i x_j - 8 m^3 (p_j x_i + p_i x_j + s delta_ij) (the
+ // derivative of m^3 is -6 m^2 x_j, of s is p_j); summed over the four corners and scaled by the
+ // same 105. A clamped corner (m = 0) contributes nothing, and the m^2 and m^3 factors make the
+ // contribution vanish continuously as it clamps. Each off-diagonal entry is computed once and
+ // placed twice, so the matrix is its own transpose bit for bit. A float3x3 is built from its nine
+ // scalars, which the GLSL lowering reads as columns and HLSL as rows: the matrix is symmetric.
+ public static float3x3 snoise_hessian(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 = 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);
+ 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 m0 = max(0.5f - new float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0f);
+ var m2 = m0 * m0;
+ var m3 = m2 * m0;
+ var px = new float4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3));
+ var wide = 48.0f * m2 * px;
+ var narrow = 8.0f * m3;
+
+ // One component of every corner's x and p per register.
+ var cxx = new float4(x0.x, x1.x, x2.x, x3.x);
+ var cxy = new float4(x0.y, x1.y, x2.y, x3.y);
+ var cxz = new float4(x0.z, x1.z, x2.z, x3.z);
+ var cpx = new float4(p0.x, p1.x, p2.x, p3.x);
+ var cpy = new float4(p0.y, p1.y, p2.y, p3.y);
+ var cpz = new float4(p0.z, p1.z, p2.z, p3.z);
+
+ var hxx = 105.0f * (dot(wide * cxx, cxx) - dot(narrow, 2.0f * cpx * cxx + px));
+ var hyy = 105.0f * (dot(wide * cxy, cxy) - dot(narrow, 2.0f * cpy * cxy + px));
+ var hzz = 105.0f * (dot(wide * cxz, cxz) - dot(narrow, 2.0f * cpz * cxz + px));
+ var hxy = 105.0f * (dot(wide * cxx, cxy) - dot(narrow, cpx * cxy + cpy * cxx));
+ var hxz = 105.0f * (dot(wide * cxx, cxz) - dot(narrow, cpx * cxz + cpz * cxx));
+ var hyz = 105.0f * (dot(wide * cxy, cxz) - dot(narrow, cpy * cxz + cpz * cxy));
+ return new float3x3(hxx, hxy, hxz, hxy, hyy, hyz, hxz, hyz, hzz);
+ }
+
+ // The integral over [t0, t1] of clamp(a + b t, lo, hi) dt, in closed form. The line meets lo at
+ // (lo - a) / b and hi at (hi - a) / b; the span is cut at those two crossings into the stretch held
+ // at the bound the line starts beyond, the stretch on the ramp (a trapezoid: its length times the
+ // line's value at the stretch's midpoint), and the stretch held at the far bound. b = 0 is the
+ // constant clamp(a, lo, hi); t1 < t0 is the negative of the integral over [t1, t0]; lo above hi
+ // clamps to hi everywhere, as clamp does. A crossing that overflows (b tiny) lies past the span and
+ // cuts nothing.
+ public static float integral_clamp_linear(float a, float b, float lo, float hi, float t0, float t1)
+ {
+ if (b == 0.0f)
+ return clamp(a, lo, hi) * (t1 - t0);
+
+ lo = min(lo, hi);
+ var direction = t1 < t0 ? -1.0f : 1.0f;
+ var start = min(t0, t1);
+ var stop = max(t0, t1);
+ var atLo = (lo - a) / b;
+ var atHi = (hi - a) / b;
+ var enter = clamp(min(atLo, atHi), start, stop);
+ var leave = clamp(max(atLo, atHi), start, stop);
+ var before = b > 0.0f ? lo : hi;
+ var after = b > 0.0f ? hi : lo;
+ var total = before * (enter - start) + (a + b * (0.5f * (enter + leave))) * (leave - enter) + after * (stop - leave);
+ return direction * total;
+ }
+}
diff --git a/packages/cultmath/src/CultMath/math.Span.cs.meta b/packages/cultmath/src/CultMath/math.Span.cs.meta
new file mode 100644
index 00000000..31f6c409
--- /dev/null
+++ b/packages/cultmath/src/CultMath/math.Span.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1466848dfbfc420ab4f3f67090944a67
diff --git a/packages/cultmath/tests/CultMath.Tests/GlslLowering.cs b/packages/cultmath/tests/CultMath.Tests/GlslLowering.cs
index 5d119d86..5684535e 100644
--- a/packages/cultmath/tests/CultMath.Tests/GlslLowering.cs
+++ b/packages/cultmath/tests/CultMath.Tests/GlslLowering.cs
@@ -74,7 +74,9 @@ internal static class GlslLowering
source = Regex.Replace(source, @"(?m)^[ \t]*#(ifndef|define|endif)\b.*$", string.Empty);
source = Regex.Replace(source, @"(?ms)^\w+ \w+\([^)]*\b(Texture2D|SamplerState)\b[^)]*\)\s*\{.*?^\}[ \t]*$", string.Empty);
- // 3. Type names: floatN, intN, uintN and boolN become vecN, ivecN, uvecN and bvecN.
+ // 3. Type names: floatN, intN, uintN and boolN become vecN, ivecN, uvecN and bvecN, and float3x3
+ // becomes mat3 (the one matrix type the library returns: a float3x3 is built from nine scalars,
+ // which HLSL reads as rows and GLSL as columns, so only symmetric matrices are exported).
source = LowerTypeNames(source);
// 4. File-scope `static const` becomes `const`.
@@ -110,7 +112,8 @@ internal static class GlslLowering
source = Regex.Replace(source, @"\bfloat([234])\b", "vec$1");
source = Regex.Replace(source, @"\bint([234])\b", "ivec$1");
source = Regex.Replace(source, @"\buint([234])\b", "uvec$1");
- return Regex.Replace(source, @"\bbool([234])\b", "bvec$1");
+ source = Regex.Replace(source, @"\bbool([234])\b", "bvec$1");
+ return Regex.Replace(source, @"\bfloat3x3\b", "mat3");
}
private static readonly Regex Cast = new(@"\((float|int|uint)\)");
diff --git a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs
index f7699379..14ea07c0 100644
--- a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs
+++ b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs
@@ -47,7 +47,7 @@ public sealed class GlslMirrorTests
// Identifier boundaries: cultmath_lerp is a CultMath function, lerp( is HLSL.
private static readonly Regex HlslOnly = new(
- @"\b(float|u?int|bool)[234]\b|\blerp\(|\bfrac\(|\bsaturate\(|\brsqrt\(|\basuint\(|\basfloat\(|\batan2\(|static const|\((float|int|uint)\)|Texture2D|SamplerState|#include|precision (high|medium|low)p");
+ @"\b(float|u?int|bool)[234]\b|\bfloat3x3\b|\blerp\(|\bfrac\(|\bsaturate\(|\brsqrt\(|\basuint\(|\basfloat\(|\batan2\(|static const|\((float|int|uint)\)|Texture2D|SamplerState|#include|precision (high|medium|low)p");
[Fact]
public void NoHlslOnlyTokensSurvive()
@@ -630,6 +630,14 @@ public sealed class GlslMirrorTests
new Family("float cultmath_cycles_arcsine(float)", "ulp-bounded, platform asin", r => new object[] { Uniform(r, -1.5f, 1.5f) }, Platform: Platform.Ulp),
new Family("float cultmath_cycles_arccosine(float)", "ulp-bounded, platform acos", r => new object[] { Uniform(r, -1.5f, 1.5f) }, Platform: Platform.Ulp),
new Family("float cultmath_cycles_arctan2(float, float)", "ulp-bounded, platform atan", r => new object[] { Uniform(r, -20.0f, 20.0f), Uniform(r, -20.0f, 20.0f) }, Platform: Platform.Ulp),
+ new Family("mat3 cultmath_snoise_hessian(vec3)", "ulp-bounded", r => new object[] { Point3(r) }),
+ new Family("mat3 cultmath_snoise_hessian(vec3)", "ulp-bounded, on the simplex tie set", r => new object[] { TiePoint3(r) }),
+ new Family("float cultmath_integral_clamp_linear(float, float, float, float, float, float)", "ulp-bounded",
+ r =>
+ {
+ var lo = Uniform(r, -2.0f, 0.5f);
+ return new object[] { Uniform(r, -4.0f, 4.0f), r.Next(8) == 0 ? 0.0f : Uniform(r, -4.0f, 4.0f), lo, lo + Uniform(r, 0.0f, 3.0f), Uniform(r, -3.0f, 3.0f), Uniform(r, -3.0f, 3.0f) };
+ }),
}).ToArray();
// The fixture text, each case's arguments given by family and point index.
@@ -693,8 +701,12 @@ public sealed class GlslMirrorTests
_ => throw new InvalidOperationException(v.GetType().Name),
}));
+ // A float3x3 flattens row by row; the matrices the library exports are symmetric, so the order is
+ // the same read as GLSL's columns.
private static IEnumerable