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:
commit
bb583e89d5
17 changed files with 8989 additions and 39 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
99
packages/cultmath/shaders/CultMath.Phacelle.glsl
Normal file
99
packages/cultmath/shaders/CultMath.Phacelle.glsl
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
660
packages/cultmath/shaders/CultMath.glsl
Normal file
660
packages/cultmath/shaders/CultMath.glsl
Normal 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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
179
packages/cultmath/tests/CultMath.Tests/GlslLowering.cs
Normal file
179
packages/cultmath/tests/CultMath.Tests/GlslLowering.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
419
packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs
Normal file
419
packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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")))
|
||||
|
|
|
|||
7347
packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json
Normal file
7347
packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json
Normal file
File diff suppressed because it is too large
Load diff
52
packages/cultmath/tools/compile-glsl.ps1
Normal file
52
packages/cultmath/tools/compile-glsl.ps1
Normal 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."
|
||||
}
|
||||
7
packages/cultmath/tools/compile-glsl.ps1.meta
Normal file
7
packages/cultmath/tools/compile-glsl.ps1.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 81a35b09c28246fe8d52cf8996540a46
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
42
packages/cultmath/tools/get-glslang.ps1
Normal file
42
packages/cultmath/tools/get-glslang.ps1
Normal 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)"
|
||||
7
packages/cultmath/tools/get-glslang.ps1.meta
Normal file
7
packages/cultmath/tools/get-glslang.ps1.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 37d057d5ae6847b7b4689d36514b3dc9
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue