CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
MPL-2.0 is file-level and the Phacelle port sat inside MIT files. phacelle and CultPhasor now live in math.Phacelle.cs and CultPhasor.cs, and cultmath_phacelle in CultMath.Phacelle.hlsl (included by CultMath.hlsl, both tracked copies), each with the MPL header and Johansen / Luke Mitchell attribution. math stays one partial class, so math.phacelle is called exactly as before. The GameCult.Math package declares MIT AND MPL-2.0 and packs THIRD-PARTY-NOTICES.md and the new include; the Unity LICENSE.md points at CultLib; build-unity-package.ps1 requires the new include. The random sweeps never met a cell whose box bound sits just under the 2.25 cut while its weight is still nonzero, so cuts of 2.245, 2.24 and 2.2 survived. Six hard-coded points (found by an offline search) sit next to such cells; the C# pruned-vs-unpruned test and the HLSL mirror test both use them, with a witness check that re-derives the property from the hash. Docs: the prune keeps about 45 of 64 cells (14 is the nonzero-weight count), the support radius is 1.49998 (d^2 = 2.24995), normalization's range and saturation, W = 0 only for adversarial hashes, and the support-edge gradient seam size. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2a3cab1bd7
commit
0acc3728ed
21 changed files with 515 additions and 285 deletions
|
|
@ -33,6 +33,8 @@ shader semantics.
|
|||
- `shaders/CultMath.hlsl`, a canonical HLSL mirror include for shader-side
|
||||
parity. HLSL already owns `float2`, `float3`, and `float4`; the include
|
||||
exposes `cultmath_*` functions for shared semantics HLSL does not provide.
|
||||
It includes `shaders/CultMath.Phacelle.hlsl` (MPL-2.0) from the same
|
||||
directory, so ship both files.
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -32,5 +32,10 @@ gradient and the exact pruning are new here.
|
|||
|
||||
- Copyright (c) 2025 Rune Skovbo Johansen.
|
||||
- Upstream: <https://github.com/lpmitchell/AdvancedTerrainErosion>
|
||||
- License: MPL-2.0 (<https://mozilla.org/MPL/2.0/>). CultMath's Unity package
|
||||
is MPL-2.0, so the licence is compatible.
|
||||
- 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 declares `MIT AND MPL-2.0` and ships this
|
||||
file. The Unity package as a whole is MPL-2.0.
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ fields is compared to its C# counterpart field by field, recursing into each
|
|||
field's own components, down to bit-for-bit scalars, matching each leaf's
|
||||
declaration path (field names, not just position) and declared type as well
|
||||
as its value. `CultCellular` (`math.cs`, `cellular`) and `CultPhasor`
|
||||
(`math.cs`, `phacelle`) are its consumers. A struct return with a mismatched, reordered-by-name, or
|
||||
(`math.Phacelle.cs`, `phacelle`) are its consumers. A struct return with a mismatched, reordered-by-name, or
|
||||
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.
|
||||
|
||||
|
|
@ -278,11 +278,27 @@ together, laid out as `float4(gradient.xyz, value.w)`, with no value-only twin
|
|||
derivatives and normalization included, rather than upstream's
|
||||
`∇cos ≈ -sin·side`, which ignores the weight gradient. It is discontinuous
|
||||
where the raw length crosses the normalization threshold and where a cell's
|
||||
weight leaves its support (distance 1.4998); the value is continuous at both.
|
||||
weight leaves its support (d² = 2.24995, distance 1.49998); the value is
|
||||
continuous at both. At the support edge `∇w` jumps from `-4·0.01111·v` to 0,
|
||||
a per-cell jump of length `0.0444·|v|` (0.0667 at `|v| = 1.5`); measured over
|
||||
random points, the output gradient jumps by a median 1.5% of its length and
|
||||
at most about 27%. `normalization` is meant to lie in `[0, 1]`: 1 or more
|
||||
saturates (every output is stretched to length 1), a negative value scales
|
||||
every output down by `1 / (1 - normalization)`, and the normalized gradient
|
||||
scales as `1 / |I|` for the blended phasor `I`, so it grows where the blend
|
||||
nearly cancels. The total weight `W` is positive for every `pcg3d` input (the
|
||||
smallest found by hill-climbing is 0.056); `W = 0`, and a NaN output, needs
|
||||
adversarial jitter no hash yields.
|
||||
Cells whose per-axis lower bound `Σ max(0, |local - g| - 0.5)²` is at least
|
||||
2.25 have weight exactly 0 and are skipped, which changes no output bit;
|
||||
`PhacelleTests` pins that against an internal overload with the prune
|
||||
disabled. No 2D variant exists; nothing consumes one.
|
||||
2.25 lie outside the support and have weight exactly 0, so they are skipped,
|
||||
which changes no output bit. Cells whose bound sits just under 2.25 can still
|
||||
carry a nonzero weight, so the cut cannot be lowered; `PhacelleTests` pins
|
||||
the cut with points next to such cells, against an internal overload with the
|
||||
prune disabled, and the HLSL mirror test pins it on the shader side. The prune
|
||||
keeps about 45 of the 64 cells on average (a saving of about 30%); about 14
|
||||
have a nonzero weight. The port sits in its own MPL-2.0 files
|
||||
(`math.Phacelle.cs`, `CultPhasor.cs`, `CultMath.Phacelle.hlsl`); see
|
||||
`THIRD-PARTY-NOTICES.md`. No 2D variant exists; nothing consumes one.
|
||||
|
||||
Integer hashing uses the PCG hashes from Jarzynski and Olano, "Hash Functions
|
||||
for GPU Rendering" (JCGT 9(3), 2020): `pcg(uint)` is O'Neill's RXS-M-XS 32/32
|
||||
|
|
|
|||
|
|
@ -78,7 +78,9 @@ $requiredTemplateFiles = @(
|
|||
"Runtime\UnityBridge\UnityConversions.cs",
|
||||
"Runtime\UnityBridge\UnityConversions.cs.meta",
|
||||
"Shaders\CultMath.hlsl",
|
||||
"Shaders\CultMath.hlsl.meta"
|
||||
"Shaders\CultMath.hlsl.meta",
|
||||
"Shaders\CultMath.Phacelle.hlsl",
|
||||
"Shaders\CultMath.Phacelle.hlsl.meta"
|
||||
)
|
||||
foreach ($relativePath in $requiredTemplateFiles) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $templateRoot $relativePath) -PathType Leaf)) {
|
||||
|
|
|
|||
99
packages/cultmath/shaders/CultMath.Phacelle.hlsl
Normal file
99
packages/cultmath/shaders/CultMath.Phacelle.hlsl
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#ifndef CULTMATH_PHACELLE_HLSL
|
||||
#define CULTMATH_PHACELLE_HLSL
|
||||
|
||||
// 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
|
||||
{
|
||||
float4 cos;
|
||||
float4 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(float3 p, float3 side, float offset, float normalization)
|
||||
{
|
||||
float3 cell = floor(p);
|
||||
float3 local = p - cell;
|
||||
float phaseOffset = offset * CULTMATH_TAU;
|
||||
float sumW = 0.0;
|
||||
float sumCos = 0.0;
|
||||
float sumSin = 0.0;
|
||||
float3 gradW = float3(0.0, 0.0, 0.0);
|
||||
float3 gradCos = float3(0.0, 0.0, 0.0);
|
||||
float3 gradSin = float3(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;
|
||||
|
||||
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);
|
||||
float3 v = local - gridStep - jitter;
|
||||
float falloff = exp(-2.0 * dot(v, v));
|
||||
float w = max(0.0, falloff - 0.01111);
|
||||
float3 dw = w > 0.0 ? (-4.0 * falloff) * v : float3(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;
|
||||
float3 gradRawCos = (gradCos - rawCos * gradW) * invW;
|
||||
float3 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;
|
||||
float3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
7
packages/cultmath/shaders/CultMath.Phacelle.hlsl.meta
Normal file
7
packages/cultmath/shaders/CultMath.Phacelle.hlsl.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e07a3b95c1d84f2aa6b83d4c5f19a0d7
|
||||
ShaderIncludeImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -503,91 +503,7 @@ CultCellular cultmath_cellular(float3 p)
|
|||
return result;
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
float4 cos;
|
||||
float4 sin;
|
||||
};
|
||||
|
||||
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.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(float3 p, float3 side, float offset, float normalization)
|
||||
{
|
||||
float3 cell = floor(p);
|
||||
float3 local = p - cell;
|
||||
float phaseOffset = offset * CULTMATH_TAU;
|
||||
float sumW = 0.0;
|
||||
float sumCos = 0.0;
|
||||
float sumSin = 0.0;
|
||||
float3 gradW = float3(0.0, 0.0, 0.0);
|
||||
float3 gradCos = float3(0.0, 0.0, 0.0);
|
||||
float3 gradSin = float3(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;
|
||||
|
||||
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);
|
||||
float3 v = local - gridStep - jitter;
|
||||
float falloff = exp(-2.0 * dot(v, v));
|
||||
float w = max(0.0, falloff - 0.01111);
|
||||
float3 dw = w > 0.0 ? (-4.0 * falloff) * v : float3(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;
|
||||
float3 gradRawCos = (gradCos - rawCos * gradW) * invW;
|
||||
float3 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;
|
||||
float3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
#include "CultMath.Phacelle.hlsl"
|
||||
|
||||
float cultmath_value_noise(float2 position)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
<RootNamespace>CultMath</RootNamespace>
|
||||
<AssemblyName>CultMath</AssemblyName>
|
||||
<PackageId>GameCult.Math</PackageId>
|
||||
<!-- MIT overall; math.Phacelle.cs and CultPhasor.cs (and the two Phacelle HLSL files) are MPL-2.0. -->
|
||||
<PackageLicenseExpression>MIT AND MPL-2.0</PackageLicenseExpression>
|
||||
<Version>0.2.4</Version>
|
||||
<Description>Clean-room C# math shaped for HLSL-style renderer code.</Description>
|
||||
<Authors>GameCult</Authors>
|
||||
|
|
@ -21,6 +23,8 @@
|
|||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\shaders\CultMath.hlsl" Pack="true" PackagePath="contentFiles\any\any\shaders\CultMath.hlsl" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="..\..\shaders\CultMath.Phacelle.hlsl" Pack="true" PackagePath="contentFiles\any\any\shaders\CultMath.Phacelle.hlsl" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Include="..\..\THIRD-PARTY-NOTICES.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
// 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/.
|
||||
//
|
||||
// The result type of math.phacelle (math.Phacelle.cs), Rune Skovbo Johansen's Phacelle noise
|
||||
// (Copyright (c) 2025, MPL-2.0; see THIRD-PARTY-NOTICES.md).
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
127
packages/cultmath/src/CultMath/math.Phacelle.cs
Normal file
127
packages/cultmath/src/CultMath/math.Phacelle.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// 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 and CultPhasor.cs are the MPL-2.0 files of CultMath; the
|
||||
// rest of the package is MIT (see THIRD-PARTY-NOTICES.md).
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public static partial class math
|
||||
{
|
||||
// Johansen's Phacelle noise (phase + cell), generalised to 3D cells with a caller-supplied
|
||||
// stripe wave vector. Each of the 4x4x4 cells around p carries one jittered feature point
|
||||
// (jitter in [-0.5, 0.5) per axis, from pcg3d over the integer cell coordinate, the same
|
||||
// integer-hashing rule as cellular) and one cosine/sine wave that runs along `side` (the wave
|
||||
// vector: its length is 2*pi times the stripe frequency, and the caller chooses which
|
||||
// perpendicular it is; on a unit sphere that is cross(p_hat, flow)). The waves are blended with
|
||||
// the bell weight max(0, exp(-2 d^2) - 0.01111) (exactly 0 at distance 1.5, the nearest a
|
||||
// feature outside the 4x4x4 block can be in 2D, which is what keeps cell-grid lines out), and
|
||||
// the blended phasor is normalized: its length is stretched to 1 wherever the raw length is at
|
||||
// least 1 - normalization, and scaled by 1 / (1 - normalization) below that. `offset` is the
|
||||
// phase in cycles (1 is a full turn). Value and normalization follow Johansen's
|
||||
// PhacelleNoise as ported in lpmitchell/AdvancedTerrainErosion.
|
||||
//
|
||||
// `normalization` is meant to lie in [0, 1]. At 0 the output is the raw blend (a phasor of
|
||||
// length at most 1); at 1 the floor is 0 and every output is stretched to length 1. Values of
|
||||
// 1 or more saturate at that (the floor is never above the raw length), and a negative value
|
||||
// scales every output down by 1 / (1 - normalization) < 1. The normalized gradient scales as
|
||||
// 1 / |I| where I is the blended phasor (below), so it grows wherever the blend nearly
|
||||
// cancels; a normalization near 1 lets that happen at every point.
|
||||
//
|
||||
// The gradient is exact: it carries the weight derivative (grad w = -4 v exp(-2 |v|^2) where
|
||||
// w > 0) and the normalization, not upstream's convention grad(cos) ~ -sin * side, which
|
||||
// ignores the weight gradient (Asura probe: 16.8 deg off at the median, 97.8 at the 95th
|
||||
// percentile; the exact form is 0.00 and 0.03). With P = sum(w * (cos, sin)) and W = sum(w),
|
||||
// I = P / W has grad I = (grad P - I * grad W) / W; above the threshold the output is I / |I|
|
||||
// with gradient (grad I - out * grad|I|) / |I|, below it grad I / (1 - normalization). The
|
||||
// value is continuous at both seams below; the gradient is not.
|
||||
// * Where |I| crosses the threshold, the gradient jumps between the two branches.
|
||||
// * Where a cell's weight leaves its support (d^2 = ln(1 / 0.01111) / 2 = 2.24995, distance
|
||||
// 1.49998), grad w jumps from -4 * 0.01111 * v to 0: a per-cell jump of length
|
||||
// 0.0444 * |v|, 0.0667 at |v| = 1.5. Measured over random points, the resulting jump in
|
||||
// the output gradient is a median 1.5% of its length and at most about 27%.
|
||||
//
|
||||
// W is the sum of 64 weights and is positive for every input pcg3d produces (the smallest
|
||||
// W found by hill-climbing the hash-driven cell contents is 0.056). It is 0, and the output
|
||||
// NaN, only if all 64 weights vanish, which takes adversarial jitter no hash yields.
|
||||
//
|
||||
// Pruning is exact: per axis, |local - g| - 0.5 (floored at 0) lower-bounds |v| for any jitter,
|
||||
// so a cell whose summed square is at least 2.25 lies outside the support (2.24995 < 2.25) and
|
||||
// has weight exactly 0: it contributes nothing to any sum. Cells whose bound sits just under
|
||||
// 2.25 can still have a nonzero weight, so the cut cannot be lowered. On average the prune
|
||||
// keeps about 45 of the 64 cells (a saving of about 30%); only about 14 have a nonzero weight.
|
||||
// Only the internal overload can switch the prune off; PhacelleTests uses it to pin bit equality.
|
||||
public static CultPhasor phacelle(float3 p, float3 side, float offset, float normalization) =>
|
||||
phacelle(p, side, offset, normalization, true);
|
||||
|
||||
internal static CultPhasor phacelle(float3 p, float3 side, float offset, float normalization, bool prune)
|
||||
{
|
||||
var cell = floor(p);
|
||||
var local = p - cell;
|
||||
var phaseOffset = offset * TAU;
|
||||
var sumW = 0.0f;
|
||||
var sumCos = 0.0f;
|
||||
var sumSin = 0.0f;
|
||||
var gradW = new float3(0.0f, 0.0f, 0.0f);
|
||||
var gradCos = new float3(0.0f, 0.0f, 0.0f);
|
||||
var gradSin = new float3(0.0f, 0.0f, 0.0f);
|
||||
|
||||
for (var dz = -1; dz <= 2; dz++)
|
||||
{
|
||||
var gz = max(0.0f, abs(local.z - (float)dz) - 0.5f);
|
||||
for (var dy = -1; dy <= 2; dy++)
|
||||
{
|
||||
var gy = max(0.0f, abs(local.y - (float)dy) - 0.5f);
|
||||
for (var dx = -1; dx <= 2; dx++)
|
||||
{
|
||||
var gx = max(0.0f, abs(local.x - (float)dx) - 0.5f);
|
||||
if (prune && gx * gx + gy * gy + gz * gz >= 2.25f)
|
||||
continue;
|
||||
|
||||
var gridStep = new float3(dx, dy, dz);
|
||||
var hash = pcg3d(int3(cell + gridStep));
|
||||
var jitter = new float3(cellular_unit(hash.x) - 0.5f, cellular_unit(hash.y) - 0.5f, cellular_unit(hash.z) - 0.5f);
|
||||
var v = local - gridStep - jitter;
|
||||
var falloff = exp(-2.0f * dot(v, v));
|
||||
var w = max(0.0f, falloff - 0.01111f);
|
||||
var dw = w > 0.0f ? (-4.0f * falloff) * v : new float3(0.0f, 0.0f, 0.0f);
|
||||
var phase = dot(v, side) + phaseOffset;
|
||||
var c = cos(phase);
|
||||
var 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invW = 1.0f / sumW;
|
||||
var rawCos = sumCos * invW;
|
||||
var rawSin = sumSin * invW;
|
||||
var gradRawCos = (gradCos - rawCos * gradW) * invW;
|
||||
var gradRawSin = (gradSin - rawSin * gradW) * invW;
|
||||
var rawLength = sqrt(rawCos * rawCos + rawSin * rawSin);
|
||||
var floorLength = 1.0f - normalization;
|
||||
|
||||
if (rawLength > floorLength)
|
||||
{
|
||||
var outCos = rawCos / rawLength;
|
||||
var outSin = rawSin / rawLength;
|
||||
var gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
return new CultPhasor(
|
||||
new float4((gradRawCos - outCos * gradLength) / rawLength, outCos),
|
||||
new float4((gradRawSin - outSin * gradLength) / rawLength, outSin));
|
||||
}
|
||||
|
||||
return new CultPhasor(
|
||||
new float4(gradRawCos / floorLength, rawCos / floorLength),
|
||||
new float4(gradRawSin / floorLength, rawSin / floorLength));
|
||||
}
|
||||
}
|
||||
2
packages/cultmath/src/CultMath/math.Phacelle.cs.meta
Normal file
2
packages/cultmath/src/CultMath/math.Phacelle.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2c9d4e6b1a8f47d3b05e7a3c9d1f8e26
|
||||
|
|
@ -968,104 +968,6 @@ public static partial class math
|
|||
// dxc and C#, bit-exact on both sides.
|
||||
internal static float cellular_unit(int bits) => ((uint)bits >> 8) * (1.0f / 16777216.0f);
|
||||
|
||||
// Johansen's Phacelle noise (phase + cell), generalised to 3D cells with a caller-supplied
|
||||
// stripe wave vector. Each of the 4x4x4 cells around p carries one jittered feature point
|
||||
// (jitter in [-0.5, 0.5) per axis, from pcg3d over the integer cell coordinate, the same
|
||||
// integer-hashing rule as cellular) and one cosine/sine wave that runs along `side` (the wave
|
||||
// vector: its length is 2*pi times the stripe frequency, and the caller chooses which
|
||||
// perpendicular it is; on a unit sphere that is cross(p_hat, flow)). The waves are blended with
|
||||
// the bell weight max(0, exp(-2 d^2) - 0.01111) (exactly 0 at distance 1.5, the nearest a
|
||||
// feature outside the 4x4x4 block can be in 2D, which is what keeps cell-grid lines out), and
|
||||
// the blended phasor is normalized: its length is stretched to 1 wherever the raw length is at
|
||||
// least 1 - normalization, and scaled by 1 / (1 - normalization) below that. `offset` is the
|
||||
// phase in cycles (1 is a full turn). Value and normalization follow Johansen's
|
||||
// PhacelleNoise as ported in lpmitchell/AdvancedTerrainErosion.
|
||||
//
|
||||
// The gradient is exact: it carries the weight derivative (grad w = -4 v exp(-2 |v|^2) where
|
||||
// w > 0) and the normalization, not upstream's convention grad(cos) ~ -sin * side, which
|
||||
// ignores the weight gradient (Asura probe: 16.8 deg off at the median, 97.8 at the 95th
|
||||
// percentile; the exact form is 0.00 and 0.03). With P = sum(w * (cos, sin)) and W = sum(w),
|
||||
// I = P / W has grad I = (grad P - I * grad W) / W; above the threshold the output is I / |I|
|
||||
// with gradient (grad I - out * grad|I|) / |I|, below it grad I / (1 - normalization). The
|
||||
// gradient is discontinuous where |I| crosses the threshold, and where a cell's weight leaves
|
||||
// its support (distance 1.4998): there grad w jumps from -4 * 0.01111 * v (length about 0.067) to 0. The value is
|
||||
// continuous at both.
|
||||
//
|
||||
// Pruning is exact: per axis, |local - g| - 0.5 (floored at 0) lower-bounds |v| for any jitter,
|
||||
// so a cell whose summed square is at least 2.25 has weight exactly 0 (exp(-4.5) = 0.011109 <
|
||||
// 0.01111) and contributes nothing to any sum. Only the internal overload can switch the prune
|
||||
// off; PhacelleTests uses it to pin bit equality. About 14 of 64 cells survive on average.
|
||||
public static CultPhasor phacelle(float3 p, float3 side, float offset, float normalization) =>
|
||||
phacelle(p, side, offset, normalization, true);
|
||||
|
||||
internal static CultPhasor phacelle(float3 p, float3 side, float offset, float normalization, bool prune)
|
||||
{
|
||||
var cell = floor(p);
|
||||
var local = p - cell;
|
||||
var phaseOffset = offset * TAU;
|
||||
var sumW = 0.0f;
|
||||
var sumCos = 0.0f;
|
||||
var sumSin = 0.0f;
|
||||
var gradW = new float3(0.0f, 0.0f, 0.0f);
|
||||
var gradCos = new float3(0.0f, 0.0f, 0.0f);
|
||||
var gradSin = new float3(0.0f, 0.0f, 0.0f);
|
||||
|
||||
for (var dz = -1; dz <= 2; dz++)
|
||||
{
|
||||
var gz = max(0.0f, abs(local.z - (float)dz) - 0.5f);
|
||||
for (var dy = -1; dy <= 2; dy++)
|
||||
{
|
||||
var gy = max(0.0f, abs(local.y - (float)dy) - 0.5f);
|
||||
for (var dx = -1; dx <= 2; dx++)
|
||||
{
|
||||
var gx = max(0.0f, abs(local.x - (float)dx) - 0.5f);
|
||||
if (prune && gx * gx + gy * gy + gz * gz >= 2.25f)
|
||||
continue;
|
||||
|
||||
var gridStep = new float3(dx, dy, dz);
|
||||
var hash = pcg3d(int3(cell + gridStep));
|
||||
var jitter = new float3(cellular_unit(hash.x) - 0.5f, cellular_unit(hash.y) - 0.5f, cellular_unit(hash.z) - 0.5f);
|
||||
var v = local - gridStep - jitter;
|
||||
var falloff = exp(-2.0f * dot(v, v));
|
||||
var w = max(0.0f, falloff - 0.01111f);
|
||||
var dw = w > 0.0f ? (-4.0f * falloff) * v : new float3(0.0f, 0.0f, 0.0f);
|
||||
var phase = dot(v, side) + phaseOffset;
|
||||
var c = cos(phase);
|
||||
var 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invW = 1.0f / sumW;
|
||||
var rawCos = sumCos * invW;
|
||||
var rawSin = sumSin * invW;
|
||||
var gradRawCos = (gradCos - rawCos * gradW) * invW;
|
||||
var gradRawSin = (gradSin - rawSin * gradW) * invW;
|
||||
var rawLength = sqrt(rawCos * rawCos + rawSin * rawSin);
|
||||
var floorLength = 1.0f - normalization;
|
||||
|
||||
if (rawLength > floorLength)
|
||||
{
|
||||
var outCos = rawCos / rawLength;
|
||||
var outSin = rawSin / rawLength;
|
||||
var gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
return new CultPhasor(
|
||||
new float4((gradRawCos - outCos * gradLength) / rawLength, outCos),
|
||||
new float4((gradRawSin - outSin * gradLength) / rawLength, outSin));
|
||||
}
|
||||
|
||||
return new CultPhasor(
|
||||
new float4(gradRawCos / floorLength, rawCos / floorLength),
|
||||
new float4(gradRawSin / floorLength, rawSin / floorLength));
|
||||
}
|
||||
|
||||
public static float value_noise(float2 position)
|
||||
{
|
||||
var cell = floor(position);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ public sealed class HlslMirrorTests
|
|||
[Fact]
|
||||
public void HlslMirrorDoesNotPublishGeometryKernels()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(GetCultMathRoot(), "shaders", "CultMath.hlsl"));
|
||||
var source = HlslSourceCompatibilityTests.ReadShaderSource(GetCultMathRoot());
|
||||
Assert.DoesNotContain("AdvancedErosionFilter.hlsl", source);
|
||||
Assert.DoesNotContain("Planetary.hlsl", source);
|
||||
Assert.DoesNotContain("spherical_erosion", source);
|
||||
|
|
@ -17,7 +17,7 @@ public sealed class HlslMirrorTests
|
|||
[Fact]
|
||||
public void HlslMirrorPublishesCultMathPrimitives()
|
||||
{
|
||||
var include = File.ReadAllText(Path.Combine(GetCultMathRoot(), "shaders", "CultMath.hlsl"));
|
||||
var include = HlslSourceCompatibilityTests.ReadShaderSource(GetCultMathRoot());
|
||||
var requiredSymbols = new[]
|
||||
{
|
||||
"cultmath_radians",
|
||||
|
|
@ -62,6 +62,17 @@ public sealed class HlslMirrorTests
|
|||
Assert.DoesNotContain("cultmath_normalize", include);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CultMath.hlsl")]
|
||||
[InlineData("CultMath.Phacelle.hlsl")]
|
||||
public void UnityPackageShaderIsIdenticalToTheCanonicalShader(string name)
|
||||
{
|
||||
var root = GetCultMathRoot();
|
||||
Assert.Equal(
|
||||
File.ReadAllText(Path.Combine(root, "shaders", name)).ReplaceLineEndings("\n"),
|
||||
File.ReadAllText(Path.Combine(root, "unity", "org.gamecult.cultmath", "Shaders", name)).ReplaceLineEndings("\n"));
|
||||
}
|
||||
|
||||
private static string GetCultMathRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
|
|
|
|||
|
|
@ -163,6 +163,33 @@ public sealed class HlslSourceCompatibilityTests
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The dense sweep almost never meets a cell whose box bound is just under the 2.25 cut and whose
|
||||
/// weight is still nonzero: a mirror cut of 2.245, 2.24 or 2.2 survives it. These points sit next
|
||||
/// to such cells (<see cref="PhacelleTests.PruneCutPoints"/>), so the mirror must match C# bit for
|
||||
/// bit, and must match the unpruned C# sum (the shader has no prune switch, so that is the
|
||||
/// independent statement that the cut is exact).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PhacelleMirrorMatchesCSharpBitForBitNextToCellsJustInsideTheCut()
|
||||
{
|
||||
var (assembly, errors) = CompileShaderMirror();
|
||||
Assert.True(assembly is not null, string.Join(Environment.NewLine, errors));
|
||||
var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!;
|
||||
var shader = Activator.CreateInstance(shaderType);
|
||||
var mirror = ShaderFunction(shaderType, "cultmath_phacelle", typeof(float3), typeof(float3), typeof(float), typeof(float))!;
|
||||
|
||||
foreach (var p in PhacelleTests.PruneCutPoints)
|
||||
{
|
||||
Assert.True(PhacelleTests.HasCellJustInsideTheCut(p), $"no cell just inside the cut at {p}");
|
||||
var actual = Values(mirror.Invoke(shader, new object[] { p, PhacelleTests.PruneCutSide, PhacelleTests.PruneCutOffset, PhacelleTests.PruneCutNormalization })!);
|
||||
var pruned = Values(phacelle(p, PhacelleTests.PruneCutSide, PhacelleTests.PruneCutOffset, PhacelleTests.PruneCutNormalization));
|
||||
var unpruned = Values(phacelle(p, PhacelleTests.PruneCutSide, PhacelleTests.PruneCutOffset, PhacelleTests.PruneCutNormalization, false));
|
||||
Assert.True(BitwiseEqual(pruned, actual), $"mirror differs from C# at p = {p}");
|
||||
Assert.True(BitwiseEqual(unpruned, actual), $"mirror differs from the unpruned C# sum at p = {p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A hand-built pair of structs standing in for a mirror struct return and its C# counterpart
|
||||
/// (invariant 8's one struct return shape, e.g. <see cref="CultCellular"/>): same field shape,
|
||||
|
|
@ -283,6 +310,18 @@ public sealed class HlslSourceCompatibilityTests
|
|||
? (float.IsNaN(a) && float.IsNaN(b)) || BitConverter.SingleToInt32Bits(a) == BitConverter.SingleToInt32Bits(b)
|
||||
: p.First.Value.Equals(p.Second.Value)));
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
internal static string ReadShaderSource(string cultMathRoot)
|
||||
{
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <summary>The documented HLSL-to-C# transformations, and nothing else.</summary>
|
||||
internal static string TransformHlslToCSharp(string hlsl)
|
||||
{
|
||||
|
|
@ -311,7 +350,7 @@ public sealed class HlslSourceCompatibilityTests
|
|||
|
||||
private static (Assembly? Assembly, IReadOnlyList<string> Errors) CompileShaderMirror()
|
||||
{
|
||||
var hlsl = File.ReadAllText(Path.Combine(FindCultMathRoot(), "shaders", "CultMath.hlsl"));
|
||||
var hlsl = ReadShaderSource(FindCultMathRoot());
|
||||
var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl));
|
||||
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
|
||||
.Split(Path.PathSeparator)
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ public sealed class PhacelleTests
|
|||
{
|
||||
// Above the threshold the output is I / |I|; below it, I / (1 - normalization). The gradient
|
||||
// is discontinuous where |I| crosses the threshold, and at the edge of each cell's weight support
|
||||
// (distance 1.4998, where max(0, ...) starts: a jump of about 4 d 0.01111 in grad w), so a band
|
||||
// (distance 1.49998, where max(0, ...) starts: a jump of 4 d 0.01111 in grad w), so a band
|
||||
// around each is excluded (by the
|
||||
// reference's raw length, since the output alone cannot say how close it is).
|
||||
const float normalization = 0.5f;
|
||||
|
|
@ -265,6 +265,71 @@ public sealed class PhacelleTests
|
|||
}
|
||||
}
|
||||
|
||||
// Points next to a cell whose jitter sits at the far corner of its cube: the cell's box bound is
|
||||
// just under the 2.25 cut (2.2451 to 2.25) while its real distance is still under the support
|
||||
// radius (d^2 < 2.24995), so its weight is small but nonzero and a prune that cuts even slightly
|
||||
// low (2.245, 2.24, 2.2) drops a contribution and changes output bits. Random points meet such
|
||||
// a cell about once in 10^4 evaluations, which is why the random sweep above let those cuts
|
||||
// through. Each was found once by an offline search (cells K in [-12, 12]^3 whose jitter on one
|
||||
// axis lies within 0.0008 of the cube face; p is the feature moved 1.4996 out along that axis,
|
||||
// so |p - feature| = 1.4996 and the bound is about 1.4988). The witness check below re-derives
|
||||
// that property from the hash, so a change to pcg3d fails loudly instead of degenerating.
|
||||
// Shared with the HLSL mirror test.
|
||||
internal static readonly float3[] PruneCutPoints =
|
||||
{
|
||||
new(1.77850008f, -2.16020727f, -0.9991166f),
|
||||
new(1.09346282f, -2.37882328f, -3.00082445f),
|
||||
new(-6.39144945f, -3.99953747f, -0.610023975f),
|
||||
new(-0.99940908f, 4.16302729f, 4.07277536f),
|
||||
new(3.99912024f, 7.21890545f, 1.09053993f),
|
||||
new(5.43959713f, 1.99937367f, -6.14929914f),
|
||||
};
|
||||
|
||||
internal static readonly float3 PruneCutSide = new(2.1f, -3.7f, 1.3f);
|
||||
internal const float PruneCutOffset = 0.3f;
|
||||
internal const float PruneCutNormalization = 0.5f;
|
||||
|
||||
// True when some cell of the 4x4x4 window has a box bound in [2.2451, 2.25) and a positive weight.
|
||||
internal static bool HasCellJustInsideTheCut(float3 p)
|
||||
{
|
||||
double fx = Math.Floor(p.x), fy = Math.Floor(p.y), fz = Math.Floor(p.z);
|
||||
double lx = p.x - fx, ly = p.y - fy, lz = p.z - fz;
|
||||
for (var dz = -1; dz <= 2; dz++)
|
||||
for (var dy = -1; dy <= 2; dy++)
|
||||
for (var dx = -1; dx <= 2; dx++)
|
||||
{
|
||||
var gx = Math.Max(0.0, Math.Abs(lx - dx) - 0.5);
|
||||
var gy = Math.Max(0.0, Math.Abs(ly - dy) - 0.5);
|
||||
var gz = Math.Max(0.0, Math.Abs(lz - dz) - 0.5);
|
||||
var bound = gx * gx + gy * gy + gz * gz;
|
||||
if (bound < 2.2451 || bound >= 2.25)
|
||||
continue;
|
||||
|
||||
var hash = math.pcg3d(new int3((int)fx + dx, (int)fy + dy, (int)fz + dz));
|
||||
double vx = lx - dx - (math.cellular_unit(hash.x) - 0.5);
|
||||
double vy = ly - dy - (math.cellular_unit(hash.y) - 0.5);
|
||||
double vz = lz - dz - (math.cellular_unit(hash.z) - 0.5);
|
||||
if (Math.Exp(-2.0 * (vx * vx + vy * vy + vz * vz)) > 0.01111)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrunedOutputIsBitEqualToTheUnprunedSumNextToCellsJustInsideTheCut()
|
||||
{
|
||||
foreach (var p in PruneCutPoints)
|
||||
{
|
||||
Assert.True(HasCellJustInsideTheCut(p), $"no cell just inside the cut at {p}: the fixture no longer exercises the prune");
|
||||
var pruned = math.phacelle(p, PruneCutSide, PruneCutOffset, PruneCutNormalization);
|
||||
var unpruned = math.phacelle(p, PruneCutSide, PruneCutOffset, PruneCutNormalization, false);
|
||||
|
||||
AssertBitEqual(unpruned.cos, pruned.cos);
|
||||
AssertBitEqual(unpruned.sin, pruned.sin);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertBitEqual(float4 expected, float4 actual)
|
||||
{
|
||||
Assert.Equal(BitConverter.SingleToInt32Bits(expected.x), BitConverter.SingleToInt32Bits(actual.x));
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ CultMath is licensed under the Mozilla Public License 2.0 (`MPL-2.0`).
|
|||
|
||||
The complete license text is available from the Mozilla Foundation at
|
||||
<https://www.mozilla.org/MPL/2.0/>. Source and modification history for the
|
||||
precompiled assembly in this package are available in the GameCult/CultMath
|
||||
repository at the corresponding package tag.
|
||||
precompiled assembly in this package are available in the GameCult/CultLib
|
||||
repository (`packages/cultmath`) at the corresponding package tag.
|
||||
|
||||
Third-party code retains its original license. See `THIRD-PARTY-NOTICES.md`.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This is CultMath's Unity 2021.3-compatible Git-UPM surface. It contains the
|
||||
precompiled `CultMath.dll`, portable symbols, the numeric
|
||||
`Shaders/CultMath.hlsl` mirror, and the `CultMath.UnityBridge` assembly.
|
||||
`Shaders/CultMath.hlsl` mirror (with its `CultMath.Phacelle.hlsl` include), and the `CultMath.UnityBridge` assembly.
|
||||
|
||||
`CultMath.UnityBridge` is the only C# source Unity compiles here. It holds the
|
||||
UnityEngine conversions the engine-free core cannot declare: `ToCultMath()` on
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
#ifndef CULTMATH_PHACELLE_HLSL
|
||||
#define CULTMATH_PHACELLE_HLSL
|
||||
|
||||
// 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
|
||||
{
|
||||
float4 cos;
|
||||
float4 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(float3 p, float3 side, float offset, float normalization)
|
||||
{
|
||||
float3 cell = floor(p);
|
||||
float3 local = p - cell;
|
||||
float phaseOffset = offset * CULTMATH_TAU;
|
||||
float sumW = 0.0;
|
||||
float sumCos = 0.0;
|
||||
float sumSin = 0.0;
|
||||
float3 gradW = float3(0.0, 0.0, 0.0);
|
||||
float3 gradCos = float3(0.0, 0.0, 0.0);
|
||||
float3 gradSin = float3(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;
|
||||
|
||||
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);
|
||||
float3 v = local - gridStep - jitter;
|
||||
float falloff = exp(-2.0 * dot(v, v));
|
||||
float w = max(0.0, falloff - 0.01111);
|
||||
float3 dw = w > 0.0 ? (-4.0 * falloff) * v : float3(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;
|
||||
float3 gradRawCos = (gradCos - rawCos * gradW) * invW;
|
||||
float3 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;
|
||||
float3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5b1c7e0a94d24f6c8a3e2d17f60b9c48
|
||||
ShaderIncludeImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -503,91 +503,7 @@ CultCellular cultmath_cellular(float3 p)
|
|||
return result;
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
float4 cos;
|
||||
float4 sin;
|
||||
};
|
||||
|
||||
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.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(float3 p, float3 side, float offset, float normalization)
|
||||
{
|
||||
float3 cell = floor(p);
|
||||
float3 local = p - cell;
|
||||
float phaseOffset = offset * CULTMATH_TAU;
|
||||
float sumW = 0.0;
|
||||
float sumCos = 0.0;
|
||||
float sumSin = 0.0;
|
||||
float3 gradW = float3(0.0, 0.0, 0.0);
|
||||
float3 gradCos = float3(0.0, 0.0, 0.0);
|
||||
float3 gradSin = float3(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;
|
||||
|
||||
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);
|
||||
float3 v = local - gridStep - jitter;
|
||||
float falloff = exp(-2.0 * dot(v, v));
|
||||
float w = max(0.0, falloff - 0.01111);
|
||||
float3 dw = w > 0.0 ? (-4.0 * falloff) * v : float3(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;
|
||||
float3 gradRawCos = (gradCos - rawCos * gradW) * invW;
|
||||
float3 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;
|
||||
float3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
#include "CultMath.Phacelle.hlsl"
|
||||
|
||||
float cultmath_value_noise(float2 position)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,5 +32,10 @@ gradient and the exact pruning are new here.
|
|||
|
||||
- Copyright (c) 2025 Rune Skovbo Johansen.
|
||||
- Upstream: <https://github.com/lpmitchell/AdvancedTerrainErosion>
|
||||
- License: MPL-2.0 (<https://mozilla.org/MPL/2.0/>). CultMath's Unity package
|
||||
is MPL-2.0, so the licence is compatible.
|
||||
- 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 declares `MIT AND MPL-2.0` and ships this
|
||||
file. The Unity package as a whole is MPL-2.0.
|
||||
|
|
|
|||
Loading…
Reference in a new issue