Merge hands/cultmath-glsl-lowering: CultMath's interval library lowered to GLSL ES 3.00 (MIT) with Phacelle in its own MPL-2.0 file

The GLSL is generated from the HLSL by one lowering, mirrored against C# by a
committed fixture evaluated on its own arguments (portable across OSes), with
the frustum ball checked by two-sided enclosure and Phacelle by a scaled
absolute bound. design.md conflict: keeps the branch's sqrt-precision text and
drops the 6e-4 sentence interval-ops r4 removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-03 14:42:28 +02:00
commit bb583e89d5
17 changed files with 8989 additions and 39 deletions

View file

@ -47,6 +47,15 @@ shader semantics.
carved sphere): its interval over the segment proves empty space in one probe,
and the noise ball is consulted only near the surface. See `docs/design.md`,
"Intervals".
- `shaders/CultMath.glsl` (MIT), the same `cultmath_*` library in GLSL ES 3.00
for WebGL2, generated from `CultMath.hlsl` (never edited by hand), and
`shaders/CultMath.Phacelle.glsl` (MPL-2.0), generated from
`CultMath.Phacelle.hlsl`, which holds `cultmath_phacelle` alone. GLSL has no
`#include`: a WebGL consumer concatenates `CultMath.glsl` after its own
`#version 300 es` line and `precision highp float; precision highp int;`
(the library declares no precision, and its hashes need 32-bit integers),
then `CultMath.Phacelle.glsl` only if it calls `cultmath_phacelle`, then its
own shader source.
- `Voronoi.SampleTones`, a C# batch surface that calls the Rust
`cultmath-core` native kernel when `cultmath_core` is available and falls back
to the managed parity path otherwise.

View file

@ -35,7 +35,9 @@ gradient and the exact pruning are new here.
- License: MPL-2.0 (<https://mozilla.org/MPL/2.0/>). MPL-2.0 is file-level, so it
covers exactly the files that carry the port and nothing else:
`src/CultMath/math.Phacelle.cs`, `src/CultMath/CultPhasor.cs`,
`shaders/CultMath.Phacelle.hlsl` and the Unity copy of that include. Each
carries the MPL-2.0 header and this attribution. The rest of CultMath is MIT,
so the `GameCult.Math` NuGet package and the `org.gamecult.cultmath` Unity
package both declare `MIT AND MPL-2.0` and ship this file.
`shaders/CultMath.Phacelle.hlsl` and the Unity copy of that include, and
`shaders/CultMath.Phacelle.glsl`, which is generated from that include. Each
carries the MPL-2.0 header and this attribution. The rest of CultMath, the
generated GLSL library included, is MIT, so the `GameCult.Math` NuGet
package and the `org.gamecult.cultmath` Unity package both declare
`MIT AND MPL-2.0` and ship this file.

View file

@ -133,6 +133,118 @@ as its value. `CultCellular` (`math.cs`, `cellular`) and `CultPhasor`
retyped 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.
### GLSL Target: Source Transformations
`shaders/CultMath.glsl` is a GLSL ES 3.00 (WebGL2) library generated from
`shaders/CultMath.hlsl` by `GlslLowering` (in the test project), and
`shaders/CultMath.Phacelle.glsl` is generated beside it from
`shaders/CultMath.Phacelle.hlsl`. Both are derived files: never edit them by
hand. Regenerate them, and the golden fixture
`tests/CultMath.Tests/fixtures/glsl-parity.json`, with:
```powershell
$env:CULTMATH_WRITE_GLSL = "1"; dotnet test packages/cultmath/tests/CultMath.Tests --filter GlslMirrorTests
```
`GlslMirrorTests.CommittedGlslEqualsLowering` fails on any difference between
either committed file and the lowering of the committed HLSL. These
transformations, in order, are the complete list:
1. Each `#include "name"` is replaced in place by the included file, by the
same resolver the C# mirror uses; GLSL has no `#include`. The exception is
an include under a file-level licence of its own, listed in one table,
`GlslLowering.SeparateFiles`: its line is dropped and the file is lowered on
its own, by the same steps, into its own output. The table has one entry,
`CultMath.Phacelle.hlsl` to `CultMath.Phacelle.glsl`, because Phacelle is
MPL-2.0 and MPL-2.0 is file-level: kept separate, it leaves
`CultMath.glsl` MIT. Nothing else in the lowering is keyed by name.
2. Include guards are dropped, and so are the functions taking
`Texture2D`/`SamplerState`; the host shader samples its own textures.
3. Type names: `floatN`, `intN`, `uintN` and `boolN` become `vecN`, `ivecN`,
`uvecN` and `bvecN`.
4. File-scope `static const` becomes `const`.
5. C-style casts `(float)x`, `(int)x` and `(uint)x` become `float(x)`, `int(x)`
and `uint(x)`. The operand is found by a balanced-bracket scanner:
`(int)((state >> 28) + 4u)` nests parentheses.
6. Intrinsics are renamed: `lerp` to `mix`, `frac` to `fract`, `rsqrt` to
`inversesqrt`, `asuint` to `floatBitsToUint`, `asfloat` to
`uintBitsToFloat`, and `saturate(x)` becomes `clamp(x, 0.0, 1.0)`.
7. Float literals pass through unchanged.
8. Each output is wrapped in a guard named for its file: `CULTMATH_GLSL`,
`CULTMATH_PHACELLE_GLSL`.
9. The first line names the file as generated, its sources and its licence,
and says how to regenerate it; `CultMath.glsl`'s also says which
functions live in the separate file.
GLSL ES 3.00 is stricter than HLSL: no implicit conversions between int, uint
and float, no C-style casts, and no `step(genType, float)` overload. Those gaps
are closed in the HLSL, never in the lowering: the HLSL is written in the subset
both languages share, with edits that are legal HLSL and compute the same bits
(`step(h, float4(0.0, 0.0, 0.0, 0.0))` rather than `step(h, 0.0)`, `+ 4u` where
a uint is added, `(float)((uint)hash.x >> 8)` before a float multiply). The C#
mirror and the FXC and dxc compiles keep those edits honest. A body that needs a
transformation outside the list fails `GlslMirrorTests.NoHlslOnlyTokensSurvive`
rather than gaining a special case.
The library declares no precision. The host shader does, and it must be
`precision highp float; precision highp int;`: the PCG hashes need 32-bit
integers. `tools/compile-glsl.ps1` writes such a wrapper (its own `#version
300 es` line, the precision, the library by string concatenation, and one call
of every public function) and compiles it with glslang, which
`tools/get-glslang.ps1` fetches pinned. It runs twice: over `CultMath.glsl`
alone, and with `-Phacelle` over `CultMath.glsl` followed by
`CultMath.Phacelle.glsl`, the order a consumer that calls `cultmath_phacelle`
concatenates them in. Two negative controls must fail: `-Body` with an HLSL
token (`float x = lerp(0.0, 1.0, 0.5);`), and `-Body` calling
`cultmath_phacelle` without `-Phacelle`, which proves the MIT file does not
carry it. The Unity package ships neither GLSL file.
Parity runs in two legs, each with one owner: HLSL against C# by the mirror
test, bit for bit; GLSL against C# by the golden fixture, which records 256
seeded cases per function family as float32 bit patterns, evaluated by the
consumer on WebGL2 and read back. The families `pcg3d` and `pcg4d` are marked
exact; the float families are ulp-bounded, the bound measured on the device.
`iv_frustum_ball` is ulp-bounded, not exact: it calls `sqrt`, which GLSL ES
3.00 does not require to be correctly rounded, and WebGL2 compilers may
reassociate. On a GTX 1070 under ANGLE's D3D11 backend its radius differs from
C# by up to 2 ulp in 43 of 256 cases, sometimes smaller. What the march needs
is enclosure, and what culling needs is a ball no wider than its rounding
widening, so the family's fixture entry carries a `check` the consumer applies
to every case: from the case's arguments `(mx, my, z0, z1, fp, warp)`, compute
in double `zm = (z0 + z1) / 2`, the centre `c = (mx zm, my zm, zm)`, its
`|c|_1 = |mx zm| + |my zm| + |zm|`, the radius
`r = (z1 - z0) / 2 sqrt(mx^2 + my^2 + 1) + z1 fp + warp` and
`d = |GPU centre - c|`, and require
`r + d <= GPU radius <= r + d + 2^-19 (r + |c|_1)`. The lower side is
enclosure; the upper side is the bound `NoiseBoundTests` pins in C#, so a
lowering that inflates the ball fails too. Only those two bounds fail the
check; the largest ulp distance is reported as for any ulp-bounded family.
`GoldenFixtureMatchesCSharp` applies the same check to C#'s own results.
`GoldenFixtureMatchesCSharp` evaluates C# on the committed fixture's own
arguments. Arguments are drawn only when the fixture is regenerated
(`CULTMATH_WRITE_GLSL=1`), because the draw itself calls `MathF.Pow`, whose
bits differ by OS. `iv_exp` and `phacelle` take their C# results from the
platform's `exp`, `sin` and `cos`, which also differ by OS, so their entries
name the platform that generated them and the test compares them case by case
against the running platform, printing the largest distance. `iv_exp`
("ulp-bounded, platform exp") must agree within one ulp. `phacelle`
("ulp-bounded, platform exp, sin, cos; across platforms |diff| <= 2^-20
max(|v|, 1)") must agree within `2^-20 max(|v|, 1)` per component, `v` being
the fixture's value: its sums of `exp`, `sin` and `cos` cancel near zero, where
ulps mean nothing, and Windows differs from the Linux fixture by up to
`3.6e-7 max(|v|, 1)`. Every other family is compared as text. `phacelle` is in
`CultMath.Phacelle.glsl`, so the evaluator concatenates
`CultMath.Phacelle.glsl` after `CultMath.glsl` for that family, with
normalization 0.5. The site does not vendor the MPL file and skips the family.
Its evaluator is the cultmath-tapes follow-up `glsl-browser-parity-run`, owned
by the next CultMath parity cut: a repeatable CultLib WebGL2 run of every
fixture family, `phacelle` included. Until it lands, the one GLSL evaluation of
`phacelle` is a single probe (GTX 1070, ANGLE D3D11: within `3.1e-6 max(|v|, 1)`
of C#). Asura's `gpu-snoise-parity` is a different check, an FXC and Unity
readback of `snoise`, and evaluates no GLSL.
## Invariant 8: Value-and-Gradient Primitives
A primitive tagged invariant 8 returns its value and analytic gradient
@ -418,8 +530,13 @@ two roundings (`z_m`, then the product), so the float centre is within
axis length at `3u`, the product, `z1 footprintPerDepth`, the sum, `+ warp`), so
the exact radius is at most `(1 + 7u)` times the float one; the widening rounds
once more. `2^-20 = 16u` covers `8u (|c|_1 + radius)` twice over, and fma
contraction only removes roundings, so HLSL and GLSL compilers are covered by
the same bound. The caller rotates and translates the centre into world space and scales
contraction only removes roundings. The axis count assumes a correctly rounded
`sqrt`, which IEEE gives C#. GLSL ES 3.00 does not: it inherits `sqrt`'s
precision from `inversesqrt`, and D3D-backed compilers may reassociate. So the
bound is proven for C# and HLSL on IEEE hardware, and on GLSL it is measured:
on a GTX 1070 under ANGLE's D3D11 backend the fixture's 256 cases keep a
minimum relative slack of `1.98e-6` (about `33u`), the same as C#'s. The
fixture's enclosure check is what measures it on each device. The caller rotates and translates the centre into world space and scales
centre and radius by its noise frequency; the rounding of that transform is the
caller's. `NoiseBoundTests.TileBallEnclosesEveryRaySegment` checks 2,000 seeded
tiles x 64 points of the r2 domain and 2,000 tiles x 8 corner points of each of

View file

@ -0,0 +1,99 @@
// Generated from shaders/CultMath.Phacelle.hlsl by GlslLowering (packages/cultmath/tests/CultMath.Tests/GlslLowering.cs). Do not edit; regenerate with CULTMATH_WRITE_GLSL=1 dotnet test --filter GlslMirrorTests. MPL-2.0; requires CultMath.glsl before it.
#ifndef CULTMATH_PHACELLE_GLSL
#define CULTMATH_PHACELLE_GLSL
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
// the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Phacelle noise. Copyright (c) 2025 Rune Skovbo Johansen (Shadertoy t3dyWl; MPL-2.0), read from the
// C# port PhacelleNoise in lpmitchell/AdvancedTerrainErosion, converted to burstable C# by Luke
// Mitchell (2026). Modified by GameCult: 3D cells, a caller-supplied stripe wave vector, an exact
// gradient and exact pruning. This file is an MPL-2.0 file; CultMath.hlsl, which includes it after
// its own definitions of CULTMATH_TAU and cultmath_pcg3d, is MIT (see THIRD-PARTY-NOTICES.md).
// Invariant 8's struct return shape for cultmath_phacelle, compared field by field and bit for bit
// against CultMath.CultPhasor, same field order.
struct CultPhasor
{
vec4 cos;
vec4 sin;
};
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.Phacelle.cs) for the stripe wave
// vector, the weight, the normalization, the exact gradient and why the prune is exact. Mirrors
// the C# body operation for operation; the C#-only internal overload that switches the prune off
// has no shader twin.
CultPhasor cultmath_phacelle(vec3 p, vec3 side, float offset, float normalization)
{
vec3 cell = floor(p);
vec3 local = p - cell;
float phaseOffset = offset * CULTMATH_TAU;
float sumW = 0.0;
float sumCos = 0.0;
float sumSin = 0.0;
vec3 gradW = vec3(0.0, 0.0, 0.0);
vec3 gradCos = vec3(0.0, 0.0, 0.0);
vec3 gradSin = vec3(0.0, 0.0, 0.0);
for (int dz = -1; dz <= 2; dz++)
{
float gz = max(0.0, abs(local.z - float(dz)) - 0.5);
for (int dy = -1; dy <= 2; dy++)
{
float gy = max(0.0, abs(local.y - float(dy)) - 0.5);
for (int dx = -1; dx <= 2; dx++)
{
float gx = max(0.0, abs(local.x - float(dx)) - 0.5);
if (gx * gx + gy * gy + gz * gz >= 2.25)
continue;
vec3 gridStep = vec3(dx, dy, dz);
ivec3 hash = cultmath_pcg3d(ivec3(cell + gridStep));
vec3 jitter = vec3(
float(uint(hash.x) >> 8) * (1.0 / 16777216.0) - 0.5,
float(uint(hash.y) >> 8) * (1.0 / 16777216.0) - 0.5,
float(uint(hash.z) >> 8) * (1.0 / 16777216.0) - 0.5);
vec3 v = local - gridStep - jitter;
float falloff = exp(-2.0 * dot(v, v));
float w = max(0.0, falloff - 0.01111);
vec3 dw = w > 0.0 ? (-4.0 * falloff) * v : vec3(0.0, 0.0, 0.0);
float phase = dot(v, side) + phaseOffset;
float c = cos(phase);
float s = sin(phase);
sumW += w;
sumCos += w * c;
sumSin += w * s;
gradW += dw;
gradCos += dw * c - (w * s) * side;
gradSin += dw * s + (w * c) * side;
}
}
}
float invW = 1.0 / sumW;
float rawCos = sumCos * invW;
float rawSin = sumSin * invW;
vec3 gradRawCos = (gradCos - rawCos * gradW) * invW;
vec3 gradRawSin = (gradSin - rawSin * gradW) * invW;
float rawLength = sqrt(rawCos * rawCos + rawSin * rawSin);
float floorLength = 1.0 - normalization;
CultPhasor result;
if (rawLength > floorLength)
{
float outCos = rawCos / rawLength;
float outSin = rawSin / rawLength;
vec3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
result.cos = vec4((gradRawCos - outCos * gradLength) / rawLength, outCos);
result.sin = vec4((gradRawSin - outSin * gradLength) / rawLength, outSin);
}
else
{
result.cos = vec4(gradRawCos / floorLength, rawCos / floorLength);
result.sin = vec4(gradRawSin / floorLength, rawSin / floorLength);
}
return result;
}
#endif

View file

@ -49,9 +49,9 @@ CultPhasor cultmath_phacelle(float3 p, float3 side, float offset, float normaliz
float3 gridStep = float3(dx, dy, dz);
int3 hash = cultmath_pcg3d(int3(cell + gridStep));
float3 jitter = float3(
((uint)hash.x >> 8) * (1.0 / 16777216.0) - 0.5,
((uint)hash.y >> 8) * (1.0 / 16777216.0) - 0.5,
((uint)hash.z >> 8) * (1.0 / 16777216.0) - 0.5);
(float)((uint)hash.x >> 8) * (1.0 / 16777216.0) - 0.5,
(float)((uint)hash.y >> 8) * (1.0 / 16777216.0) - 0.5,
(float)((uint)hash.z >> 8) * (1.0 / 16777216.0) - 0.5);
float3 v = local - gridStep - jitter;
float falloff = exp(-2.0 * dot(v, v));
float w = max(0.0, falloff - 0.01111);

View file

@ -0,0 +1,660 @@
// Generated from shaders/CultMath.hlsl and CultMath.Interval.hlsl by GlslLowering (packages/cultmath/tests/CultMath.Tests/GlslLowering.cs). Do not edit; regenerate with CULTMATH_WRITE_GLSL=1 dotnet test --filter GlslMirrorTests. MIT. cultmath_phacelle is in CultMath.Phacelle.glsl, to be concatenated after this file by consumers that call it.
#ifndef CULTMATH_GLSL
#define CULTMATH_GLSL
// CultMath HLSL mirror.
//
// HLSL already owns the vec2/vec3/vec4 type names and most primitive
// intrinsics. This include names the CultMath semantic surface explicitly so
// shader code, C# wrappers, and Rust/native parity fixtures can share the same
// small vocabulary without local helper drift.
const float CULTMATH_PI = 3.14159265358979323846;
const float CULTMATH_TAU = 6.28318530717958647692;
const float CULTMATH_HALF_PI = 1.57079632679489661923;
float cultmath_radians(float degrees_value) { return degrees_value * (CULTMATH_PI / 180.0); }
float cultmath_degrees(float radians_value) { return radians_value * (180.0 / CULTMATH_PI); }
float cultmath_frac(float value) { return value - floor(value); }
vec2 cultmath_frac(vec2 value) { return value - floor(value); }
vec3 cultmath_frac(vec3 value) { return value - floor(value); }
vec4 cultmath_frac(vec4 value) { return value - floor(value); }
float cultmath_clamp(float value, float minimum, float maximum) { return min(max(value, minimum), maximum); }
vec2 cultmath_clamp(vec2 value, vec2 minimum, vec2 maximum) { return min(max(value, minimum), maximum); }
vec3 cultmath_clamp(vec3 value, vec3 minimum, vec3 maximum) { return min(max(value, minimum), maximum); }
vec4 cultmath_clamp(vec4 value, vec4 minimum, vec4 maximum) { return min(max(value, minimum), maximum); }
// DXIL Saturate is FMin(1, FMax(0, x)); dxc lowers min/max to the same FMin/FMax, NaN rules included.
float cultmath_saturate(float value) { return min(1.0, max(0.0, value)); }
vec2 cultmath_saturate(vec2 value) { return min(vec2(1.0, 1.0), max(vec2(0.0, 0.0), value)); }
vec3 cultmath_saturate(vec3 value) { return min(vec3(1.0, 1.0, 1.0), max(vec3(0.0, 0.0, 0.0), value)); }
vec4 cultmath_saturate(vec4 value) { return min(vec4(1.0, 1.0, 1.0, 1.0), max(vec4(0.0, 0.0, 0.0, 0.0), value)); }
float cultmath_lerp(float start, float end, float amount) { return start + (end - start) * amount; }
vec2 cultmath_lerp(vec2 start, vec2 end, vec2 amount) { return start + (end - start) * amount; }
vec3 cultmath_lerp(vec3 start, vec3 end, vec3 amount) { return start + (end - start) * amount; }
vec4 cultmath_lerp(vec4 start, vec4 end, vec4 amount) { return start + (end - start) * amount; }
float cultmath_step(float edge, float value) { return value < edge ? 0.0 : 1.0; }
vec2 cultmath_step(vec2 edge, vec2 value) { return vec2(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y)); }
vec3 cultmath_step(vec3 edge, vec3 value) { return vec3(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z)); }
vec4 cultmath_step(vec4 edge, vec4 value) { return vec4(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z), cultmath_step(edge.w, value.w)); }
float cultmath_smoothstep(float minimum, float maximum, float value)
{
float t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (3.0 - 2.0 * t);
}
vec2 cultmath_smoothstep(vec2 minimum, vec2 maximum, vec2 value)
{
vec2 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (vec2(3.0, 3.0) - vec2(2.0, 2.0) * t);
}
vec3 cultmath_smoothstep(vec3 minimum, vec3 maximum, vec3 value)
{
vec3 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (vec3(3.0, 3.0, 3.0) - vec3(2.0, 2.0, 2.0) * t);
}
vec4 cultmath_smoothstep(vec4 minimum, vec4 maximum, vec4 value)
{
vec4 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (vec4(3.0, 3.0, 3.0, 3.0) - vec4(2.0, 2.0, 2.0, 2.0) * t);
}
float cultmath_smoothstep01(float value) { return cultmath_smoothstep(0.0, 1.0, value); }
float cultmath_smootherstep(float value)
{
value = cultmath_saturate(value);
return value * value * value * (value * (value * 6.0 - 15.0) + 10.0);
}
vec2 cultmath_snoise_mod289(vec2 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
vec3 cultmath_snoise_mod289(vec3 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
vec4 cultmath_snoise_mod289(vec4 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
vec3 cultmath_snoise_permute(vec3 value) { return cultmath_snoise_mod289(((value * 34.0) + 10.0) * value); }
vec4 cultmath_snoise_permute(vec4 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(vec3) mirror.
float cultmath_snoise(vec3 value)
{
vec2 c = vec2(1.0 / 6.0, 1.0 / 3.0);
vec3 i = floor(value + dot(value, c.yyy));
vec3 x0 = value - i + dot(i, c.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + c.xxx;
vec3 x2 = x0 - i2 + c.yyy;
vec3 x3 = x0 - 0.5;
i = cultmath_snoise_mod289(i);
vec4 p = cultmath_snoise_permute(cultmath_snoise_permute(cultmath_snoise_permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
const float n_ = 0.142857142857;
vec3 ns = vec3(2.0 * n_, 0.5 * n_ - 1.0, n_);
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0, 0.0, 0.0, 0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 105.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
float cultmath_snoise(vec2 value)
{
vec4 c = vec4(
0.211324865405187,
0.366025403784439,
-0.577350269189626,
0.024390243902439);
vec2 i = floor(value + dot(value, c.yy));
vec2 x0 = value - i + dot(i, c.xx);
vec2 i1 = x0.x > x0.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = vec4(x0.x + c.x - i1.x, x0.y + c.x - i1.y, x0.x + c.z, x0.y + c.z);
i = cultmath_snoise_mod289(i);
vec3 p = cultmath_snoise_permute(
cultmath_snoise_permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
m *= m;
m *= m;
vec3 x = 2.0 * fract(p * c.w) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
vec3 g = vec3(
a0.x * x0.x + h.x * x0.y,
a0.y * x12.x + h.y * x12.y,
a0.z * x12.z + h.z * x12.w);
return 130.0 * dot(m, g);
}
// Analytic gradient of cultmath_snoise(vec3) (invariant 8), following webgl-noise
// src/noise3Dgrad.glsl (Ashima Arts / McEwan, MIT); same kernel, permutation and scale as
// cultmath_snoise(vec3) (stegu/webgl-noise 22434e04d7). See the C# math.snoise_grad comment for the
// derivation. Every local up through p0..p3 and x0..x3 is cultmath_snoise(vec3)'s own derivation
// verbatim.
vec4 cultmath_snoise_grad(vec3 value)
{
vec2 c = vec2(1.0 / 6.0, 1.0 / 3.0);
vec3 i = floor(value + dot(value, c.yyy));
vec3 x0 = value - i + dot(i, c.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + c.xxx;
vec3 x2 = x0 - i2 + c.yyy;
vec3 x3 = x0 - 0.5;
i = cultmath_snoise_mod289(i);
vec4 p = cultmath_snoise_permute(cultmath_snoise_permute(cultmath_snoise_permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
const float n_ = 0.142857142857;
vec3 ns = vec3(2.0 * n_, 0.5 * n_ - 1.0, n_);
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0, 0.0, 0.0, 0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m0 = max(0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
vec4 m2 = m0 * m0;
vec4 m3 = m2 * m0;
vec4 m4 = m2 * m2;
vec4 px = vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3));
float value2 = 105.0 * dot(m4, px);
vec3 grad = -8.0 * m3.x * x0 * px.x + m4.x * p0
+ -8.0 * m3.y * x1 * px.y + m4.y * p1
+ -8.0 * m3.z * x2 * px.z + m4.z * p2
+ -8.0 * m3.w * x3 * px.w + m4.w * p3;
grad *= 105.0;
return vec4(grad, value2);
}
// 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.
vec4 cultmath_fbm_grad(vec3 p, int octaves, float lacunarity, float gain)
{
octaves = clamp(octaves, 0, 16);
float amplitude = 1.0;
float frequency = 1.0;
float value = 0.0;
vec3 gradient = vec3(0.0, 0.0, 0.0);
for (int i = 0; i < octaves; i++)
{
vec4 n = cultmath_snoise_grad(p * frequency);
value += amplitude * n.w;
gradient += amplitude * frequency * n.xyz;
frequency *= lacunarity;
amplitude *= gain;
}
return vec4(gradient, value);
}
// Ridged multifractal noise (Musgrave, Texturing and Modeling, ch. 16); see the C# math.ridged_grad
// comment for the fold/sign derivation, the deliberate crease at n = 0, and why octaves is clamped.
vec4 cultmath_ridged_grad(vec3 p, int octaves, float lacunarity, float gain)
{
octaves = clamp(octaves, 0, 16);
float amplitude = 1.0;
float frequency = 1.0;
float value = 0.0;
vec3 gradient = vec3(0.0, 0.0, 0.0);
for (int i = 0; i < octaves; i++)
{
vec4 n = cultmath_snoise_grad(p * frequency);
float s = sign(n.w);
value += amplitude * (1.0 - abs(n.w));
gradient += -amplitude * frequency * s * n.xyz;
frequency *= lacunarity;
amplitude *= gain;
}
return vec4(gradient, value);
}
float cultmath_lengthsq(vec2 value) { return dot(value, value); }
float cultmath_lengthsq(vec3 value) { return dot(value, value); }
float cultmath_lengthsq(vec4 value) { return dot(value, value); }
float cultmath_distance(vec2 left, vec2 right) { return length(left - right); }
float cultmath_distance(vec3 left, vec3 right) { return length(left - right); }
float cultmath_distance(vec4 left, vec4 right) { return length(left - right); }
vec2 cultmath_reflect(vec2 incident, vec2 normal_value)
{
return incident - 2.0 * dot(normal_value, incident) * normal_value;
}
vec3 cultmath_reflect(vec3 incident, vec3 normal_value)
{
return incident - 2.0 * dot(normal_value, incident) * normal_value;
}
vec2 cultmath_rotate(vec2 value, float radians_value)
{
float s = sin(radians_value);
float c = cos(radians_value);
return vec2(value.x * c - value.y * s, value.x * s + value.y * c);
}
vec2 cultmath_rotate_degrees(vec2 value, float degrees_value)
{
return cultmath_rotate(value, cultmath_radians(degrees_value));
}
float cultmath_csum(vec2 value) { return value.x + value.y; }
float cultmath_csum(vec3 value) { return value.x + value.y + value.z; }
float cultmath_csum(vec4 value) { return value.x + value.y + value.z + value.w; }
float cultmath_decay(float source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
vec2 cultmath_decay(vec2 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
vec3 cultmath_decay(vec3 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
vec4 cultmath_decay(vec4 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float cultmath_damp(float start, float end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
vec2 cultmath_damp(vec2 start, vec2 end, float lambda_value, float dt)
{
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, vec2(amount, amount));
}
vec3 cultmath_damp(vec3 start, vec3 end, float lambda_value, float dt)
{
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, vec3(amount, amount, amount));
}
vec4 cultmath_damp(vec4 start, vec4 end, float lambda_value, float dt)
{
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, vec4(amount, amount, amount, amount));
}
float cultmath_catmullrom(float p0, float p1, float p2, float p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
vec2 cultmath_catmullrom(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
vec3 cultmath_catmullrom(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
vec4 cultmath_catmullrom(vec4 p0, vec4 p1, vec4 p2, vec4 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float cultmath_quadratic_bezier(float p0, float p1, float p2, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * p0 + 2.0 * inv * t * p1 + t * t * p2;
}
vec3 cultmath_quadratic_bezier(vec3 p0, vec3 p1, vec3 p2, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * p0 + 2.0 * inv * t * p1 + t * t * p2;
}
float cultmath_cubic_bezier(float p0, float p1, float p2, float p3, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * inv * p0 + 3.0 * inv * inv * t * p1 + 3.0 * inv * t * t * p2 + t * t * t * p3;
}
vec3 cultmath_cubic_bezier(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * inv * p0 + 3.0 * inv * inv * t * p1 + 3.0 * inv * t * t * p2 + t * t * t * p3;
}
float cultmath_hash(float value) { return cultmath_frac(sin(value) * 43758.5453); }
float cultmath_hash(vec2 value) { return cultmath_hash(dot(value, vec2(127.1, 311.7))); }
float cultmath_hash(vec3 value) { return cultmath_hash(dot(value, vec3(127.1, 311.7, 74.7))); }
// PCG hashes (Jarzynski and Olano, JCGT 2020), written over scalar uints so CultMath's ivec3/ivec4 carry the bits.
uint cultmath_pcg(uint value)
{
uint state = value * 747796405u + 2891336453u;
uint word = ((state >> int((state >> 28) + 4u)) ^ state) * 277803737u;
return (word >> 22) ^ word;
}
ivec3 cultmath_pcg3d(ivec3 value)
{
uint x = uint(value.x) * 1664525u + 1013904223u, y = uint(value.y) * 1664525u + 1013904223u, z = uint(value.z) * 1664525u + 1013904223u;
x += y * z; y += z * x; z += x * y;
x ^= x >> 16; y ^= y >> 16; z ^= z >> 16;
x += y * z; y += z * x; z += x * y;
return ivec3(int(x), int(y), int(z));
}
ivec3 cultmath_pcg3d(vec3 value) { return cultmath_pcg3d(ivec3(int(floatBitsToUint(value.x)), int(floatBitsToUint(value.y)), int(floatBitsToUint(value.z)))); }
ivec3 cultmath_pcg3d(vec2 value) { return cultmath_pcg3d(vec3(value, 0.0)); }
ivec4 cultmath_pcg4d(ivec4 value)
{
uint x = uint(value.x) * 1664525u + 1013904223u, y = uint(value.y) * 1664525u + 1013904223u;
uint z = uint(value.z) * 1664525u + 1013904223u, w = uint(value.w) * 1664525u + 1013904223u;
x += y * w; y += z * x; z += x * y; w += y * z;
x ^= x >> 16; y ^= y >> 16; z ^= z >> 16; w ^= w >> 16;
x += y * w; y += z * x; z += x * y; w += y * z;
return ivec4(int(x), int(y), int(z), int(w));
}
ivec4 cultmath_pcg4d(vec4 value) { return cultmath_pcg4d(ivec4(int(floatBitsToUint(value.x)), int(floatBitsToUint(value.y)), int(floatBitsToUint(value.z)), int(floatBitsToUint(value.w)))); }
// Inigo Quilez, "smooth minimum": quadratic-polynomial smin carried to value-and-gradient form. See
// math.smin_grad's comment (math.cs) for why the gradient blend is exact, not an approximation.
vec4 cultmath_smin_grad(vec4 a, vec4 b, float k)
{
float h = clamp(0.5 + 0.5 * (b.w - a.w) / k, 0.0, 1.0);
float value = mix(b.w, a.w, h) - k * h * (1.0 - h);
vec3 gradient = mix(b.xyz, a.xyz, h);
return vec4(gradient, value);
}
// Invariant 8's one struct return shape (design.md, "HLSL Target: Source Transformations"):
// compared field by field and bit for bit against CultMath.CultCellular, same field order.
struct CultCellular
{
vec4 nearest;
vec4 edge;
float id;
};
// Worley, "A Cellular Texture Basis Function" (SIGGRAPH 1996). See math.cellular's comment
// (math.cs) for the hashing, gradient, search-radius exactness proof, id source and
// degenerate-point conventions this mirrors exactly, including the 5x5x5 lower-bound prune.
CultCellular cultmath_cellular(vec3 p)
{
vec3 cell = floor(p);
float f1 = 1.0e30;
float f2 = 1.0e30;
vec3 c1 = vec3(0.0, 0.0, 0.0);
vec3 c2 = vec3(0.0, 0.0, 0.0);
vec3 cellId = vec3(0.0, 0.0, 0.0);
for (int dz = -2; dz <= 2; dz++)
{
float lz = max(0.0, abs(float(dz)) - 1.0);
for (int dy = -2; dy <= 2; dy++)
{
float ly = max(0.0, abs(float(dy)) - 1.0);
for (int dx = -2; dx <= 2; dx++)
{
float lx = max(0.0, abs(float(dx)) - 1.0);
float lowerBound = sqrt(lx * lx + ly * ly + lz * lz);
if (lowerBound >= f2)
continue;
vec3 neighbor = cell + vec3(dx, dy, dz);
ivec3 hash = cultmath_pcg3d(ivec3(neighbor));
vec3 jitter = vec3(
float(uint(hash.x) >> 8) * (1.0 / 16777216.0),
float(uint(hash.y) >> 8) * (1.0 / 16777216.0),
float(uint(hash.z) >> 8) * (1.0 / 16777216.0));
vec3 feature = neighbor + jitter;
float d = cultmath_distance(p, feature);
if (d < f1)
{
f2 = f1; c2 = c1;
f1 = d; c1 = feature; cellId = neighbor;
}
else if (d < f2)
{
f2 = d; c2 = feature;
}
}
}
}
// F1 = 0 and F2 = 0 share the same degenerate-gradient guard; see math.cellular's comment
// (math.cs) for why F2 = 0 is reachable past roughly |p| = 2^23.
vec3 grad1 = f1 > 0.0 ? (p - c1) / f1 : vec3(0.0, 0.0, 0.0);
vec3 grad2 = f2 > 0.0 ? (p - c2) / f2 : vec3(0.0, 0.0, 0.0);
// id comes from pcg4d over the winning cell's integer coordinate alone (same int-hashing rule
// as the jitter above), never from the pcg3d hash that produced its jitter (F4).
ivec4 idHash = cultmath_pcg4d(ivec4(ivec3(cellId), 0));
CultCellular result;
result.nearest = vec4(grad1, f1);
result.edge = vec4(grad2 - grad1, f2 - f1);
result.id = float(uint(idHash.w) >> 8) * (1.0 / 16777216.0);
return result;
}
// Interval arithmetic over vec2(lo, hi), lo <= hi, both finite. Every cultmath_iv_* function returns
// an interval containing f(x) for every x in its input intervals, mirroring math.Interval.cs bit for
// bit; see that file and docs/design.md ("Intervals") for each op's enclosure and its ulp widening.
// Included by CultMath.hlsl after its own definitions of cultmath_smoothstep and cultmath_snoise.
// Lipschitz constant of cultmath_snoise(vec3); provenance in math.Interval.cs (SNOISE_LIPSCHITZ).
const float CULTMATH_SNOISE_LIPSCHITZ = 10.099261;
vec2 cultmath_iv_point(float value) { return vec2(value, value); }
vec2 cultmath_iv_add(vec2 a, vec2 b) { return vec2(a.x + b.x, a.y + b.y); }
vec2 cultmath_iv_sub(vec2 a, vec2 b) { return vec2(a.x - b.y, a.y - b.x); }
vec2 cultmath_iv_neg(vec2 a) { return vec2(-a.y, -a.x); }
vec2 cultmath_iv_mul(vec2 a, vec2 b)
{
float p0 = a.x * b.x;
float p1 = a.x * b.y;
float p2 = a.y * b.x;
float p3 = a.y * b.y;
return vec2(min(min(p0, p1), min(p2, p3)), max(max(p0, p1), max(p2, p3)));
}
vec2 cultmath_iv_scale(vec2 a, float s) { return s >= 0.0 ? vec2(a.x * s, a.y * s) : vec2(a.y * s, a.x * s); }
vec2 cultmath_iv_abs(vec2 a)
{
if (a.x >= 0.0)
return a;
if (a.y <= 0.0)
return vec2(-a.y, -a.x);
return vec2(0.0, max(-a.x, a.y));
}
vec2 cultmath_iv_min(vec2 a, vec2 b) { return vec2(min(a.x, b.x), min(a.y, b.y)); }
vec2 cultmath_iv_max(vec2 a, vec2 b) { return vec2(max(a.x, b.x), max(a.y, b.y)); }
vec2 cultmath_iv_sqr(vec2 a)
{
float l = a.x * a.x;
float h = a.y * a.y;
if (a.x >= 0.0)
return vec2(l, h);
if (a.y <= 0.0)
return vec2(h, l);
return vec2(0.0, max(l, h));
}
vec2 cultmath_iv_sqrt(vec2 a) { return vec2(sqrt(max(a.x, 0.0)), sqrt(max(a.y, 0.0))); }
// Widened by one ulp on each side: exp is not required to be correctly rounded.
vec2 cultmath_iv_exp(vec2 a)
{
float lo = exp(a.x);
float hi = exp(a.y);
return vec2(lo > 0.0 ? uintBitsToFloat(floatBitsToUint(lo) - 1u) : 0.0, uintBitsToFloat(floatBitsToUint(hi) + 1u));
}
vec2 cultmath_iv_clamp(vec2 a, float minimum, float maximum)
{
return vec2(cultmath_clamp(a.x, minimum, maximum), cultmath_clamp(a.y, minimum, maximum));
}
vec2 cultmath_iv_saturate(vec2 a) { return vec2(cultmath_saturate(a.x), cultmath_saturate(a.y)); }
// The endpoints, ordered, widened by one ulp inside [0, 1]: the float32 evaluation of smoothstep is
// monotone only to within one ulp.
vec2 cultmath_iv_smoothstep(float minimum, float maximum, vec2 a)
{
float s0 = cultmath_smoothstep(minimum, maximum, a.x);
float s1 = cultmath_smoothstep(minimum, maximum, a.y);
float lo = min(s0, s1);
float hi = max(s0, s1);
return vec2(lo > 0.0 ? uintBitsToFloat(floatBitsToUint(lo) - 1u) : 0.0, hi < 1.0 ? uintBitsToFloat(floatBitsToUint(hi) + 1u) : 1.0);
}
// The natural interval extension of x + (y - x) * amount, which is how cultmath_lerp evaluates.
vec2 cultmath_iv_lerp(vec2 a, vec2 b, float amount) { return cultmath_iv_add(a, cultmath_iv_scale(cultmath_iv_sub(b, a), amount)); }
// [n - L r, n + L r] intersected with [-1, 1], n = cultmath_snoise(centre). One snoise, value only.
vec2 cultmath_iv_snoise_ball(vec3 centre, float radius)
{
float n = cultmath_snoise(centre);
float e = CULTMATH_SNOISE_LIPSCHITZ * radius;
return vec2(max(n - e, -1.0), min(n + e, 1.0));
}
// Octave sum of cultmath_iv_snoise_ball, compounding frequency and amplitude as the value-and-gradient fBm does.
vec2 cultmath_iv_fbm_ball(vec3 centre, float radius, int octaves, float lacunarity, float gain)
{
octaves = clamp(octaves, 0, 16);
float amplitude = 1.0;
float frequency = 1.0;
vec2 sum = vec2(0.0, 0.0);
for (int i = 0; i < octaves; i++)
{
sum = cultmath_iv_add(sum, cultmath_iv_scale(cultmath_iv_snoise_ball(centre * frequency, radius * abs(frequency)), amplitude));
frequency *= lacunarity;
amplitude *= gain;
}
return sum;
}
// A ball (xyz centre, w radius) enclosing every point of every ray of a screen tile over the depth segment
// [z0, z1], warped by at most warp. Camera frame: pinhole at the origin looking down +z, ray (m z, z).
// footprintPerDepth = N / (sqrt(2) f) for an N x N tile at focal length f. The radius is widened by
// (|c|_1 + radius) 2^-20, which covers the ball's own float32 rounding. Derivation in math.Interval.cs.
vec4 cultmath_iv_frustum_ball(vec2 centreSlope, float z0, float z1, float footprintPerDepth, float warp)
{
float zm = (z0 + z1) * 0.5;
float axis = sqrt(centreSlope.x * centreSlope.x + centreSlope.y * centreSlope.y + 1.0);
float cx = centreSlope.x * zm;
float cy = centreSlope.y * zm;
float radius = (z1 - z0) * 0.5 * axis + z1 * footprintPerDepth + warp;
radius += (abs(cx) + abs(cy) + abs(zm) + radius) * 9.5367431640625e-7;
return vec4(cx, cy, zm, radius);
}
float cultmath_value_noise(vec2 position)
{
vec2 cell = floor(position);
vec2 local = cultmath_frac(position);
vec2 u = local * local * (vec2(3.0, 3.0) - vec2(2.0, 2.0) * local);
float a = cultmath_hash(cell);
float b = cultmath_hash(cell + vec2(1.0, 0.0));
float c = cultmath_hash(cell + vec2(0.0, 1.0));
float d = cultmath_hash(cell + vec2(1.0, 1.0));
return cultmath_lerp(cultmath_lerp(a, b, u.x), cultmath_lerp(c, d, u.x), u.y);
}
float cultmath_value_noise_bicubic(vec2 position)
{
vec2 cell = floor(position);
vec2 local = cultmath_frac(position);
float y0 = cultmath_catmullrom(cultmath_hash(cell + vec2(-1.0, -1.0)), cultmath_hash(cell + vec2(0.0, -1.0)), cultmath_hash(cell + vec2(1.0, -1.0)), cultmath_hash(cell + vec2(2.0, -1.0)), local.x);
float y1 = cultmath_catmullrom(cultmath_hash(cell + vec2(-1.0, 0.0)), cultmath_hash(cell + vec2(0.0, 0.0)), cultmath_hash(cell + vec2(1.0, 0.0)), cultmath_hash(cell + vec2(2.0, 0.0)), local.x);
float y2 = cultmath_catmullrom(cultmath_hash(cell + vec2(-1.0, 1.0)), cultmath_hash(cell + vec2(0.0, 1.0)), cultmath_hash(cell + vec2(1.0, 1.0)), cultmath_hash(cell + vec2(2.0, 1.0)), local.x);
float y3 = cultmath_catmullrom(cultmath_hash(cell + vec2(-1.0, 2.0)), cultmath_hash(cell + vec2(0.0, 2.0)), cultmath_hash(cell + vec2(1.0, 2.0)), cultmath_hash(cell + vec2(2.0, 2.0)), local.x);
return cultmath_catmullrom(y0, y1, y2, y3, local.y);
}
#endif

View file

@ -111,7 +111,7 @@ float cultmath_snoise(float3 value)
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, 0.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;
@ -197,7 +197,7 @@ float4 cultmath_snoise_grad(float3 value)
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, 0.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;
@ -316,17 +316,20 @@ float cultmath_damp(float start, float end, float lambda_value, float dt)
float2 cultmath_damp(float2 start, float2 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, float2(amount, amount));
}
float3 cultmath_damp(float3 start, float3 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, float3(amount, amount, amount));
}
float4 cultmath_damp(float4 start, float4 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, float4(amount, amount, amount, amount));
}
float cultmath_catmullrom(float p0, float p1, float p2, float p3, float t)
@ -393,7 +396,7 @@ float cultmath_hash(float3 value) { return cultmath_hash(dot(value, float3(127.1
uint cultmath_pcg(uint value)
{
uint state = value * 747796405u + 2891336453u;
uint word = ((state >> (int)((state >> 28) + 4)) ^ state) * 277803737u;
uint word = ((state >> (int)((state >> 28) + 4u)) ^ state) * 277803737u;
return (word >> 22) ^ word;
}
@ -406,8 +409,8 @@ int3 cultmath_pcg3d(int3 value)
return int3((int)x, (int)y, (int)z);
}
int3 cultmath_pcg3d(float2 value) { return cultmath_pcg3d(float3(value, 0.0)); }
int3 cultmath_pcg3d(float3 value) { return cultmath_pcg3d(int3((int)asuint(value.x), (int)asuint(value.y), (int)asuint(value.z))); }
int3 cultmath_pcg3d(float2 value) { return cultmath_pcg3d(float3(value, 0.0)); }
int4 cultmath_pcg4d(int4 value)
{
@ -468,9 +471,9 @@ CultCellular cultmath_cellular(float3 p)
float3 neighbor = cell + float3(dx, dy, dz);
int3 hash = cultmath_pcg3d(int3(neighbor));
float3 jitter = float3(
((uint)hash.x >> 8) * (1.0 / 16777216.0),
((uint)hash.y >> 8) * (1.0 / 16777216.0),
((uint)hash.z >> 8) * (1.0 / 16777216.0));
(float)((uint)hash.x >> 8) * (1.0 / 16777216.0),
(float)((uint)hash.y >> 8) * (1.0 / 16777216.0),
(float)((uint)hash.z >> 8) * (1.0 / 16777216.0));
float3 feature = neighbor + jitter;
float d = cultmath_distance(p, feature);
@ -499,7 +502,7 @@ CultCellular cultmath_cellular(float3 p)
CultCellular result;
result.nearest = float4(grad1, f1);
result.edge = float4(grad2 - grad1, f2 - f1);
result.id = ((uint)idHash.w >> 8) * (1.0 / 16777216.0);
result.id = (float)((uint)idHash.w >> 8) * (1.0 / 16777216.0);
return result;
}

View file

@ -0,0 +1,179 @@
using System.Text;
using System.Text.RegularExpressions;
namespace CultMath.Tests;
/// <summary>
/// The HLSL to GLSL ES 3.00 transformation that produces <c>shaders/CultMath.glsl</c>, and one file per
/// <see cref="SeparateFiles"/> entry, from <c>shaders/CultMath.hlsl</c> and its includes. The steps below,
/// in order, are the complete list documented in docs/design.md (GLSL Target: Source Transformations).
/// A dialect gap outside the list is fixed in the HLSL, in the subset both languages share, never by a
/// special case here; GlslMirrorTests fails on any HLSL-only token that survives.
/// </summary>
internal static class GlslLowering
{
/// <summary>
/// Includes under a file-level licence of their own: each is lowered into its own output file instead
/// of being inlined, so CultMath.glsl stays MIT. This table is the lowering's only rule keyed by name.
/// </summary>
internal static readonly IReadOnlyDictionary<string, (string Glsl, string Licence)> SeparateFiles =
new Dictionary<string, (string, string)>
{
// Phacelle is a port of MPL-2.0 code, and MPL-2.0 covers the files that carry it.
["CultMath.Phacelle.hlsl"] = ("CultMath.Phacelle.glsl", "MPL-2.0"),
};
private const string Regenerate =
"by GlslLowering (packages/cultmath/tests/CultMath.Tests/GlslLowering.cs). Do not edit; regenerate with " +
"CULTMATH_WRITE_GLSL=1 dotnet test --filter GlslMirrorTests.";
/// <summary>
/// The lowered files as (name under shaders/, text): CultMath.glsl first, then one per
/// <see cref="SeparateFiles"/> entry, which requires CultMath.glsl before it.
/// </summary>
internal static IReadOnlyList<(string File, string Text)> Lower(string cultMathRoot)
{
// 1. Each #include is inlined in place by the C# mirror's own resolver, except a SeparateFiles
// include, which leaves nothing behind and is lowered into its own file.
var inlined = new List<string>();
var library = HlslSourceCompatibilityTests.ReadShaderSource(cultMathRoot, name =>
{
if (SeparateFiles.ContainsKey(name))
return false;
inlined.Add(name);
return true;
});
var outputs = new List<(string File, string Text)>();
var pointers = new List<string>();
foreach (var (hlsl, (glsl, licence)) in SeparateFiles)
{
var text = LowerText(
File.ReadAllText(Path.Combine(cultMathRoot, "shaders", hlsl)),
$"// Generated from shaders/{hlsl} {Regenerate} {licence}; requires CultMath.glsl before it.",
glsl);
var functions = Regex.Matches(text, @"(?m)^\w+\s+(cultmath_\w+)\s*\(").Select(m => m.Groups[1].Value);
pointers.Add($" {string.Join(", ", functions)} is in {glsl}, to be concatenated after this file by consumers that call it.");
outputs.Add((glsl, text));
}
var includes = string.Concat(inlined.Select(name => " and " + name));
outputs.Insert(0, ("CultMath.glsl", LowerText(
library, $"// Generated from shaders/CultMath.hlsl{includes} {Regenerate} MIT.{string.Concat(pointers)}", "CultMath.glsl")));
return outputs;
}
private static string LowerText(string hlsl, string provenance, string file)
{
var source = hlsl.ReplaceLineEndings("\n");
// 2. Include guards are dropped, and so are the functions taking Texture2D/SamplerState, exactly
// as the C# mirror drops them; the host shader samples its own textures.
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.
source = LowerTypeNames(source);
// 4. File-scope `static const` becomes `const`.
source = Regex.Replace(source, @"(?m)^static const ", "const ");
// 5. C-style casts (float)x, (int)x and (uint)x become constructor calls float(x), int(x) and
// uint(x). The operand is found by a balanced scanner, not a regex: (int)((state >> 28) + 4u)
// nests parentheses.
source = LowerCasts(source);
// 6. Intrinsic renames: lerp -> mix, frac -> fract, rsqrt -> inversesqrt, asuint ->
// floatBitsToUint, asfloat -> uintBitsToFloat, and saturate(x) -> clamp(x, 0.0, 1.0).
// Identifier boundaries keep cultmath_lerp and the rest of the cultmath_ names intact.
source = Regex.Replace(source, @"\blerp\(", "mix(");
source = Regex.Replace(source, @"\bfrac\(", "fract(");
source = Regex.Replace(source, @"\brsqrt\(", "inversesqrt(");
source = Regex.Replace(source, @"\basuint\(", "floatBitsToUint(");
source = Regex.Replace(source, @"\basfloat\(", "uintBitsToFloat(");
source = LowerSaturate(source);
// 7. Float literals pass through: HLSL's unsuffixed 0.5 and 1.0e30 are GLSL literals already.
// 8. The guard named for the file (CultMath.glsl -> CULTMATH_GLSL), and 9. the provenance line.
var guard = file.ToUpperInvariant().Replace('.', '_');
return provenance + "\n#ifndef " + guard + "\n#define " + guard + "\n" + source.Trim('\n') + "\n\n#endif\n";
}
/// <summary>Step 3 alone; GlslMirrorTests maps the HLSL signatures with it to compare overloads.</summary>
internal static string LowerTypeNames(string source)
{
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");
}
private static readonly Regex Cast = new(@"\((float|int|uint)\)");
private static string LowerCasts(string source)
{
for (var match = Cast.Match(source); match.Success; match = Cast.Match(source))
{
var start = match.Index + match.Length;
var end = OperandEnd(source, start);
// A parenthesized operand lends its own parentheses to the constructor call.
var operand = source[start] == '(' && Balanced(source, start) == end ? source[(start + 1)..(end - 1)] : source[start..end];
source = source[..match.Index] + match.Groups[1].Value + "(" + operand + ")" + source[end..];
}
return source;
}
// The cast operand: a parenthesized group, or an identifier or number followed by any member
// accesses, calls and indexing. Anything else is outside the list and throws.
private static int OperandEnd(string source, int index)
{
if (index < source.Length && source[index] == '(')
return Balanced(source, index);
var token = Regex.Match(source[index..], @"^[A-Za-z_0-9]+");
if (!token.Success)
throw new InvalidOperationException($"cast operand outside the lowering list at: {source.Substring(index, Math.Min(40, source.Length - index))}");
index += token.Length;
while (index < source.Length)
{
if (source[index] == '(' || source[index] == '[')
index = Balanced(source, index);
else if (source[index] == '.' && index + 1 < source.Length && (char.IsLetter(source[index + 1]) || source[index + 1] == '_'))
index += 1 + Regex.Match(source[(index + 1)..], @"^[A-Za-z_0-9]+").Length;
else
break;
}
return index;
}
// The index one past the bracket that closes the one at `open`.
private static int Balanced(string source, int open)
{
var depth = 0;
for (var i = open; i < source.Length; i++)
{
if (source[i] is '(' or '[')
depth++;
else if (source[i] is ')' or ']' && --depth == 0)
return i + 1;
}
throw new InvalidOperationException($"unbalanced bracket at {open}");
}
private static string LowerSaturate(string source)
{
var saturate = new Regex(@"\bsaturate\(");
for (var match = saturate.Match(source); match.Success; match = saturate.Match(source))
{
var open = match.Index + match.Length - 1;
var close = Balanced(source, open);
var argument = source[(open + 1)..(close - 1)];
source = source[..match.Index] + "clamp(" + argument + ", 0.0, 1.0)" + source[close..];
}
return source;
}
}

View file

@ -0,0 +1,419 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using CultMath;
using Xunit;
namespace CultMath.Tests;
/// <summary>
/// shaders/CultMath.glsl and shaders/CultMath.Phacelle.glsl are derived: each is exactly its
/// <see cref="GlslLowering.Lower"/> output from the committed HLSL, no HLSL-only token survives the
/// lowering, no public function is dropped except the texture samplers, the MPL-2.0 Phacelle code lives
/// only in its own file, and the golden fixture a WebGL2 consumer evaluates is C#'s current output. With
/// CULTMATH_WRITE_GLSL=1 set, the first and last tests write the committed files before comparing, which
/// is the one regeneration path.
/// </summary>
public sealed class GlslMirrorTests
{
private readonly ITestOutputHelper output;
public GlslMirrorTests(ITestOutputHelper output) => this.output = output;
private static bool Write => Environment.GetEnvironmentVariable("CULTMATH_WRITE_GLSL") == "1";
private static string Root => HlslSourceCompatibilityTests.FindCultMathRoot();
private static string FixturePath => Path.Combine(Root, "tests", "CultMath.Tests", "fixtures", "glsl-parity.json");
private static string Hlsl => HlslSourceCompatibilityTests.ReadShaderSource(Root).ReplaceLineEndings("\n");
private static IReadOnlyList<(string File, string Text)> Lowered => GlslLowering.Lower(Root);
[Fact]
public void CommittedGlslEqualsLowering()
{
foreach (var (file, expected) in Lowered)
{
var path = Path.Combine(Root, "shaders", file);
if (Write)
File.WriteAllText(path, expected);
var committed = File.ReadAllText(path).ReplaceLineEndings("\n");
Assert.True(committed == expected, $"shaders/{file} is not the lowering of the committed HLSL (regenerate with CULTMATH_WRITE_GLSL=1):\n" + UnifiedDiff(committed, expected));
}
}
// 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\(|static const|\((float|int|uint)\)|Texture2D|SamplerState|#include|precision (high|medium|low)p");
[Fact]
public void NoHlslOnlyTokensSurvive()
{
foreach (var (file, text) in Lowered)
{
var survivors = HlslOnly.Matches(text).Select(m => m.Value).Distinct().ToArray();
Assert.True(survivors.Length == 0, $"HLSL-only tokens in {file}: " + string.Join(", ", survivors));
}
}
private static readonly Regex Definition = new(@"(?m)^\w+\s+(cultmath_\w+)\s*\(([^)]*)\)");
// A definition's signature, its name and parameter types, so an overload counts on its own:
// "cultmath_pcg3d(vec2)". HLSL type names are mapped by the lowering's own step 3.
private static string Signature(Match definition) =>
definition.Groups[1].Value + "(" + string.Join(", ", definition.Groups[2].Value
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.Select(parameter => GlslLowering.LowerTypeNames(string.Join(' ', Regex.Split(parameter, @"\s+")[..^1])))) + ")";
[Fact]
public void EveryPublicFunctionIsLowered()
{
var lowered = Lowered;
var hlsl = Definition.Matches(Hlsl).Where(m => !Regex.IsMatch(m.Groups[2].Value, @"\b(Texture2D|SamplerState)\b"))
.Select(Signature).ToHashSet();
var glsl = lowered.SelectMany(output => Definition.Matches(output.Text)).Select(Signature).ToHashSet();
Assert.Contains("cultmath_pcg3d(vec2)", glsl);
Assert.True(hlsl.SetEquals(glsl), "HLSL only: " + string.Join(", ", hlsl.Except(glsl)) + "; GLSL only: " + string.Join(", ", glsl.Except(hlsl)));
// The MPL-2.0 code is in its own file and nowhere in the MIT library.
var library = lowered.Single(output => output.File == "CultMath.glsl").Text;
var phacelle = lowered.Single(output => output.File == "CultMath.Phacelle.glsl").Text;
foreach (var token in new[] { "cultmath_phacelle(", "CultPhasor" })
{
Assert.DoesNotContain(token, library);
Assert.Contains(token, phacelle);
}
}
// The two-file contract a consumer relies on when it concatenates CultMath.Phacelle.glsl after
// CultMath.glsl: distinct guards (a shared one would hide the second file's body), each provenance
// line naming its file's licence, and the MIT file pointing at the one that holds cultmath_phacelle.
[Fact]
public void TwoFileHeaderContract()
{
var lowered = Lowered;
var guards = lowered.Select(output => Regex.Match(output.Text, @"^[^\n]*\n#ifndef (\w+)\n#define \1\n").Groups[1].Value).ToArray();
Assert.All(guards, guard => Assert.NotEqual(string.Empty, guard));
Assert.Equal(guards.Length, guards.Distinct().Count());
var library = lowered.Single(output => output.File == "CultMath.glsl").Text.Split('\n')[0];
var phacelle = lowered.Single(output => output.File == "CultMath.Phacelle.glsl").Text.Split('\n')[0];
Assert.Contains(" MIT.", library);
Assert.DoesNotContain("MPL", library);
Assert.Contains("cultmath_phacelle is in CultMath.Phacelle.glsl", library);
Assert.Contains(" MPL-2.0;", phacelle);
Assert.DoesNotContain("MIT", phacelle);
}
[Fact]
public void GoldenFixtureMatchesCSharp()
{
// Arguments are drawn only here, when regenerating: Interval draws widths through MathF.Pow, whose
// bits differ by OS. Checking evaluates C# on the committed fixture's own arguments.
if (Write)
{
var random = new System.Random(Seed);
Directory.CreateDirectory(Path.GetDirectoryName(FixturePath)!);
File.WriteAllText(FixturePath, GoldenFixture((f, _) => Families[f].Arguments(random)));
}
var committed = JsonNode.Parse(File.ReadAllText(FixturePath))!.AsObject();
var committedFamilies = committed["functions"]!.AsArray();
Assert.Equal(Families.Length, committedFamilies.Count);
for (var f = 0; f < Families.Length; f++)
Assert.True(committedFamilies[f]!["cases"]!.AsArray().Count == Points, $"{Families[f].Glsl}: the fixture does not hold {Points} cases");
var current = JsonNode.Parse(GoldenFixture((f, p) =>
Arguments(Resolve(Families[f].Glsl), ((string)committedFamilies[f]!["cases"]![p]!).Split(" -> ")[0])))!.AsObject();
var currentFamilies = current["functions"]!.AsArray();
// Family by family: every family is compared as text, except those whose C# bits come from the
// platform's libm, which are compared case by case within their family's platform bound. A family
// with a check must also pass it on C#'s own results, so the rule the WebGL2 consumer applies holds
// for the reference.
for (var f = 0; f < Families.Length; f++)
{
var want = currentFamilies[f]!.AsObject();
var have = committedFamilies[f]!.AsObject();
var name = (string)want["glsl"]!;
Assert.Equal(name, (string?)have["glsl"]);
Assert.Equal((string)want["tolerance"]!, (string?)have["tolerance"]);
Assert.Equal((string?)want["check"], (string?)have["check"]);
switch (Families[f].Platform)
{
case Platform.Ulp:
var ulps = MaxDistance(have["cases"]!.AsArray(), want["cases"]!.AsArray(), (x, y) => Math.Abs((long)Ordered(x) - Ordered(y)));
output.WriteLine($"{name}: within {ulps} ulp of this platform's libm ({RuntimeInformation.OSDescription}); fixture generated on {have["platform"]}");
Assert.True(ulps <= 1, $"{name}: {ulps} ulp from this platform's result, more than 1");
break;
case Platform.Scaled:
var scaled = MaxDistance(have["cases"]!.AsArray(), want["cases"]!.AsArray(), (x, y) =>
{
double v = BitConverter.Int32BitsToSingle(x), w = BitConverter.Int32BitsToSingle(y);
return x == y ? 0.0 : Math.Abs(v - w) / Math.Max(Math.Abs(v), 1.0);
});
output.WriteLine($"{name}: |diff| / max(|v|, 1) at most {scaled:R} from this platform's libm ({RuntimeInformation.OSDescription}); fixture generated on {have["platform"]}");
Assert.True(scaled <= ScaledPlatformBound, $"{name}: {scaled:R} max(|v|, 1) from this platform's result, more than 2^-20");
break;
default:
Assert.True(JsonNode.DeepEquals(want, have), $"fixtures/glsl-parity.json family {name} is not C#'s current output (regenerate with CULTMATH_WRITE_GLSL=1)");
break;
}
if (want["check"] is not null)
AssertFrustumBall(want["cases"]!.AsArray());
}
committed.Remove("functions");
current.Remove("functions");
Assert.True(JsonNode.DeepEquals(current, committed), "fixtures/glsl-parity.json header is not the generator's (regenerate with CULTMATH_WRITE_GLSL=1)");
}
// The largest distance, component by component, between two families' results (committed first).
private static double MaxDistance(JsonArray committed, JsonArray current, Func<int, int, double> distance)
{
var max = 0.0;
for (var c = 0; c < current.Count; c++)
{
var haveBits = ((string)committed[c]!).Split(" -> ")[1].Split(' ').Select(h => (int)Convert.ToUInt32(h, 16)).ToArray();
var wantBits = ((string)current[c]!).Split(" -> ")[1].Split(' ').Select(h => (int)Convert.ToUInt32(h, 16)).ToArray();
Assert.Equal(wantBits.Length, haveBits.Length);
max = Math.Max(max, haveBits.Zip(wantBits, distance).Max());
}
return max;
}
// FrustumBall applied to C#'s own results: every case's ball encloses the exact one and is no wider
// than its rounding widening allows.
private static void AssertFrustumBall(JsonArray cases)
{
foreach (var entry in cases)
{
var sides = ((string)entry!).Split(" -> ");
var a = Floats(sides[0]);
var b = Floats(sides[1]);
double mx = a[0], my = a[1], z0 = a[2], z1 = a[3], footprint = a[4], warp = a[5];
var zm = (z0 + z1) / 2.0;
var radius = (z1 - z0) / 2.0 * Math.Sqrt(mx * mx + my * my + 1.0) + z1 * footprint + warp;
double dx = b[0] - mx * zm, dy = b[1] - my * zm, dz = b[2] - zm;
var reach = radius + Math.Sqrt(dx * dx + dy * dy + dz * dz);
var c1 = Math.Abs(mx * zm) + Math.Abs(my * zm) + Math.Abs(zm);
Assert.True(b[3] >= reach, $"iv_frustum_ball case {entry}: radius short of the exact ball by {reach - b[3]}");
Assert.True(b[3] <= reach + Math.ScaleB(radius + c1, -19), $"iv_frustum_ball case {entry}: radius past the exact ball by {b[3] - reach}, more than 2^-19 (r + |c|_1)");
}
}
// A committed case's arguments, decoded into the C# method's parameter types in Hex's field order.
private static object[] Arguments(MethodInfo method, string hex)
{
var scalars = new Queue<uint>(hex.Split(' ').Select(h => Convert.ToUInt32(h, 16)));
var arguments = method.GetParameters().Select(parameter => FromScalars(parameter.ParameterType, scalars)).ToArray();
Assert.Empty(scalars);
return arguments;
}
private static object FromScalars(Type type, Queue<uint> scalars)
{
if (type == typeof(float))
return BitConverter.Int32BitsToSingle((int)scalars.Dequeue());
if (type == typeof(int))
return (int)scalars.Dequeue();
if (type == typeof(uint))
return scalars.Dequeue();
var value = Activator.CreateInstance(type)!;
foreach (var field in type.GetFields().Where(f => !f.IsStatic))
field.SetValue(value, FromScalars(field.FieldType, scalars));
return value;
}
private static double[] Floats(string hex) =>
hex.Split(' ').Select(h => (double)BitConverter.Int32BitsToSingle((int)Convert.ToUInt32(h, 16))).ToArray();
// A float32 bit pattern as an integer whose order and differences are the floats' order and ulps.
private static int Ordered(int bits) => bits < 0 ? int.MinValue - bits : bits;
// ---- The golden fixture ----
private const int Seed = 20261002;
private const int Points = 256;
// How GoldenFixtureMatchesCSharp compares a family whose C# bits come from the platform's libm (exp,
// sin, cos) and so differ by OS; such an entry records the platform that generated it. Every other
// family is compared as text.
private enum Platform { None, Ulp, Scaled }
// Platform.Scaled: |diff| <= 2^-20 max(|v|, 1) per component. Ulps mean nothing near zero, where
// phacelle's sums of exp, sin and cos cancel; Windows differs from Linux by up to 3.6e-7 of it.
private const double ScaledPlatformBound = 9.5367431640625e-7;
private sealed record Family(string Glsl, string Tolerance, Func<System.Random, object[]> Arguments, string? Check = null, Platform Platform = Platform.None);
private static float Uniform(System.Random r, float lo, float hi) => lo + (hi - lo) * r.NextSingle();
private static float3 Point3(System.Random r) => new(Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f));
private static int Bits(System.Random r) => r.Next(int.MinValue, int.MaxValue);
private static float2 Interval(System.Random r)
{
var centre = Uniform(r, -20.0f, 20.0f);
var width = MathF.Pow(10.0f, Uniform(r, -4.0f, 1.0f));
return new float2(centre - width * 0.5f, centre + width * 0.5f);
}
// iv_frustum_ball's sqrt is not correctly rounded on WebGL2 and its compilers may reassociate, so its
// bits are not portable; what the march needs is enclosure, and what culling needs is a ball no wider
// than its rounding widening. The consumer checks both in double from each case's arguments. It is
// written into the family's fixture entry, where the consumer reads it.
private const string FrustumBall =
"enclosure and tightness: from the arguments (mx, my, z0, z1, fp, warp) compute in double zm = (z0 + z1) / 2, " +
"c = (mx zm, my zm, zm), |c|_1 = |mx zm| + |my zm| + |zm|, r = (z1 - z0) / 2 sqrt(mx^2 + my^2 + 1) + z1 fp + warp " +
"and d = |GPU centre - c| (Euclidean); every case must have r + d <= GPU radius <= r + d + 2^-19 (r + |c|_1). " +
"Report the largest ulp distance per component as for any ulp-bounded family; only the two bounds fail the check.";
// A Phacelle stripe wave vector: each component in [-6, 6], about one stripe per cell.
private static float3 Side(System.Random r) => new(Uniform(r, -6.0f, 6.0f), Uniform(r, -6.0f, 6.0f), Uniform(r, -6.0f, 6.0f));
// Every family the site's WebGL2 readback evaluates, in a fixed order. A new family is appended, so the
// seeded stream and every earlier family's cases stay as they were. pcg3d and pcg4d are "exact"; the
// float families are ulp-bounded, with the bound measured on the device by the consumer, and
// iv_frustum_ball also carries an enclosure check. phacelle is in CultMath.Phacelle.glsl, so its
// evaluator concatenates that file after CultMath.glsl; the site, which does not vendor it, skips it.
private static readonly Family[] Families =
{
new("float cultmath_snoise(vec3)", "ulp-bounded", r => new object[] { Point3(r) }),
new("float cultmath_snoise(vec2)", "ulp-bounded", r => new object[] { new float2(Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f)) }),
new("vec4 cultmath_snoise_grad(vec3)", "ulp-bounded", r => new object[] { Point3(r) }),
new("vec4 cultmath_fbm_grad(vec3, int, float, float)", "ulp-bounded", r => new object[] { Point3(r), 4, 2.0f, 0.5f }),
new("vec4 cultmath_ridged_grad(vec3, int, float, float)", "ulp-bounded", r => new object[] { Point3(r), 4, 2.0f, 0.5f }),
new("CultCellular cultmath_cellular(vec3)", "ulp-bounded", r => new object[] { Point3(r) }),
new("ivec3 cultmath_pcg3d(ivec3)", "exact", r => new object[] { new int3(Bits(r), Bits(r), Bits(r)) }),
new("ivec4 cultmath_pcg4d(ivec4)", "exact", r => new object[] { new int4(Bits(r), Bits(r), Bits(r), Bits(r)) }),
new("vec2 cultmath_iv_point(float)", "ulp-bounded", r => new object[] { Uniform(r, -20.0f, 20.0f) }),
new("vec2 cultmath_iv_add(vec2, vec2)", "ulp-bounded", r => new object[] { Interval(r), Interval(r) }),
new("vec2 cultmath_iv_sub(vec2, vec2)", "ulp-bounded", r => new object[] { Interval(r), Interval(r) }),
new("vec2 cultmath_iv_neg(vec2)", "ulp-bounded", r => new object[] { Interval(r) }),
new("vec2 cultmath_iv_mul(vec2, vec2)", "ulp-bounded", r => new object[] { Interval(r), Interval(r) }),
new("vec2 cultmath_iv_scale(vec2, float)", "ulp-bounded", r => new object[] { Interval(r), Uniform(r, -4.0f, 4.0f) }),
new("vec2 cultmath_iv_abs(vec2)", "ulp-bounded", r => new object[] { Interval(r) }),
new("vec2 cultmath_iv_min(vec2, vec2)", "ulp-bounded", r => new object[] { Interval(r), Interval(r) }),
new("vec2 cultmath_iv_max(vec2, vec2)", "ulp-bounded", r => new object[] { Interval(r), Interval(r) }),
new("vec2 cultmath_iv_sqr(vec2)", "ulp-bounded", r => new object[] { Interval(r) }),
new("vec2 cultmath_iv_sqrt(vec2)", "ulp-bounded", r => new object[] { Interval(r) }),
new("vec2 cultmath_iv_clamp(vec2, float, float)", "ulp-bounded", r => new object[] { Interval(r), Uniform(r, -25.0f, 0.0f), Uniform(r, 0.0f, 25.0f) }),
new("vec2 cultmath_iv_saturate(vec2)", "ulp-bounded", r => new object[] { Interval(r) }),
new("vec2 cultmath_iv_smoothstep(float, float, vec2)", "ulp-bounded", r => new object[] { Uniform(r, -25.0f, 0.0f), Uniform(r, 0.0f, 25.0f), Interval(r) }),
new("vec2 cultmath_iv_lerp(vec2, vec2, float)", "ulp-bounded", r => new object[] { Interval(r), Interval(r), Uniform(r, 0.0f, 1.0f) }),
new("vec2 cultmath_iv_snoise_ball(vec3, float)", "ulp-bounded", r => new object[] { Point3(r), Uniform(r, 0.001f, 2.0f) }),
new("vec2 cultmath_iv_fbm_ball(vec3, float, int, float, float)", "ulp-bounded", r => new object[] { Point3(r), Uniform(r, 0.001f, 2.0f), 4, 2.0f, 0.5f }),
new("vec4 cultmath_iv_frustum_ball(vec2, float, float, float, float)", "ulp-bounded", r =>
{
var slope = new float2(Uniform(r, -1.0f, 1.0f), Uniform(r, -1.0f, 1.0f));
var z0 = Uniform(r, 0.1f, 100.0f);
return new object[] { slope, z0, z0 + Uniform(r, 0.01f, 50.0f), Uniform(r, 0.0001f, 0.05f), Uniform(r, 0.0f, 2.0f) };
}, FrustumBall),
new("vec2 cultmath_iv_exp(vec2)", "ulp-bounded, platform exp", r => new object[] { Interval(r) }, Platform: Platform.Ulp),
new("CultPhasor cultmath_phacelle(vec3, vec3, float, float)", "ulp-bounded, platform exp, sin, cos; across platforms |diff| <= 2^-20 max(|v|, 1)",
r => new object[] { Point3(r), Side(r), Uniform(r, 0.0f, 1.0f), 0.5f }, Platform: Platform.Scaled),
};
// The fixture text, each case's arguments given by family and point index.
private static string GoldenFixture(Func<int, int, object[]> arguments)
{
var json = new StringBuilder();
json.Append("{\n");
json.Append(" \"generator\": \"GlslMirrorTests.GoldenFixtureMatchesCSharp (packages/cultmath/tests/CultMath.Tests); regenerate with CULTMATH_WRITE_GLSL=1\",\n");
json.Append($" \"seed\": {Seed},\n");
json.Append($" \"points\": {Points},\n");
json.Append(" \"encoding\": \"each case is 'arguments -> result', every scalar a float32 or 32-bit integer bit pattern in hex, vectors and structs flattened in field order\",\n");
json.Append(" \"functions\": [\n");
for (var f = 0; f < Families.Length; f++)
{
var family = Families[f];
var method = Resolve(family.Glsl);
json.Append(" {\n");
json.Append($" \"glsl\": \"{family.Glsl}\",\n");
json.Append($" \"tolerance\": \"{family.Tolerance}\",\n");
if (family.Check is not null)
json.Append($" \"check\": \"{family.Check}\",\n");
if (family.Platform != Platform.None)
json.Append($" \"platform\": \"{RuntimeInformation.OSDescription}; {RuntimeInformation.FrameworkDescription}\",\n");
json.Append(" \"cases\": [\n");
for (var p = 0; p < Points; p++)
{
var values = arguments(f, p);
var result = method.Invoke(null, values)!;
json.Append(" \"").Append(Hex(values)).Append(" -> ").Append(Hex(new[] { result })).Append('"');
json.Append(p + 1 < Points ? ",\n" : "\n");
}
json.Append(" ]\n");
json.Append(f + 1 < Families.Length ? " },\n" : " }\n");
}
json.Append(" ]\n}\n");
return json.ToString();
}
private static readonly Dictionary<string, Type> GlslTypes = new()
{
["float"] = typeof(float), ["int"] = typeof(int), ["vec2"] = typeof(float2), ["vec3"] = typeof(float3),
["vec4"] = typeof(float4), ["ivec3"] = typeof(int3), ["ivec4"] = typeof(int4),
};
// The C# math method a GLSL signature names: cultmath_ dropped, parameter types mapped.
private static MethodInfo Resolve(string glsl)
{
var match = Regex.Match(glsl, @"^\w+ cultmath_(\w+)\(([^)]*)\)$");
var types = match.Groups[2].Value.Split(", ").Select(t => GlslTypes[t]).ToArray();
return typeof(math).GetMethod(match.Groups[1].Value, BindingFlags.Public | BindingFlags.Static, null, types, null)
?? throw new InvalidOperationException("no C# math counterpart for " + glsl);
}
private static string Hex(IEnumerable<object> values) => string.Join(" ", values.SelectMany(Scalars).Select(v => v switch
{
float f => BitConverter.SingleToInt32Bits(f).ToString("x8"),
int i => i.ToString("x8"),
uint u => u.ToString("x8"),
_ => throw new InvalidOperationException(v.GetType().Name),
}));
private static IEnumerable<object> Scalars(object value) => value is float or int or uint
? new[] { value }
: value.GetType().GetFields().Where(f => !f.IsStatic).SelectMany(f => Scalars(f.GetValue(value)!));
// ---- A small unified diff for the failure message ----
private static string UnifiedDiff(string before, string after)
{
var a = before.Split('\n');
var b = after.Split('\n');
if ((long)a.Length * b.Length > 4_000_000)
{
var first = Enumerable.Range(0, Math.Min(a.Length, b.Length)).FirstOrDefault(i => a[i] != b[i], Math.Min(a.Length, b.Length));
return $"@@ first difference at line {first + 1} @@\n-{(first < a.Length ? a[first] : "")}\n+{(first < b.Length ? b[first] : "")}";
}
var lcs = new int[a.Length + 1, b.Length + 1];
for (var i = a.Length - 1; i >= 0; i--)
for (var j = b.Length - 1; j >= 0; j--)
lcs[i, j] = a[i] == b[j] ? lcs[i + 1, j + 1] + 1 : Math.Max(lcs[i + 1, j], lcs[i, j + 1]);
var lines = new List<string> { "--- committed", "+++ lowering" };
int x = 0, y = 0;
while (x < a.Length || y < b.Length)
{
if (x < a.Length && y < b.Length && a[x] == b[y]) { x++; y++; continue; }
lines.Add($"@@ -{x + 1} +{y + 1} @@");
while (x < a.Length && (y >= b.Length || lcs[x + 1, y] >= lcs[x, y + 1]) && !(y < b.Length && a[x] == b[y]))
lines.Add("-" + a[x++]);
while (y < b.Length && (x >= a.Length || a[x] != b[y]))
lines.Add("+" + b[y++]);
if (lines.Count > 200)
break;
}
return string.Join("\n", lines.Take(200));
}
}

View file

@ -315,13 +315,17 @@ public sealed class HlslSourceCompatibilityTests
/// <summary>
/// The text of <c>CultMath.hlsl</c> with each <c>#include "name"</c> replaced in place by the
/// included file from the same directory, as the shader compiler would resolve it. Only the
/// package's own includes exist, so a missing file is an error rather than a skip.
/// package's own includes exist, so a missing file is an error rather than a skip. An include for
/// which <paramref name="inline"/> returns false is dropped with its line instead (the GLSL
/// lowering's separately licensed files); the C# mirror inlines every one.
/// </summary>
internal static string ReadShaderSource(string cultMathRoot)
internal static string ReadShaderSource(string cultMathRoot, Func<string, bool>? inline = null)
{
var shaders = Path.Combine(cultMathRoot, "shaders");
return Regex.Replace(File.ReadAllText(Path.Combine(shaders, "CultMath.hlsl")), @"(?m)^#include ""([^""]+)""[ \t]*\r?$",
match => File.ReadAllText(Path.Combine(shaders, match.Groups[1].Value)));
return Regex.Replace(File.ReadAllText(Path.Combine(shaders, "CultMath.hlsl")), @"(?m)^#include ""([^""]+)""[ \t]*\r?$(\n?)",
match => inline is null || inline(match.Groups[1].Value)
? File.ReadAllText(Path.Combine(shaders, match.Groups[1].Value)) + match.Groups[2].Value
: string.Empty);
}
/// <summary>The documented HLSL-to-C# transformations, and nothing else.</summary>
@ -374,7 +378,7 @@ public sealed class HlslSourceCompatibilityTests
return (result.Success ? Assembly.Load(stream.ToArray()) : null, errors);
}
private static string FindCultMathRoot()
internal static string FindCultMathRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "packages", "cultmath", "shaders", "CultMath.hlsl")))

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
param(
# A GLSL source to append after the generated wrapper's library instead of the call-every-function
# body; tools\compile-glsl.ps1 -Body 'float x = lerp(0.0, 1.0, 0.5);' is the negative control.
[string]$Body = "",
# Also concatenate shaders\CultMath.Phacelle.glsl (MPL-2.0) after CultMath.glsl, as a consumer that
# calls cultmath_phacelle does. Without it the wrapper is the MIT library alone.
[switch]$Phacelle,
[string]$GlslangPath = ""
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
if (-not $GlslangPath) {
$found = Get-ChildItem -LiteralPath (Join-Path $repoRoot ".tools\glslang\current") -Recurse -File -Include "glslang.exe", "glslangValidator.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($found) { $GlslangPath = $found.FullName }
}
# The wrapper is what a WebGL2 consumer writes: its own #version line and precision, then the library
# by string concatenation (GLSL has no #include), then a fragment shader that calls every public
# function once with zero arguments of its declared types.
$library = Get-Content -LiteralPath (Join-Path $repoRoot "shaders\CultMath.glsl") -Raw
if ($Phacelle) {
$library += Get-Content -LiteralPath (Join-Path $repoRoot "shaders\CultMath.Phacelle.glsl") -Raw
}
$calls = foreach ($match in [regex]::Matches($library, '(?m)^\w+\s+(cultmath_\w+)\s*\(([^)]*)\)')) {
$arguments = foreach ($parameter in ($match.Groups[2].Value -split ',')) {
$type = ($parameter.Trim() -split '\s+')[0]
if ($type -eq 'int' -or $type -like 'ivec*') { "$type(0)" }
elseif ($type -eq 'uint' -or $type -like 'uvec*') { "$type(0u)" }
else { "$type(0.0)" }
}
" $($match.Groups[1].Value)($($arguments -join ', '));"
}
if (-not $Body) {
$Body = $calls -join "`n"
}
$wrapper = "#version 300 es`nprecision highp float;`nprecision highp int;`n$library`nout vec4 cultmathColor;`nvoid main()`n{`n$Body`n cultmathColor = vec4(0.0);`n}`n"
$wrapperPath = Join-Path $repoRoot ".tools\glsl\cultmath-wrapper.frag"
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $wrapperPath) | Out-Null
[System.IO.File]::WriteAllText($wrapperPath, $wrapper)
Write-Host "wrapper: $wrapperPath ($(@($calls).Count) functions)"
if (-not $GlslangPath -or -not (Test-Path $GlslangPath)) {
throw "glslang not found. Run tools\get-glslang.ps1 first or pass -GlslangPath."
}
& $GlslangPath -S frag $wrapperPath
if ($LASTEXITCODE -ne 0) {
throw "glslang failed with exit code $LASTEXITCODE."
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 81a35b09c28246fe8d52cf8996540a46
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,42 @@
param(
[string]$InstallRoot = ".tools\glslang"
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$installPath = Join-Path $repoRoot $InstallRoot
New-Item -ItemType Directory -Force -Path $installPath | Out-Null
# Pinned: the GLSL compile check means this front end (Khronos glslang 16.6.0). The hash is the
# SHA256 digest GitHub publishes for this asset of KhronosGroup/glslang release 16.6.0.
$zipName = "glslang-16.6.0-windows-x86_64-release.zip"
$zipUrl = "https://github.com/KhronosGroup/glslang/releases/download/16.6.0/$zipName"
$zipSha256 = "82BF434E69B9BB4829DE7E2B4BC2C5E7A7861E53D66CF75E5CC70F5F694A8D9B"
$zipPath = Join-Path $installPath $zipName
Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue
& curl.exe -L --retry 5 --retry-delay 2 --fail -o $zipPath $zipUrl
if ($LASTEXITCODE -ne 0) {
throw "curl.exe failed with exit code $LASTEXITCODE."
}
$actualSha256 = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash
if ($actualSha256 -ne $zipSha256) {
Remove-Item -LiteralPath $zipPath -Force
throw "$zipName SHA256 is $actualSha256, expected $zipSha256."
}
$currentPath = Join-Path $installPath "current"
Remove-Item -LiteralPath $currentPath -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $currentPath | Out-Null
Expand-Archive -LiteralPath $zipPath -DestinationPath $currentPath -Force
$glslangPath = Get-ChildItem -LiteralPath $currentPath -Recurse -File -Include "glslang.exe", "glslangValidator.exe" | Select-Object -First 1
if (-not $glslangPath) {
throw "glslang executable was not found after extraction."
}
& $glslangPath.FullName --version
Write-Host "glslang installed at $($glslangPath.FullName)"

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 37d057d5ae6847b7b4689d36514b3dc9
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -49,9 +49,9 @@ CultPhasor cultmath_phacelle(float3 p, float3 side, float offset, float normaliz
float3 gridStep = float3(dx, dy, dz);
int3 hash = cultmath_pcg3d(int3(cell + gridStep));
float3 jitter = float3(
((uint)hash.x >> 8) * (1.0 / 16777216.0) - 0.5,
((uint)hash.y >> 8) * (1.0 / 16777216.0) - 0.5,
((uint)hash.z >> 8) * (1.0 / 16777216.0) - 0.5);
(float)((uint)hash.x >> 8) * (1.0 / 16777216.0) - 0.5,
(float)((uint)hash.y >> 8) * (1.0 / 16777216.0) - 0.5,
(float)((uint)hash.z >> 8) * (1.0 / 16777216.0) - 0.5);
float3 v = local - gridStep - jitter;
float falloff = exp(-2.0 * dot(v, v));
float w = max(0.0, falloff - 0.01111);

View file

@ -111,7 +111,7 @@ float cultmath_snoise(float3 value)
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, 0.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;
@ -197,7 +197,7 @@ float4 cultmath_snoise_grad(float3 value)
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, 0.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;
@ -316,17 +316,20 @@ float cultmath_damp(float start, float end, float lambda_value, float dt)
float2 cultmath_damp(float2 start, float2 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, float2(amount, amount));
}
float3 cultmath_damp(float3 start, float3 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, float3(amount, amount, amount));
}
float4 cultmath_damp(float4 start, float4 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
float amount = 1.0 - exp(-lambda_value * dt);
return cultmath_lerp(start, end, float4(amount, amount, amount, amount));
}
float cultmath_catmullrom(float p0, float p1, float p2, float p3, float t)
@ -393,7 +396,7 @@ float cultmath_hash(float3 value) { return cultmath_hash(dot(value, float3(127.1
uint cultmath_pcg(uint value)
{
uint state = value * 747796405u + 2891336453u;
uint word = ((state >> (int)((state >> 28) + 4)) ^ state) * 277803737u;
uint word = ((state >> (int)((state >> 28) + 4u)) ^ state) * 277803737u;
return (word >> 22) ^ word;
}
@ -406,8 +409,8 @@ int3 cultmath_pcg3d(int3 value)
return int3((int)x, (int)y, (int)z);
}
int3 cultmath_pcg3d(float2 value) { return cultmath_pcg3d(float3(value, 0.0)); }
int3 cultmath_pcg3d(float3 value) { return cultmath_pcg3d(int3((int)asuint(value.x), (int)asuint(value.y), (int)asuint(value.z))); }
int3 cultmath_pcg3d(float2 value) { return cultmath_pcg3d(float3(value, 0.0)); }
int4 cultmath_pcg4d(int4 value)
{
@ -468,9 +471,9 @@ CultCellular cultmath_cellular(float3 p)
float3 neighbor = cell + float3(dx, dy, dz);
int3 hash = cultmath_pcg3d(int3(neighbor));
float3 jitter = float3(
((uint)hash.x >> 8) * (1.0 / 16777216.0),
((uint)hash.y >> 8) * (1.0 / 16777216.0),
((uint)hash.z >> 8) * (1.0 / 16777216.0));
(float)((uint)hash.x >> 8) * (1.0 / 16777216.0),
(float)((uint)hash.y >> 8) * (1.0 / 16777216.0),
(float)((uint)hash.z >> 8) * (1.0 / 16777216.0));
float3 feature = neighbor + jitter;
float d = cultmath_distance(p, feature);
@ -499,7 +502,7 @@ CultCellular cultmath_cellular(float3 p)
CultCellular result;
result.nearest = float4(grad1, f1);
result.edge = float4(grad2 - grad1, f2 - f1);
result.id = ((uint)idHash.w >> 8) * (1.0 / 16777216.0);
result.id = (float)((uint)idHash.w >> 8) * (1.0 / 16777216.0);
return result;
}