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 <noreply@anthropic.com>
This commit is contained in:
parent
29e50ad315
commit
1dca43596e
4 changed files with 142 additions and 1 deletions
|
|
@ -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",
|
||||
<https://iquilezles.org/articles/smin/>), 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
|
||||
|
|
|
|||
25
packages/cultmath/src/CultMath/CultCellular.cs
Normal file
25
packages/cultmath/src/CultMath/CultCellular.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
namespace CultMath;
|
||||
|
||||
/// <summary>
|
||||
/// The value-and-gradient result of <see cref="math.cellular(float3)"/> (invariant 8's one struct
|
||||
/// return shape). Every field follows the same <c>float4(gradient.xyz, value.w)</c> layout as a
|
||||
/// bare value-and-gradient return.
|
||||
/// </summary>
|
||||
public struct CultCellular
|
||||
{
|
||||
/// <summary>(∇F1, F1): the gradient and distance to the nearest feature point.</summary>
|
||||
public float4 nearest;
|
||||
|
||||
/// <summary>(∇F2 - ∇F1, F2 - F1): for lineae along cell borders.</summary>
|
||||
public float4 edge;
|
||||
|
||||
/// <summary>The nearest cell's pcg3d hash, mapped to [0, 1).</summary>
|
||||
public float id;
|
||||
|
||||
public CultCellular(float4 nearest, float4 edge, float id)
|
||||
{
|
||||
this.nearest = nearest;
|
||||
this.edge = edge;
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
2
packages/cultmath/src/CultMath/CultCellular.cs.meta
Normal file
2
packages/cultmath/src/CultMath/CultCellular.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 76be9498b63e410891715b3ec965caf8
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue