From 1dca43596e81e8c10db7b6fc801a2e612dd4a2b0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 25 Sep 2026 19:17:39 +0200 Subject: [PATCH] Add smin_grad and cellular value-and-gradient primitives to CultMath smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h) is the exact analytic derivative, not an approximation, since the h-derivative terms cancel algebraically inside the smoothing band. cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996), searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature points. It returns the new CultCellular struct (nearest, edge, id), following the repo's existing degenerate-normal convention (GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a finite zero gradient instead of NaN at a feature point. Both are fresh implementations from the cited literature, not ports, so they get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry. design.md gains an "Invariant 8: Value-and-Gradient Primitives" section documenting both primitives' semantics and conventions. Co-Authored-By: Claude Sonnet 5 --- packages/cultmath/docs/design.md | 43 ++++++++++- .../cultmath/src/CultMath/CultCellular.cs | 25 +++++++ .../src/CultMath/CultCellular.cs.meta | 2 + packages/cultmath/src/CultMath/math.cs | 73 +++++++++++++++++++ 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 packages/cultmath/src/CultMath/CultCellular.cs create mode 100644 packages/cultmath/src/CultMath/CultCellular.cs.meta diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index f5f5d5a5..dde7a4d2 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -100,7 +100,10 @@ needs anything else, the test fails: no CultMath analog. 3. File-scope `static const` becomes `const`. 4. Floating literals gain `f` (`0.5` becomes `0.5f`). -5. The file body is wrapped in a class, since C# has no free functions; HLSL +5. Struct fields gain `public`. HLSL struct members have no access-modifier + concept; a C# struct's fields default to `private`, which would hide them + from the mirror test's reflection-based field comparison. +6. The file body is wrapped in a class, since C# has no free functions; HLSL functions become private instance methods. HLSL constructs that stay outside the target and must not appear in the mirror: @@ -120,6 +123,44 @@ compiled under `using static CultMath.math;`, so its intrinsics are C# `math` itself: the test proves the composition in the text, not the intrinsic rules, which only `HlslSemanticsTests` pins. +The comparison also allows exactly one struct return shape: a mirror function +whose HLSL return type is a plain struct of `float`/`floatN`/`int`/`intN` +fields is compared to its C# counterpart field by field, recursing into each +field's own components, down to bit-for-bit scalars. `CultCellular` (`math.cs`, +`cellular`) is the first and, for now, only consumer. A struct return with a +mismatched field does not get a second comparison path or a shape of its own; +it fails the same walk that already handles a bare vector return. + +## Invariant 8: Value-and-Gradient Primitives + +A primitive tagged invariant 8 returns its value and analytic gradient +together, laid out as `float4(gradient.xyz, value.w)`, with no value-only twin +(a value-only form would be a second path with no consumer). `smin_grad` and +`cellular` are the first primitives in this family: + +- `smin_grad(float4 a, float4 b, float k)` is Inigo Quilez's cubic-polynomial + smooth minimum ("smooth minimum", + ), carried to value-and-gradient + form. Its blend factor `h` is affine in `b.w - a.w`, so differentiating the + value with respect to position, the terms carrying `dh/dp` cancel exactly; + the surviving gradient is `lerp(∇b, ∇a, h)`, the same `h` the value uses. + This is the analytic gradient, not an approximation, and it is continuous + across the `|a.w - b.w| = k` seam where `h` saturates to 0 or 1. +- `cellular(float3 p)` is Worley's cellular texture basis function ("A + Cellular Texture Basis Function", SIGGRAPH 1996), searched over the + jittered 3×3×3 neighbourhood of feature points selected by `pcg3d` (never + the sin-based `hash`). It returns `CultCellular { nearest, edge, id }`: + `nearest` is `(∇F1, F1)`, `edge` is `(∇F2 - ∇F1, F2 - F1)`, and `id` is the + nearest cell's `pcg3d` hash mapped to `[0, 1)`. `∇F1`/`∇F2` are undefined + exactly at their own feature point (`F1 = 0` or `F2 = 0`); `cellular` + follows the repo's existing degenerate-normal convention + (`GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle`, which + guards a zero-length normal and returns the zero vector instead of the NaN + a bare `normalize` gives there) and returns the zero vector rather than NaN. + The identity of the nearest and second-nearest feature point changes + discontinuously across the `F1 = F2` set (a genuine kink, not a numerical + artifact), so `∇F1` and `∇F2` are only continuous away from that set. + Integer hashing uses the PCG hashes from Jarzynski and Olano, "Hash Functions for GPU Rendering" (JCGT 9(3), 2020): `pcg(uint)` is O'Neill's RXS-M-XS 32/32 permutation over one LCG step, and `pcg3d`/`pcg4d` are the paper's (3 → 3) and diff --git a/packages/cultmath/src/CultMath/CultCellular.cs b/packages/cultmath/src/CultMath/CultCellular.cs new file mode 100644 index 00000000..a48cb95e --- /dev/null +++ b/packages/cultmath/src/CultMath/CultCellular.cs @@ -0,0 +1,25 @@ +namespace CultMath; + +/// +/// The value-and-gradient result of (invariant 8's one struct +/// return shape). Every field follows the same float4(gradient.xyz, value.w) layout as a +/// bare value-and-gradient return. +/// +public struct CultCellular +{ + /// (∇F1, F1): the gradient and distance to the nearest feature point. + public float4 nearest; + + /// (∇F2 - ∇F1, F2 - F1): for lineae along cell borders. + public float4 edge; + + /// The nearest cell's pcg3d hash, mapped to [0, 1). + public float id; + + public CultCellular(float4 nearest, float4 edge, float id) + { + this.nearest = nearest; + this.edge = edge; + this.id = id; + } +} diff --git a/packages/cultmath/src/CultMath/CultCellular.cs.meta b/packages/cultmath/src/CultMath/CultCellular.cs.meta new file mode 100644 index 00000000..8c9ed1ae --- /dev/null +++ b/packages/cultmath/src/CultMath/CultCellular.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 76be9498b63e410891715b3ec965caf8 diff --git a/packages/cultmath/src/CultMath/math.cs b/packages/cultmath/src/CultMath/math.cs index b5d5339c..32d6afb1 100644 --- a/packages/cultmath/src/CultMath/math.cs +++ b/packages/cultmath/src/CultMath/math.cs @@ -688,6 +688,79 @@ public static partial class math 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); + // Inigo Quilez, "smooth minimum" (https://iquilezles.org/articles/smin/): the cubic-polynomial + // smin, carried to value-and-gradient form (invariant 8). h is affine in (b.w - a.w) inside the + // smoothing band, so differentiating value = lerp(b.w, a.w, h) - k*h*(1-h) with respect to + // position, the terms carrying dh/dp cancel exactly (dh/dp * [(a.w-b.w) - k + 2*k*h] and the + // bracket is identically zero given how h is built from a.w-b.w). What is left is exactly + // lerp(∇b, ∇a, h): the gradient blend is not an approximation, it is the analytic gradient. + // Outside the band (|a.w - b.w| >= k), h saturates to 0 or 1 and this is exactly min(a, b) with + // that input's own gradient, continuously (h and the correction term both reach the boundary at + // the same value from the smooth side, so there is no kink to exclude here the way cellular's + // F1 = F2 seam needs one). + public static float4 smin_grad(float4 a, float4 b, float k) + { + var h = saturate(0.5f + 0.5f * (b.w - a.w) / k); + var value = lerp(b.w, a.w, h) - k * h * (1.0f - h); + var gradient = lerp(new float3(b.x, b.y, b.z), new float3(a.x, a.y, a.z), h); + return new float4(gradient, value); + } + + // Worley, "A Cellular Texture Basis Function" (SIGGRAPH 1996): F1/F2 and their analytic + // gradients, searched over the jittered 3x3x3 neighbourhood of feature points. Feature points + // are selected with pcg3d (design.md, "Integer hashing"), never the sin-based hash. The gradient + // of a distance field, del|p - c|, is the unit vector (p - c)/|p - c|; that is undefined exactly + // at a feature point (F1 = 0), so cellular follows the repo's existing degenerate-normal + // convention (GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle: guard the zero-length + // case and return the zero vector instead of the NaN a bare normalize would produce there). + public static CultCellular cellular(float3 p) + { + var cell = floor(p); + var f1 = 1.0e30f; + var f2 = 1.0e30f; + var c1 = float3.zero; + var c2 = float3.zero; + var hash1 = 0; + + for (var dz = -1; dz <= 1; dz++) + { + for (var dy = -1; dy <= 1; dy++) + { + for (var dx = -1; dx <= 1; dx++) + { + var neighbor = cell + new float3(dx, dy, dz); + var hash = pcg3d(neighbor); + var jitter = new float3(cellular_unit(hash.x), cellular_unit(hash.y), cellular_unit(hash.z)); + var feature = neighbor + jitter; + var d = distance(p, feature); + + if (d < f1) + { + f2 = f1; c2 = c1; + f1 = d; c1 = feature; hash1 = hash.x; + } + else if (d < f2) + { + f2 = d; c2 = feature; + } + } + } + } + + var grad1 = f1 > 0.0f ? (p - c1) / f1 : float3.zero; + var grad2 = f2 > 0.0f ? (p - c2) / f2 : float3.zero; + + return new CultCellular( + new float4(grad1, f1), + new float4(grad2 - grad1, f2 - f1), + cellular_unit(hash1)); + } + + // Maps a pcg3d output component's uint bit pattern to [0, 1) by the standard uint-to-float + // divide-by-2^32; the division is by an exact power of two, and the uint-to-float conversion is + // the same IEEE-754 round-to-nearest both dxc and C# use, so this is bit-exact on both sides. + private static float cellular_unit(int bits) => (uint)bits * (1.0f / 4294967296.0f); + public static float value_noise(float2 position) { var cell = floor(position);