Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
using System ;
2026-07-12 19:33:59 +01:00
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
// Cut 2a-i fix batch 1: cellular_unit's [0,1) mapping bug (F7) is only reachable from cellular's
// private call sites; this lets CultMath.Tests probe it directly instead of hunting for a hash
// input that happens to land on the bit pattern under test.
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CultMath.Tests")]
2026-05-08 01:49:44 +01:00
namespace CultMath ;
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
public static partial class math
2026-05-08 01:49:44 +01:00
{
public const float PI = MathF . PI ;
public const float TAU = MathF . PI * 2.0f ;
public const float HALF_PI = MathF . PI * 0.5f ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
/// <summary>
/// Extrinsic Euler order for <see cref="CultMath.float3x3.Euler(CultMath.float3, RotationOrder)"/>:
/// the first letter is the axis applied first. Nested here so <c>using static CultMath.math;</c>
/// brings it into scope with the rest of the shader vocabulary.
/// </summary>
public enum RotationOrder : byte
{
XYZ ,
XZY ,
YXZ ,
YZX ,
ZXY ,
ZYX ,
Default = ZXY ,
}
2026-06-10 21:22:43 +01:00
public static float2 float2 ( float x , float y ) = > new ( x , y ) ;
public static float2 float2 ( float value ) = > new ( value , value ) ;
2026-07-12 19:33:59 +01:00
public static bool2 bool2 ( bool x , bool y ) = > new ( x , y ) ;
public static bool2 bool2 ( bool value ) = > new ( value , value ) ;
public static int2 int2 ( int x , int y ) = > new ( x , y ) ;
public static int2 int2 ( int value ) = > new ( value , value ) ;
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
// Mixed constructor functions (float4(float2, float2), ...) are generated in Swizzles.g.cs.
2026-06-10 21:22:43 +01:00
public static float3 float3 ( float x , float y , float z ) = > new ( x , y , z ) ;
public static float3 float3 ( float value ) = > new ( value , value , value ) ;
public static float4 float4 ( float x , float y , float z , float w ) = > new ( x , y , z , w ) ;
public static float4 float4 ( float value ) = > new ( value , value , value , value ) ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
public static bool3 bool3 ( bool x , bool y , bool z ) = > new ( x , y , z ) ;
public static bool3 bool3 ( bool value ) = > new ( value , value , value ) ;
public static bool4 bool4 ( bool x , bool y , bool z , bool w ) = > new ( x , y , z , w ) ;
public static bool4 bool4 ( bool value ) = > new ( value , value , value , value ) ;
public static int3 int3 ( int x , int y , int z ) = > new ( x , y , z ) ;
public static int3 int3 ( int value ) = > new ( value , value , value ) ;
public static int4 int4 ( int x , int y , int z , int w ) = > new ( x , y , z , w ) ;
public static int4 int4 ( int value ) = > new ( value , value , value , value ) ;
// Matrix constructor functions take rows, as HLSL does. Under `using static CultMath.math;`
// these method groups hide the type names in member access, so static members need a
// qualified type: `CultMath.float3x3.Euler(...)`, `CultMath.float2x2.Rotate(...)`.
public static float2x2 float2x2 ( float m00 , float m01 , float m10 , float m11 ) = > new ( m00 , m01 , m10 , m11 ) ;
public static float2x2 float2x2 ( float2 row0 , float2 row1 ) = > new ( row0 , row1 ) ;
public static float3x3 float3x3 ( float m00 , float m01 , float m02 , float m10 , float m11 , float m12 , float m20 , float m21 , float m22 ) = >
new ( m00 , m01 , m02 , m10 , m11 , m12 , m20 , m21 , m22 ) ;
public static float3x3 float3x3 ( float3 row0 , float3 row1 , float3 row2 ) = > new ( row0 , row1 , row2 ) ;
2026-05-08 01:49:44 +01:00
public static float radians ( float degrees ) = > degrees * ( PI / 180.0f ) ;
public static float degrees ( float radians ) = > radians * ( 180.0f / PI ) ;
2026-05-25 20:33:04 +01:00
public static float sin ( float value ) = > MathF . Sin ( value ) ;
public static float cos ( float value ) = > MathF . Cos ( value ) ;
public static float tan ( float value ) = > MathF . Tan ( value ) ;
public static float asin ( float value ) = > MathF . Asin ( value ) ;
public static float acos ( float value ) = > MathF . Acos ( value ) ;
public static float atan ( float value ) = > MathF . Atan ( value ) ;
public static float atan2 ( float y , float x ) = > MathF . Atan2 ( y , x ) ;
public static float sqrt ( float value ) = > MathF . Sqrt ( value ) ;
2026-07-12 19:33:59 +01:00
public static float pow ( float value , float power ) = > MathF . Pow ( value , power ) ;
public static bool isinf ( float value ) = > float . IsInfinity ( value ) ;
2026-06-10 21:22:43 +01:00
public static float exp ( float value ) = > MathF . Exp ( value ) ;
2026-07-12 19:33:59 +01:00
public static double exp ( double value ) = > Math . Exp ( value ) ;
2026-06-10 21:22:43 +01:00
public static float2 exp ( float2 value ) = > new ( exp ( value . x ) , exp ( value . y ) ) ;
public static float3 exp ( float3 value ) = > new ( exp ( value . x ) , exp ( value . y ) , exp ( value . z ) ) ;
public static float4 exp ( float4 value ) = > new ( exp ( value . x ) , exp ( value . y ) , exp ( value . z ) , exp ( value . w ) ) ;
2026-07-12 19:33:59 +01:00
public static double2 exp ( double2 value ) = > new ( exp ( value . x ) , exp ( value . y ) ) ;
public static double3 exp ( double3 value ) = > new ( exp ( value . x ) , exp ( value . y ) , exp ( value . z ) ) ;
2026-05-08 01:49:44 +01:00
public static float abs ( float value ) = > MathF . Abs ( value ) ;
public static float2 abs ( float2 value ) = > new ( abs ( value . x ) , abs ( value . y ) ) ;
public static float3 abs ( float3 value ) = > new ( abs ( value . x ) , abs ( value . y ) , abs ( value . z ) ) ;
public static float4 abs ( float4 value ) = > new ( abs ( value . x ) , abs ( value . y ) , abs ( value . z ) , abs ( value . w ) ) ;
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
public static int abs ( int value ) = > Math . Abs ( value ) ;
public static int2 abs ( int2 value ) = > new ( abs ( value . x ) , abs ( value . y ) ) ;
public static int3 abs ( int3 value ) = > new ( abs ( value . x ) , abs ( value . y ) , abs ( value . z ) ) ;
public static int4 abs ( int4 value ) = > new ( abs ( value . x ) , abs ( value . y ) , abs ( value . z ) , abs ( value . w ) ) ;
// HLSL sign returns int. NaN compares false both ways, so it yields 0 instead of throwing.
public static int sign ( float value ) = > value > 0.0f ? 1 : value < 0.0f ? - 1 : 0 ;
public static int2 sign ( float2 value ) = > new ( sign ( value . x ) , sign ( value . y ) ) ;
public static int3 sign ( float3 value ) = > new ( sign ( value . x ) , sign ( value . y ) , sign ( value . z ) ) ;
public static int4 sign ( float4 value ) = > new ( sign ( value . x ) , sign ( value . y ) , sign ( value . z ) , sign ( value . w ) ) ;
public static int sign ( int value ) = > value > 0 ? 1 : value < 0 ? - 1 : 0 ;
public static int2 sign ( int2 value ) = > new ( sign ( value . x ) , sign ( value . y ) ) ;
public static int3 sign ( int3 value ) = > new ( sign ( value . x ) , sign ( value . y ) , sign ( value . z ) ) ;
public static int4 sign ( int4 value ) = > new ( sign ( value . x ) , sign ( value . y ) , sign ( value . z ) , sign ( value . w ) ) ;
2026-07-12 19:33:59 +01:00
2026-05-08 01:49:44 +01:00
public static float floor ( float value ) = > MathF . Floor ( value ) ;
public static float2 floor ( float2 value ) = > new ( floor ( value . x ) , floor ( value . y ) ) ;
public static float3 floor ( float3 value ) = > new ( floor ( value . x ) , floor ( value . y ) , floor ( value . z ) ) ;
public static float4 floor ( float4 value ) = > new ( floor ( value . x ) , floor ( value . y ) , floor ( value . z ) , floor ( value . w ) ) ;
public static float ceil ( float value ) = > MathF . Ceiling ( value ) ;
public static float2 ceil ( float2 value ) = > new ( ceil ( value . x ) , ceil ( value . y ) ) ;
public static float3 ceil ( float3 value ) = > new ( ceil ( value . x ) , ceil ( value . y ) , ceil ( value . z ) ) ;
public static float4 ceil ( float4 value ) = > new ( ceil ( value . x ) , ceil ( value . y ) , ceil ( value . z ) , ceil ( value . w ) ) ;
public static float frac ( float value ) = > value - floor ( value ) ;
public static float2 frac ( float2 value ) = > value - floor ( value ) ;
public static float3 frac ( float3 value ) = > value - floor ( value ) ;
public static float4 frac ( float4 value ) = > value - floor ( value ) ;
Add HLSL vector conversions, int clamp, and double frac to CultMath
Aetheria Cut 8a needs int2(float2), clamp(int, int, int), and
frac(double). Fill them in CultMath as dxc compiles them:
- generate-swizzles.ps1 emits same-size conversion constructors and
math functions between float, int, and bool vectors. dxc lowers
float to int as fptosi (toward zero; NaN and out-of-range undefined),
numeric to bool as != 0 (fcmp une, NaN is true), bool to numeric as
0 or 1. floatN(int) splat keeps floatN(0) from going ambiguous
against floatN(intN).
- Int and int-vector clamp are min(max(x, a), b), matching dxc's
IMin(IMax(x, a), b); inverted bounds return b.
- frac(double) keeps double precision. dxc demotes double frac to
float, as it does exp and lerp; CultMath's existing double overloads
already diverge that way, and design.md now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:16:32 +02:00
// dxc has no double Frc: it demotes to float, as it does for exp and lerp. CultMath's double
// overloads keep double precision instead, so frac(double) is x - floor(x) in double.
public static double frac ( double value ) = > value - Math . Floor ( value ) ;
2026-05-08 01:49:44 +01:00
Match float min, max, clamp, and saturate to DXIL FMin/FMax/Saturate
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.
Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:19 +02:00
// dxc lowers float min/max/clamp to DXIL FMin/FMax (saturate to Saturate, defined as FMin(1, FMax(0, x))).
// DXIL.rst: FMin(a, b) is a < b ? a : b, FMax(a, b) is a >= b ? a : b, and a NaN operand returns the other.
public static float min ( float left , float right ) = > left < right | | float . IsNaN ( right ) ? left : right ;
2026-07-12 19:33:59 +01:00
public static int min ( int left , int right ) = > Math . Min ( left , right ) ;
2026-05-08 01:49:44 +01:00
public static float2 min ( float2 left , float2 right ) = > new ( min ( left . x , right . x ) , min ( left . y , right . y ) ) ;
public static float3 min ( float3 left , float3 right ) = > new ( min ( left . x , right . x ) , min ( left . y , right . y ) , min ( left . z , right . z ) ) ;
public static float4 min ( float4 left , float4 right ) = > new ( min ( left . x , right . x ) , min ( left . y , right . y ) , min ( left . z , right . z ) , min ( left . w , right . w ) ) ;
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
public static int2 min ( int2 left , int2 right ) = > new ( min ( left . x , right . x ) , min ( left . y , right . y ) ) ;
public static int3 min ( int3 left , int3 right ) = > new ( min ( left . x , right . x ) , min ( left . y , right . y ) , min ( left . z , right . z ) ) ;
public static int4 min ( int4 left , int4 right ) = > new ( min ( left . x , right . x ) , min ( left . y , right . y ) , min ( left . z , right . z ) , min ( left . w , right . w ) ) ;
2026-05-08 01:49:44 +01:00
Match float min, max, clamp, and saturate to DXIL FMin/FMax/Saturate
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.
Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:19 +02:00
public static float max ( float left , float right ) = > left > = right | | float . IsNaN ( right ) ? left : right ;
2026-07-12 19:33:59 +01:00
public static int max ( int left , int right ) = > Math . Max ( left , right ) ;
2026-05-08 01:49:44 +01:00
public static float2 max ( float2 left , float2 right ) = > new ( max ( left . x , right . x ) , max ( left . y , right . y ) ) ;
public static float3 max ( float3 left , float3 right ) = > new ( max ( left . x , right . x ) , max ( left . y , right . y ) , max ( left . z , right . z ) ) ;
public static float4 max ( float4 left , float4 right ) = > new ( max ( left . x , right . x ) , max ( left . y , right . y ) , max ( left . z , right . z ) , max ( left . w , right . w ) ) ;
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
public static int2 max ( int2 left , int2 right ) = > new ( max ( left . x , right . x ) , max ( left . y , right . y ) ) ;
public static int3 max ( int3 left , int3 right ) = > new ( max ( left . x , right . x ) , max ( left . y , right . y ) , max ( left . z , right . z ) ) ;
public static int4 max ( int4 left , int4 right ) = > new ( max ( left . x , right . x ) , max ( left . y , right . y ) , max ( left . z , right . z ) , max ( left . w , right . w ) ) ;
2026-05-08 01:49:44 +01:00
public static float clamp ( float value , float minimum , float maximum ) = > min ( max ( value , minimum ) , maximum ) ;
public static float2 clamp ( float2 value , float2 minimum , float2 maximum ) = > min ( max ( value , minimum ) , maximum ) ;
public static float3 clamp ( float3 value , float3 minimum , float3 maximum ) = > min ( max ( value , minimum ) , maximum ) ;
public static float4 clamp ( float4 value , float4 minimum , float4 maximum ) = > min ( max ( value , minimum ) , maximum ) ;
Add HLSL vector conversions, int clamp, and double frac to CultMath
Aetheria Cut 8a needs int2(float2), clamp(int, int, int), and
frac(double). Fill them in CultMath as dxc compiles them:
- generate-swizzles.ps1 emits same-size conversion constructors and
math functions between float, int, and bool vectors. dxc lowers
float to int as fptosi (toward zero; NaN and out-of-range undefined),
numeric to bool as != 0 (fcmp une, NaN is true), bool to numeric as
0 or 1. floatN(int) splat keeps floatN(0) from going ambiguous
against floatN(intN).
- Int and int-vector clamp are min(max(x, a), b), matching dxc's
IMin(IMax(x, a), b); inverted bounds return b.
- frac(double) keeps double precision. dxc demotes double frac to
float, as it does exp and lerp; CultMath's existing double overloads
already diverge that way, and design.md now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:16:32 +02:00
// dxc lowers int clamp to IMin(IMax(x, a), b): inverted bounds return b rather than throwing like Math.Clamp.
public static int clamp ( int value , int minimum , int maximum ) = > min ( max ( value , minimum ) , maximum ) ;
public static int2 clamp ( int2 value , int2 minimum , int2 maximum ) = > min ( max ( value , minimum ) , maximum ) ;
public static int3 clamp ( int3 value , int3 minimum , int3 maximum ) = > min ( max ( value , minimum ) , maximum ) ;
public static int4 clamp ( int4 value , int4 minimum , int4 maximum ) = > min ( max ( value , minimum ) , maximum ) ;
2026-05-08 01:49:44 +01:00
Match float min, max, clamp, and saturate to DXIL FMin/FMax/Saturate
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.
Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:19 +02:00
// DXIL Saturate is FMin(1, FMax(0, x)); that operand order, unlike clamp(x, 0, 1), maps -0 to +0.
public static float saturate ( float value ) = > min ( 1.0f , max ( 0.0f , value ) ) ;
public static float2 saturate ( float2 value ) = > min ( 1.0f , max ( 0.0f , value ) ) ;
public static float3 saturate ( float3 value ) = > min ( 1.0f , max ( 0.0f , value ) ) ;
public static float4 saturate ( float4 value ) = > min ( 1.0f , max ( 0.0f , value ) ) ;
2026-05-08 01:49:44 +01:00
public static float lerp ( float start , float end , float amount ) = > start + ( end - start ) * amount ;
2026-07-12 19:33:59 +01:00
public static double lerp ( double start , double end , double amount ) = > start + ( end - start ) * amount ;
2026-05-08 01:49:44 +01:00
public static float2 lerp ( float2 start , float2 end , float2 amount ) = > start + ( end - start ) * amount ;
public static float3 lerp ( float3 start , float3 end , float3 amount ) = > start + ( end - start ) * amount ;
public static float4 lerp ( float4 start , float4 end , float4 amount ) = > start + ( end - start ) * amount ;
2026-07-12 19:33:59 +01:00
public static double2 lerp ( double2 start , double2 end , double2 amount ) = > start + ( end - start ) * amount ;
public static double3 lerp ( double3 start , double3 end , double3 amount ) = > start + ( end - start ) * amount ;
public static float unlerp ( float start , float end , float value ) = > ( value - start ) / ( end - start ) ;
public static double unlerp ( double start , double end , double value ) = > ( value - start ) / ( end - start ) ;
public static float2 unlerp ( float2 start , float2 end , float2 value ) = > ( value - start ) / ( end - start ) ;
public static float3 unlerp ( float3 start , float3 end , float3 value ) = > ( value - start ) / ( end - start ) ;
public static float4 unlerp ( float4 start , float4 end , float4 value ) = > ( value - start ) / ( end - start ) ;
public static double2 unlerp ( double2 start , double2 end , double2 value ) = > ( value - start ) / ( end - start ) ;
public static double3 unlerp ( double3 start , double3 end , double3 value ) = > ( value - start ) / ( end - start ) ;
2026-05-08 01:49:44 +01:00
2026-09-14 14:09:49 +02:00
// dxc lowers step(y, x) to x < y ? 0 : 1 (fcmp olt, then select), so a NaN on either side yields 1.
public static float step ( float edge , float value ) = > value < edge ? 0.0f : 1.0f ;
2026-05-08 01:49:44 +01:00
public static float2 step ( float2 edge , float2 value ) = > new ( step ( edge . x , value . x ) , step ( edge . y , value . y ) ) ;
public static float3 step ( float3 edge , float3 value ) = > new ( step ( edge . x , value . x ) , step ( edge . y , value . y ) , step ( edge . z , value . z ) ) ;
public static float4 step ( float4 edge , float4 value ) = > new ( step ( edge . x , value . x ) , step ( edge . y , value . y ) , step ( edge . z , value . z ) , step ( edge . w , value . w ) ) ;
public static float smoothstep ( float minimum , float maximum , float value )
{
var t = saturate ( ( value - minimum ) / ( maximum - minimum ) ) ;
return t * t * ( 3.0f - 2.0f * t ) ;
}
public static float2 smoothstep ( float2 minimum , float2 maximum , float2 value )
{
var t = saturate ( ( value - minimum ) / ( maximum - minimum ) ) ;
return t * t * ( 3.0f - 2.0f * t ) ;
}
public static float3 smoothstep ( float3 minimum , float3 maximum , float3 value )
{
var t = saturate ( ( value - minimum ) / ( maximum - minimum ) ) ;
return t * t * ( 3.0f - 2.0f * t ) ;
}
public static float4 smoothstep ( float4 minimum , float4 maximum , float4 value )
{
var t = saturate ( ( value - minimum ) / ( maximum - minimum ) ) ;
return t * t * ( 3.0f - 2.0f * t ) ;
}
public static float dot ( float2 left , float2 right ) = > left . x * right . x + left . y * right . y ;
public static float dot ( float3 left , float3 right ) = > left . x * right . x + left . y * right . y + left . z * right . z ;
public static float dot ( float4 left , float4 right ) = > left . x * right . x + left . y * right . y + left . z * right . z + left . w * right . w ;
public static float3 cross ( float3 left , float3 right ) = >
new (
left . y * right . z - left . z * right . y ,
left . z * right . x - left . x * right . z ,
left . x * right . y - left . y * right . x ) ;
public static float lengthsq ( float2 value ) = > dot ( value , value ) ;
public static float lengthsq ( float3 value ) = > dot ( value , value ) ;
public static float lengthsq ( float4 value ) = > dot ( value , value ) ;
public static float length ( float2 value ) = > MathF . Sqrt ( lengthsq ( value ) ) ;
public static float length ( float3 value ) = > MathF . Sqrt ( lengthsq ( value ) ) ;
public static float length ( float4 value ) = > MathF . Sqrt ( lengthsq ( value ) ) ;
public static float distance ( float2 left , float2 right ) = > length ( left - right ) ;
public static float distance ( float3 left , float3 right ) = > length ( left - right ) ;
public static float distance ( float4 left , float4 right ) = > length ( left - right ) ;
2026-09-14 18:04:04 +02:00
// dxc lowers normalize(x) to x * Rsqrt(Dot(x, x)), and DXIL Rsqrt is 1 / sqrt(src): a zero vector is 0 * inf = NaN.
public static float2 normalize ( float2 value ) = > value * ( 1.0f / MathF . Sqrt ( dot ( value , value ) ) ) ;
public static float3 normalize ( float3 value ) = > value * ( 1.0f / MathF . Sqrt ( dot ( value , value ) ) ) ;
public static float4 normalize ( float4 value ) = > value * ( 1.0f / MathF . Sqrt ( dot ( value , value ) ) ) ;
2026-07-12 19:33:59 +01:00
public static quaternion normalize ( quaternion value )
{
var length = MathF . Sqrt ( value . x * value . x + value . y * value . y + value . z * value . z + value . w * value . w ) ;
if ( length < = 1.0e-20f )
{
return quaternion . identity ;
}
var invLength = 1.0f / length ;
return new quaternion ( value . x * invLength , value . y * invLength , value . z * invLength , value . w * invLength ) ;
}
2026-05-08 01:49:44 +01:00
public static float2 reflect ( float2 incident , float2 normal ) = > incident - 2.0f * dot ( normal , incident ) * normal ;
public static float3 reflect ( float3 incident , float3 normal ) = > incident - 2.0f * dot ( normal , incident ) * normal ;
2026-06-10 21:22:43 +01:00
2026-07-12 19:33:59 +01:00
public static float2 rotate ( float2 value , float radians )
{
var s = sin ( radians ) ;
var c = cos ( radians ) ;
return new float2 ( value . x * c - value . y * s , value . x * s + value . y * c ) ;
}
public static float2 rotate_degrees ( float2 value , float degrees ) = > rotate ( value , radians ( degrees ) ) ;
2026-06-10 21:22:43 +01:00
public static float csum ( float2 value ) = > value . x + value . y ;
public static float csum ( float3 value ) = > value . x + value . y + value . z ;
public static float csum ( float4 value ) = > value . x + value . y + value . z + value . w ;
public static float decay ( float source , float lambda , float dt ) = > source * exp ( - lambda * dt ) ;
public static float2 decay ( float2 source , float lambda , float dt ) = > source * exp ( - lambda * dt ) ;
public static float3 decay ( float3 source , float lambda , float dt ) = > source * exp ( - lambda * dt ) ;
public static float4 decay ( float4 source , float lambda , float dt ) = > source * exp ( - lambda * dt ) ;
public static float2 decay ( float2 source , float2 lambda , float dt ) = > source * exp ( - lambda * dt ) ;
public static float3 decay ( float3 source , float3 lambda , float dt ) = > source * exp ( - lambda * dt ) ;
public static float4 decay ( float4 source , float4 lambda , float dt ) = > source * exp ( - lambda * dt ) ;
public static float damp ( float start , float end , float lambda , float dt ) = > lerp ( start , end , 1.0f - exp ( - lambda * dt ) ) ;
public static float2 damp ( float2 start , float2 end , float lambda , float dt ) = > lerp ( start , end , 1.0f - exp ( - lambda * dt ) ) ;
public static float3 damp ( float3 start , float3 end , float lambda , float dt ) = > lerp ( start , end , 1.0f - exp ( - lambda * dt ) ) ;
public static float4 damp ( float4 start , float4 end , float lambda , float dt ) = > lerp ( start , end , 1.0f - exp ( - lambda * dt ) ) ;
public static float first_order_intercept_time ( float shotSpeed , float3 targetRelativePosition , float3 targetRelativeVelocity )
{
var velocitySquared = lengthsq ( targetRelativeVelocity ) ;
if ( velocitySquared < 0.001f )
{
return 0.0f ;
}
var a = velocitySquared - shotSpeed * shotSpeed ;
if ( abs ( a ) < 0.001f )
{
var time = - lengthsq ( targetRelativePosition ) / ( 2.0f * dot ( targetRelativeVelocity , targetRelativePosition ) ) ;
return max ( time , 0.0f ) ;
}
var b = 2.0f * dot ( targetRelativeVelocity , targetRelativePosition ) ;
var c = lengthsq ( targetRelativePosition ) ;
var determinant = b * b - 4.0f * a * c ;
if ( determinant > 0.0f )
{
var root = sqrt ( determinant ) ;
var t1 = ( - b + root ) / ( 2.0f * a ) ;
var t2 = ( - b - root ) / ( 2.0f * a ) ;
if ( t1 > 0.0f )
{
return t2 > 0.0f ? min ( t1 , t2 ) : t1 ;
}
return max ( t2 , 0.0f ) ;
}
if ( determinant < 0.0f )
{
return 0.0f ;
}
return max ( - b / ( 2.0f * a ) , 0.0f ) ;
}
public static float3 first_order_intercept (
float3 shooterPosition ,
float3 shooterVelocity ,
float shotSpeed ,
float3 targetPosition ,
float3 targetVelocity )
{
var targetRelativePosition = targetPosition - shooterPosition ;
var targetRelativeVelocity = targetVelocity - shooterVelocity ;
var time = first_order_intercept_time ( shotSpeed , targetRelativePosition , targetRelativeVelocity ) ;
return targetPosition + time * targetRelativeVelocity ;
}
public static float distance_to_segment ( float2 point , float2 start , float2 end , out float2 closest )
2026-10-16 18:14:50 +02:00
{
closest = segment_closest ( point , start , end ) ;
return distance ( point , closest ) ;
}
/// <summary>The distance from <paramref name="point"/> to the segment <paramref name="start"/>-<paramref name="end"/>; a degenerate segment is its start.</summary>
public static float distance_to_segment ( float2 point , float2 start , float2 end ) = >
distance ( point , segment_closest ( point , start , end ) ) ;
/// <summary>The distance from <paramref name="point"/> to the segment <paramref name="start"/>-<paramref name="end"/>; a degenerate segment is its start.</summary>
public static float distance_to_segment ( float3 point , float3 start , float3 end ) = >
distance ( point , segment_closest ( point , start , end ) ) ;
2026-10-16 19:15:55 +02:00
/// <summary>The point of the segment <paramref name="start"/>-<paramref name="end"/> nearest <paramref name="point_value"/>; a degenerate segment is its start.</summary>
public static float2 segment_closest ( float2 point_value , float2 start , float2 end )
2026-06-10 21:22:43 +01:00
{
var segment = end - start ;
var segmentLengthSq = lengthsq ( segment ) ;
if ( segmentLengthSq < = 0.0f )
2026-10-16 18:14:50 +02:00
return start ;
2026-06-10 21:22:43 +01:00
2026-10-16 18:14:50 +02:00
var t = saturate ( dot ( point_value - start , segment ) / segmentLengthSq ) ;
return start + segment * t ;
}
2026-10-16 19:15:55 +02:00
/// <summary>The point of the segment <paramref name="start"/>-<paramref name="end"/> nearest <paramref name="point_value"/>; a degenerate segment is its start.</summary>
public static float3 segment_closest ( float3 point_value , float3 start , float3 end )
2026-10-16 18:14:50 +02:00
{
var segment = end - start ;
var segmentLengthSq = lengthsq ( segment ) ;
if ( segmentLengthSq < = 0.0f )
return start ;
var t = saturate ( dot ( point_value - start , segment ) / segmentLengthSq ) ;
return start + segment * t ;
2026-06-10 21:22:43 +01:00
}
public static float catmullrom ( float p0 , float p1 , float p2 , float p3 , float t )
{
var t2 = t * t ;
var t3 = t2 * t ;
return 0.5f * ( ( 2.0f * p1 ) + ( p2 - p0 ) * t + ( 2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3 ) * t2 + ( - p0 + 3.0f * p1 - 3.0f * p2 + p3 ) * t3 ) ;
}
public static float2 catmullrom ( float2 p0 , float2 p1 , float2 p2 , float2 p3 , float t ) = >
0.5f * ( ( 2.0f * p1 ) + ( p2 - p0 ) * t + ( 2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3 ) * ( t * t ) + ( - p0 + 3.0f * p1 - 3.0f * p2 + p3 ) * ( t * t * t ) ) ;
public static float3 catmullrom ( float3 p0 , float3 p1 , float3 p2 , float3 p3 , float t ) = >
0.5f * ( ( 2.0f * p1 ) + ( p2 - p0 ) * t + ( 2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3 ) * ( t * t ) + ( - p0 + 3.0f * p1 - 3.0f * p2 + p3 ) * ( t * t * t ) ) ;
public static float4 catmullrom ( float4 p0 , float4 p1 , float4 p2 , float4 p3 , float t ) = >
0.5f * ( ( 2.0f * p1 ) + ( p2 - p0 ) * t + ( 2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3 ) * ( t * t ) + ( - p0 + 3.0f * p1 - 3.0f * p2 + p3 ) * ( t * t * t ) ) ;
public static float quadratic_bezier ( float p0 , float p1 , float p2 , float t )
{
t = saturate ( t ) ;
var inv = 1.0f - t ;
return inv * inv * p0 + 2.0f * inv * t * p1 + t * t * p2 ;
}
public static float3 quadratic_bezier ( float3 p0 , float3 p1 , float3 p2 , float t )
{
t = saturate ( t ) ;
var inv = 1.0f - t ;
return inv * inv * p0 + 2.0f * inv * t * p1 + t * t * p2 ;
}
public static float cubic_bezier ( float p0 , float p1 , float p2 , float p3 , float t )
{
t = saturate ( t ) ;
var inv = 1.0f - t ;
return inv * inv * inv * p0 + 3.0f * inv * inv * t * p1 + 3.0f * inv * t * t * p2 + t * t * t * p3 ;
}
public static float3 cubic_bezier ( float3 p0 , float3 p1 , float3 p2 , float3 p3 , float t )
{
t = saturate ( t ) ;
var inv = 1.0f - t ;
return inv * inv * inv * p0 + 3.0f * inv * inv * t * p1 + 3.0f * inv * t * t * p2 + t * t * t * p3 ;
}
public static float smoothstep01 ( float value ) = > smoothstep ( 0.0f , 1.0f , value ) ;
public static float smootherstep ( float value )
{
value = saturate ( value ) ;
return value * value * value * ( value * ( value * 6.0f - 15.0f ) + 10.0f ) ;
}
public static float hash ( float value ) = > frac ( sin ( value ) * 43758.5453f ) ;
public static float hash ( float2 value ) = > hash ( dot ( value , new float2 ( 127.1f , 311.7f ) ) ) ;
public static float hash ( float3 value ) = > hash ( dot ( value , new float3 ( 127.1f , 311.7f , 74.7f ) ) ) ;
Add PCG integer hashes to CultMath and release 0.2.3
CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and
returns float. Unity.Mathematics' hash returned a well-mixed uint, and
seeding from the float hash collapses: a 100x100 position grid gave 1625
distinct values in Aetheria's zone generator.
- math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4)
follow Jarzynski and Olano, "Hash Functions for GPU Rendering"
(JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32.
pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the
constants checked against pcg-c's pcg_variants.h.
- CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit
patterns in int3/int4. The float overloads hash IEEE-754 bits via the
HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at
0, the paper's route for 2 -> N inputs.
- The HLSL mirror gains the same functions, written over scalar uints
so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean)
and in C#. HlslSourceCompatibilityTests now compares int and uint
arguments and results by value; a wrong shift in the mirror's pcg4d
gives 208 mismatches.
- Known-answer vectors come from an independent Python transcription.
pcg3d separates a 100x100 half-unit grid (the full int3). One
component alone has one birthday collision there, as a 32-bit value
can. The same grid on the integer lattice has none.
- design.md states that the hashes are integer-exact in HLSL and on the
CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does.
- Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:53 +02:00
public static uint asuint ( float value ) = > ( uint ) BitConverter . SingleToInt32Bits ( value ) ;
CultMath intervals: cultmath_iv_* over float2(lo, hi) in C# and HLSL, with snoise and fbm ball bounds
math.Interval.cs and shaders/CultMath.Interval.hlsl (included from CultMath.hlsl after Phacelle;
both tracked copies) add iv_point, iv_add, iv_sub, iv_neg, iv_mul, iv_scale, iv_abs, iv_min,
iv_max, iv_sqr, iv_sqrt, iv_exp, iv_clamp, iv_saturate, iv_smoothstep, iv_lerp, iv_snoise_ball
and iv_fbm_ball, and SNOISE_LIPSCHITZ = 10.099261 (1.10 x the refined max |snoise_grad| that
NoiseBoundTests.MeasureLipschitz prints). iv_exp and iv_smoothstep widen by one ulp through
asfloat(uint), added beside asuint as the HLSL intrinsic. The mirror test now compares 52
functions; the Unity package and the NuGet content files carry the new include.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:52:33 +02:00
public static float asfloat ( uint value ) = > BitConverter . Int32BitsToSingle ( ( int ) value ) ;
Add PCG integer hashes to CultMath and release 0.2.3
CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and
returns float. Unity.Mathematics' hash returned a well-mixed uint, and
seeding from the float hash collapses: a 100x100 position grid gave 1625
distinct values in Aetheria's zone generator.
- math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4)
follow Jarzynski and Olano, "Hash Functions for GPU Rendering"
(JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32.
pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the
constants checked against pcg-c's pcg_variants.h.
- CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit
patterns in int3/int4. The float overloads hash IEEE-754 bits via the
HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at
0, the paper's route for 2 -> N inputs.
- The HLSL mirror gains the same functions, written over scalar uints
so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean)
and in C#. HlslSourceCompatibilityTests now compares int and uint
arguments and results by value; a wrong shift in the mirror's pcg4d
gives 208 mismatches.
- Known-answer vectors come from an independent Python transcription.
pcg3d separates a 100x100 half-unit grid (the full int3). One
component alone has one birthday collision there, as a 32-bit value
can. The same grid on the integer lattice has none.
- design.md states that the hashes are integer-exact in HLSL and on the
CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does.
- Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:53 +02:00
// PCG hashes from Jarzynski and Olano, "Hash Functions for GPU Rendering" (JCGT 9(3), 2020). Integer-only, so
// dxc and C# agree bit for bit. pcg3d/pcg4d carry uint bit patterns in int3/int4; float overloads hash IEEE bits.
public static uint pcg ( uint value )
{
var state = value * 747796405 u + 2891336453 u ;
var word = ( ( state > > ( int ) ( ( state > > 28 ) + 4 ) ) ^ state ) * 277803737 u ;
return ( word > > 22 ) ^ word ;
}
public static int3 pcg3d ( int3 value )
{
uint x = ( uint ) value . x * 1664525 u + 1013904223 u , y = ( uint ) value . y * 1664525 u + 1013904223 u , z = ( uint ) value . z * 1664525 u + 1013904223 u ;
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 new int3 ( ( int ) x , ( int ) y , ( int ) z ) ;
}
// The paper hashes 2 -> N inputs through pcg3d with the unused component held constant.
public static int3 pcg3d ( float2 value ) = > pcg3d ( new float3 ( value , 0.0f ) ) ;
public static int3 pcg3d ( float3 value ) = > pcg3d ( new int3 ( ( int ) asuint ( value . x ) , ( int ) asuint ( value . y ) , ( int ) asuint ( value . z ) ) ) ;
public static int4 pcg4d ( int4 value )
{
uint x = ( uint ) value . x * 1664525 u + 1013904223 u , y = ( uint ) value . y * 1664525 u + 1013904223 u ;
uint z = ( uint ) value . z * 1664525 u + 1013904223 u , w = ( uint ) value . w * 1664525 u + 1013904223 u ;
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 new int4 ( ( int ) x , ( int ) y , ( int ) z , ( int ) w ) ;
}
public static int4 pcg4d ( float4 value ) = >
pcg4d ( new int4 ( ( int ) asuint ( value . x ) , ( int ) asuint ( value . y ) , ( int ) asuint ( value . z ) , ( int ) asuint ( value . w ) ) ) ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
public static float log ( float value ) = > MathF . Log ( value ) ;
public static float2 log ( float2 value ) = > new ( log ( value . x ) , log ( value . y ) ) ;
public static float3 log ( float3 value ) = > new ( log ( value . x ) , log ( value . y ) , log ( value . z ) ) ;
public static float4 log ( float4 value ) = > new ( log ( value . x ) , log ( value . y ) , log ( value . z ) , log ( value . w ) ) ;
public static float2 pow ( float2 value , float2 power ) = > new ( pow ( value . x , power . x ) , pow ( value . y , power . y ) ) ;
public static float3 pow ( float3 value , float3 power ) = > new ( pow ( value . x , power . x ) , pow ( value . y , power . y ) , pow ( value . z , power . z ) ) ;
public static float4 pow ( float4 value , float4 power ) = > new ( pow ( value . x , power . x ) , pow ( value . y , power . y ) , pow ( value . z , power . z ) , pow ( value . w , power . w ) ) ;
public static float2 sqrt ( float2 value ) = > new ( sqrt ( value . x ) , sqrt ( value . y ) ) ;
public static float3 sqrt ( float3 value ) = > new ( sqrt ( value . x ) , sqrt ( value . y ) , sqrt ( value . z ) ) ;
public static float4 sqrt ( float4 value ) = > new ( sqrt ( value . x ) , sqrt ( value . y ) , sqrt ( value . z ) , sqrt ( value . w ) ) ;
public static float2 sin ( float2 value ) = > new ( sin ( value . x ) , sin ( value . y ) ) ;
public static float3 sin ( float3 value ) = > new ( sin ( value . x ) , sin ( value . y ) , sin ( value . z ) ) ;
public static float4 sin ( float4 value ) = > new ( sin ( value . x ) , sin ( value . y ) , sin ( value . z ) , sin ( value . w ) ) ;
public static float2 cos ( float2 value ) = > new ( cos ( value . x ) , cos ( value . y ) ) ;
public static float3 cos ( float3 value ) = > new ( cos ( value . x ) , cos ( value . y ) , cos ( value . z ) ) ;
public static float4 cos ( float4 value ) = > new ( cos ( value . x ) , cos ( value . y ) , cos ( value . z ) , cos ( value . w ) ) ;
public static float2 acos ( float2 value ) = > new ( acos ( value . x ) , acos ( value . y ) ) ;
public static float3 acos ( float3 value ) = > new ( acos ( value . x ) , acos ( value . y ) , acos ( value . z ) ) ;
public static float4 acos ( float4 value ) = > new ( acos ( value . x ) , acos ( value . y ) , acos ( value . z ) , acos ( value . w ) ) ;
public static float2 atan2 ( float2 y , float2 x ) = > new ( atan2 ( y . x , x . x ) , atan2 ( y . y , x . y ) ) ;
public static float3 atan2 ( float3 y , float3 x ) = > new ( atan2 ( y . x , x . x ) , atan2 ( y . y , x . y ) , atan2 ( y . z , x . z ) ) ;
public static float4 atan2 ( float4 y , float4 x ) = > new ( atan2 ( y . x , x . x ) , atan2 ( y . y , x . y ) , atan2 ( y . z , x . z ) , atan2 ( y . w , x . w ) ) ;
2026-09-14 14:47:10 +02:00
// DXIL FMin/FMax take double overloads with the same rules as float (see min/max above).
public static double min ( double left , double right ) = > left < right | | double . IsNaN ( right ) ? left : right ;
public static double max ( double left , double right ) = > left > = right | | double . IsNaN ( right ) ? left : right ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
public static bool any ( bool2 value ) = > value . x | | value . y ;
public static bool any ( bool3 value ) = > value . x | | value . y | | value . z ;
public static bool any ( bool4 value ) = > value . x | | value . y | | value . z | | value . w ;
public static bool all ( bool2 value ) = > value . x & & value . y ;
public static bool all ( bool3 value ) = > value . x & & value . y & & value . z ;
public static bool all ( bool4 value ) = > value . x & & value . y & & value . z & & value . w ;
Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.
Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.
Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.
Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
// HLSL any/all on numeric vectors test components against zero (NaN != 0 counts as true).
public static bool any ( float2 value ) = > value . x ! = 0.0f | | value . y ! = 0.0f ;
public static bool any ( float3 value ) = > value . x ! = 0.0f | | value . y ! = 0.0f | | value . z ! = 0.0f ;
public static bool any ( float4 value ) = > value . x ! = 0.0f | | value . y ! = 0.0f | | value . z ! = 0.0f | | value . w ! = 0.0f ;
public static bool all ( float2 value ) = > value . x ! = 0.0f & & value . y ! = 0.0f ;
public static bool all ( float3 value ) = > value . x ! = 0.0f & & value . y ! = 0.0f & & value . z ! = 0.0f ;
public static bool all ( float4 value ) = > value . x ! = 0.0f & & value . y ! = 0.0f & & value . z ! = 0.0f & & value . w ! = 0.0f ;
public static bool any ( int2 value ) = > value . x ! = 0 | | value . y ! = 0 ;
public static bool any ( int3 value ) = > value . x ! = 0 | | value . y ! = 0 | | value . z ! = 0 ;
public static bool any ( int4 value ) = > value . x ! = 0 | | value . y ! = 0 | | value . z ! = 0 | | value . w ! = 0 ;
public static bool all ( int2 value ) = > value . x ! = 0 & & value . y ! = 0 ;
public static bool all ( int3 value ) = > value . x ! = 0 & & value . y ! = 0 & & value . z ! = 0 ;
public static bool all ( int4 value ) = > value . x ! = 0 & & value . y ! = 0 & & value . z ! = 0 & & value . w ! = 0 ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
// HLSL 2021 argument order: select(condition, whenTrue, whenFalse).
// Unity.Mathematics' select(falseValue, trueValue, condition) is the reverse.
public static float select ( bool condition , float whenTrue , float whenFalse ) = > condition ? whenTrue : whenFalse ;
public static float2 select ( bool2 condition , float2 whenTrue , float2 whenFalse ) = >
new ( condition . x ? whenTrue . x : whenFalse . x , condition . y ? whenTrue . y : whenFalse . y ) ;
public static float3 select ( bool3 condition , float3 whenTrue , float3 whenFalse ) = >
new ( condition . x ? whenTrue . x : whenFalse . x , condition . y ? whenTrue . y : whenFalse . y , condition . z ? whenTrue . z : whenFalse . z ) ;
public static float4 select ( bool4 condition , float4 whenTrue , float4 whenFalse ) = >
new ( condition . x ? whenTrue . x : whenFalse . x , condition . y ? whenTrue . y : whenFalse . y , condition . z ? whenTrue . z : whenFalse . z , condition . w ? whenTrue . w : whenFalse . w ) ;
// HLSL mul: mul(m, v) treats v as a column vector, mul(v, m) as a row vector.
public static float mul ( float left , float right ) = > left * right ;
public static float2 mul ( float2x2 m , float2 v ) = >
new ( m . _m00 * v . x + m . _m01 * v . y , m . _m10 * v . x + m . _m11 * v . y ) ;
public static float2 mul ( float2 v , float2x2 m ) = >
new ( v . x * m . _m00 + v . y * m . _m10 , v . x * m . _m01 + v . y * m . _m11 ) ;
public static float2x2 mul ( float2x2 a , float2x2 b ) = >
new (
a . _m00 * b . _m00 + a . _m01 * b . _m10 , a . _m00 * b . _m01 + a . _m01 * b . _m11 ,
a . _m10 * b . _m00 + a . _m11 * b . _m10 , a . _m10 * b . _m01 + a . _m11 * b . _m11 ) ;
public static float3 mul ( float3x3 m , float3 v ) = >
new (
m . _m00 * v . x + m . _m01 * v . y + m . _m02 * v . z ,
m . _m10 * v . x + m . _m11 * v . y + m . _m12 * v . z ,
m . _m20 * v . x + m . _m21 * v . y + m . _m22 * v . z ) ;
public static float3 mul ( float3 v , float3x3 m ) = >
new (
v . x * m . _m00 + v . y * m . _m10 + v . z * m . _m20 ,
v . x * m . _m01 + v . y * m . _m11 + v . z * m . _m21 ,
v . x * m . _m02 + v . y * m . _m12 + v . z * m . _m22 ) ;
public static float3x3 mul ( float3x3 a , float3x3 b ) = >
new (
a . _m00 * b . _m00 + a . _m01 * b . _m10 + a . _m02 * b . _m20 ,
a . _m00 * b . _m01 + a . _m01 * b . _m11 + a . _m02 * b . _m21 ,
a . _m00 * b . _m02 + a . _m01 * b . _m12 + a . _m02 * b . _m22 ,
a . _m10 * b . _m00 + a . _m11 * b . _m10 + a . _m12 * b . _m20 ,
a . _m10 * b . _m01 + a . _m11 * b . _m11 + a . _m12 * b . _m21 ,
a . _m10 * b . _m02 + a . _m11 * b . _m12 + a . _m12 * b . _m22 ,
a . _m20 * b . _m00 + a . _m21 * b . _m10 + a . _m22 * b . _m20 ,
a . _m20 * b . _m01 + a . _m21 * b . _m11 + a . _m22 * b . _m21 ,
a . _m20 * b . _m02 + a . _m21 * b . _m12 + a . _m22 * b . _m22 ) ;
2026-09-22 20:42:57 +02:00
// Abramowitz & Stegun 7.1.26, a single-precision rational/exponential fit with a stated
// max absolute error of 1.5e-7. erf is odd, so only x >= 0 is fit and the sign is restored.
Fix erfinv's sign inversion at the domain edges
math.erfinv(1.0f) returned -infinity and erfinv(-1.0f) returned
+infinity: at value = +-1, w = -log((1-value)(1+value)) = -log(0) =
+infinity, the tail branch's leading coefficient is negative, and
`return p * value` delivered the inverted infinity. erf saturates to
exactly 1.0f at 5.543 sigma, so any interval edge beyond that reaches
this bug in ordinary use.
Guard both edges explicitly before the polynomial runs: erfinv(1) is
now +infinity, erfinv(-1) is -infinity, and anything strictly outside
[-1, 1] (including +-infinity) is NaN, which is the correct answer and
was already the fallthrough behavior. Documented the domain and the
edge behavior in a comment, matching the house style `sign` already
uses at math.cs:84. The approximation itself is unchanged.
Also recorded the true measured accuracy beside erf and erfinv: worst
absolute error 6.621e-7 and 5.066e-7 respectively, both inside the
2e-6 bar this package's tests pin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:29:04 +02:00
// Measured against an independent reference: worst absolute error 6.621e-7, inside the
// 2e-6 bar this package's tests pin.
2026-09-22 20:42:57 +02:00
public static float erf ( float value )
{
const float p = 0.3275911f ;
const float a1 = 0.254829592f ;
const float a2 = - 0.284496736f ;
const float a3 = 1.421413741f ;
const float a4 = - 1.453152027f ;
const float a5 = 1.061405429f ;
var s = sign ( value ) ;
var x = abs ( value ) ;
var t = 1.0f / ( 1.0f + p * x ) ;
var poly = ( ( ( ( a5 * t + a4 ) * t + a3 ) * t + a2 ) * t + a1 ) * t ;
var result = 1.0f - poly * exp ( - x * x ) ;
return s * result ;
}
// Giles, "Approximating the erfinv function" (GPU Computing Gems, 2010): a single-precision
// minimax polynomial in two ranges of w = -ln((1-x)(1+x)), central and tail.
Fix erfinv's sign inversion at the domain edges
math.erfinv(1.0f) returned -infinity and erfinv(-1.0f) returned
+infinity: at value = +-1, w = -log((1-value)(1+value)) = -log(0) =
+infinity, the tail branch's leading coefficient is negative, and
`return p * value` delivered the inverted infinity. erf saturates to
exactly 1.0f at 5.543 sigma, so any interval edge beyond that reaches
this bug in ordinary use.
Guard both edges explicitly before the polynomial runs: erfinv(1) is
now +infinity, erfinv(-1) is -infinity, and anything strictly outside
[-1, 1] (including +-infinity) is NaN, which is the correct answer and
was already the fallthrough behavior. Documented the domain and the
edge behavior in a comment, matching the house style `sign` already
uses at math.cs:84. The approximation itself is unchanged.
Also recorded the true measured accuracy beside erf and erfinv: worst
absolute error 6.621e-7 and 5.066e-7 respectively, both inside the
2e-6 bar this package's tests pin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:29:04 +02:00
// Domain is [-1, 1]. Outside it, including +-infinity, the result is NaN (there is no real
// inverse). At the edges the mathematical limit is infinite: erfinv(1) = +infinity and
// erfinv(-1) = -infinity. The unguarded polynomial hits log(0) = -infinity at those edges and
// the tail branch's leading coefficient is negative, so it delivers the wrong sign; guard the
// edges explicitly instead of trusting the fit through the singularity.
// Measured against an independent reference: worst absolute error 5.066e-7, and the
// erf(erfinv(y)) round trip 6.109e-7 worst case, both inside the 2e-6 bar this package's
// tests pin.
2026-09-22 20:42:57 +02:00
public static float erfinv ( float value )
{
Fix erfinv's sign inversion at the domain edges
math.erfinv(1.0f) returned -infinity and erfinv(-1.0f) returned
+infinity: at value = +-1, w = -log((1-value)(1+value)) = -log(0) =
+infinity, the tail branch's leading coefficient is negative, and
`return p * value` delivered the inverted infinity. erf saturates to
exactly 1.0f at 5.543 sigma, so any interval edge beyond that reaches
this bug in ordinary use.
Guard both edges explicitly before the polynomial runs: erfinv(1) is
now +infinity, erfinv(-1) is -infinity, and anything strictly outside
[-1, 1] (including +-infinity) is NaN, which is the correct answer and
was already the fallthrough behavior. Documented the domain and the
edge behavior in a comment, matching the house style `sign` already
uses at math.cs:84. The approximation itself is unchanged.
Also recorded the true measured accuracy beside erf and erfinv: worst
absolute error 6.621e-7 and 5.066e-7 respectively, both inside the
2e-6 bar this package's tests pin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:29:04 +02:00
if ( value > = 1.0f ) return value > 1.0f ? float . NaN : float . PositiveInfinity ;
if ( value < = - 1.0f ) return value < - 1.0f ? float . NaN : float . NegativeInfinity ;
2026-09-22 20:42:57 +02:00
var w = - log ( ( 1.0f - value ) * ( 1.0f + value ) ) ;
float p ;
if ( w < 5.0f )
{
w - = 2.5f ;
p = 2.81022636e-08f ;
p = 3.43273939e-07f + p * w ;
p = - 3.5233877e-06f + p * w ;
p = - 4.39150654e-06f + p * w ;
p = 0.00021858087f + p * w ;
p = - 0.00125372503f + p * w ;
p = - 0.00417768164f + p * w ;
p = 0.246640727f + p * w ;
p = 1.50140941f + p * w ;
}
else
{
w = sqrt ( w ) - 3.0f ;
p = - 0.000200214257f ;
p = 0.000100950558f + p * w ;
p = 0.00134934322f + p * w ;
p = - 0.00367342844f + p * w ;
p = 0.00573950773f + p * w ;
p = - 0.0076224613f + p * w ;
p = 0.00943887047f + p * w ;
p = 1.00167406f + p * w ;
p = 2.83297682f + p * w ;
}
return p * value ;
}
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
// Ashima Arts / Ian McEwan 3D simplex noise (MIT), mirrored by cultmath_snoise(float3)
CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
// in shaders/CultMath.hlsl with the same float32 evaluation order. Follows stegu/webgl-noise
// src/noise3D.glsl at 22434e04d7 (kernel radius^2 0.5, scale 105, permute (34x+10)x); see
// THIRD-PARTY-NOTICES.md. Continuous across simplex-cell boundaries
snoise tie rule: x over y, y over z, x over z (a total order) in C# and both HLSL copies
The cyclic non-strict step(x0.yzx, x0.xyz) made the four corners a non-simplex
where x0's components are equal: the origin and every (t, t, t) read a different
function (snoise(0) was -0.4358730, jumps up to 0.77). One strict compare, z
against x, gives simplexnoise1234's order. Off the tie set old and new return
the same bits.
NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal fails against the old
rule (0.054 within 1 ulp of (-53.6, -53.6, -53.6)). The origin golden is 0 and a
(1.25, 1.25, 1.25) diagonal row joins it. SNOISE_LIPSCHITZ and the GLSL follow
in the next commits, so the ratio pin and CommittedGlslEqualsLowering are red here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:07:02 +02:00
// (NoiseGradTests.SnoiseAndItsGradientAreContinuousAcrossSimplexCellBoundaries). Ties in x0 resolve
// in Gustavson's simplexnoise1234 order (x over y, y over z, x over z), a total order, so the four
// corners are always a simplex; upstream's step is cyclic and degenerates where x0's components are
// equal (NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal).
CultMath intervals: cultmath_iv_* over float2(lo, hi) in C# and HLSL, with snoise and fbm ball bounds
math.Interval.cs and shaders/CultMath.Interval.hlsl (included from CultMath.hlsl after Phacelle;
both tracked copies) add iv_point, iv_add, iv_sub, iv_neg, iv_mul, iv_scale, iv_abs, iv_min,
iv_max, iv_sqr, iv_sqrt, iv_exp, iv_clamp, iv_saturate, iv_smoothstep, iv_lerp, iv_snoise_ball
and iv_fbm_ball, and SNOISE_LIPSCHITZ = 10.099261 (1.10 x the refined max |snoise_grad| that
NoiseBoundTests.MeasureLipschitz prints). iv_exp and iv_smoothstep widen by one ulp through
asfloat(uint), added beside asuint as the HLSL intrinsic. The mirror test now compares 52
functions; the Unity package and the NuGet content files carry the new include.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:52:33 +02:00
// SNOISE_LIPSCHITZ (math.Interval.cs) is measured from this kernel: a change here must re-run
// NoiseBoundTests.MeasureLipschitz and re-pin it.
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
public static float snoise ( float3 value )
{
const float cx = 1.0f / 6.0f ;
const float cy = 1.0f / 3.0f ;
var i = floor ( value + dot ( value , new float3 ( cy , cy , cy ) ) ) ;
var x0 = value - i + dot ( i , new float3 ( cx , cx , cx ) ) ;
snoise tie rule: x over y, y over z, x over z (a total order) in C# and both HLSL copies
The cyclic non-strict step(x0.yzx, x0.xyz) made the four corners a non-simplex
where x0's components are equal: the origin and every (t, t, t) read a different
function (snoise(0) was -0.4358730, jumps up to 0.77). One strict compare, z
against x, gives simplexnoise1234's order. Off the tie set old and new return
the same bits.
NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal fails against the old
rule (0.054 within 1 ulp of (-53.6, -53.6, -53.6)). The origin golden is 0 and a
(1.25, 1.25, 1.25) diagonal row joins it. SNOISE_LIPSCHITZ and the GLSL follow
in the next commits, so the ratio pin and CommittedGlslEqualsLowering are red here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:07:02 +02:00
var g = new float3 ( step ( x0 . y , x0 . x ) , step ( x0 . z , x0 . y ) , 1.0f - step ( x0 . z , x0 . x ) ) ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
var l = 1.0f - g ;
var lzxy = new float3 ( l . z , l . x , l . y ) ;
var i1 = min ( g , lzxy ) ;
var i2 = max ( g , lzxy ) ;
var x1 = x0 - i1 + cx ;
var x2 = x0 - i2 + cy ;
var x3 = x0 - 0.5f ;
i = snoise_mod289 ( i ) ;
var p = snoise_permute ( snoise_permute ( snoise_permute (
i . z + new float4 ( 0.0f , i1 . z , i2 . z , 1.0f ) ) + i . y + new float4 ( 0.0f , i1 . y , i2 . y , 1.0f ) ) + i . x + new float4 ( 0.0f , i1 . x , i2 . x , 1.0f ) ) ;
const float n = 0.142857142857f ;
var ns = new float3 ( 2.0f * n , 0.5f * n - 1.0f , n ) ;
var j = p - 49.0f * floor ( p * ns . z * ns . z ) ;
var xs = floor ( j * ns . z ) ;
var ys = floor ( j - 7.0f * xs ) ;
var x = xs * ns . x + ns . y ;
var y = ys * ns . x + ns . y ;
var h = 1.0f - abs ( x ) - abs ( y ) ;
var b0 = new float4 ( x . x , x . y , y . x , y . y ) ;
var b1 = new float4 ( x . z , x . w , y . z , y . w ) ;
var s0 = floor ( b0 ) * 2.0f + 1.0f ;
var s1 = floor ( b1 ) * 2.0f + 1.0f ;
var sh = - step ( h , 0.0f ) ;
var a0 = new float4 ( b0 . x , b0 . z , b0 . y , b0 . w ) + new float4 ( s0 . x , s0 . z , s0 . y , s0 . w ) * new float4 ( sh . x , sh . x , sh . y , sh . y ) ;
var a1 = new float4 ( b1 . x , b1 . z , b1 . y , b1 . w ) + new float4 ( s1 . x , s1 . z , s1 . y , s1 . w ) * new float4 ( sh . z , sh . z , sh . w , sh . w ) ;
var p0 = new float3 ( a0 . x , a0 . y , h . x ) ;
var p1 = new float3 ( a0 . z , a0 . w , h . y ) ;
var p2 = new float3 ( a1 . x , a1 . y , h . z ) ;
var p3 = new float3 ( a1 . z , a1 . w , h . w ) ;
var norm = 1.79284291400159f - 0.85373472095314f * new float4 ( dot ( p0 , p0 ) , dot ( p1 , p1 ) , dot ( p2 , p2 ) , dot ( p3 , p3 ) ) ;
p0 * = norm . x ;
p1 * = norm . y ;
p2 * = norm . z ;
p3 * = norm . w ;
CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
var m = max ( 0.5f - new float4 ( dot ( x0 , x0 ) , dot ( x1 , x1 ) , dot ( x2 , x2 ) , dot ( x3 , x3 ) ) , 0.0f ) ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
m = m * m ;
CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
return 105.0f * dot ( m * m , new float4 ( dot ( p0 , x0 ) , dot ( p1 , x1 ) , dot ( p2 , x2 ) , dot ( p3 , x3 ) ) ) ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
}
2026-07-17 07:45:28 +01:00
// Ashima Arts / Ian McEwan 2D simplex noise, kept component-explicit so
// the C# and HLSL mirrors preserve the same float32 evaluation order.
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
public static float snoise ( float2 value )
2026-07-17 07:45:28 +01:00
{
var c = new float4 (
0.211324865405187f ,
0.366025403784439f ,
- 0.577350269189626f ,
0.024390243902439f ) ;
var i = floor ( value + dot ( value , new float2 ( c . y , c . y ) ) ) ;
var x0 = value - i + dot ( i , new float2 ( c . x , c . x ) ) ;
var i1 = x0 . x > x0 . y ? new float2 ( 1.0f , 0.0f ) : new float2 ( 0.0f , 1.0f ) ;
var x12 = new float4 (
x0 . x + c . x - i1 . x ,
x0 . y + c . x - i1 . y ,
x0 . x + c . z ,
x0 . y + c . z ) ;
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
i = snoise_mod289 ( i ) ;
var p = snoise_permute ( snoise_permute ( i . y + new float3 ( 0.0f , i1 . y , 1.0f ) ) + i . x + new float3 ( 0.0f , i1 . x , 1.0f ) ) ;
2026-07-17 07:45:28 +01:00
var m = max ( 0.5f - new float3 (
dot ( x0 , x0 ) ,
x12 . x * x12 . x + x12 . y * x12 . y ,
x12 . z * x12 . z + x12 . w * x12 . w ) , 0.0f ) ;
m = m * m ;
m = m * m ;
var x = 2.0f * frac ( p * c . w ) - 1.0f ;
var h = abs ( x ) - 0.5f ;
var ox = floor ( x + 0.5f ) ;
var a0 = x - ox ;
m * = 1.79284291400159f - 0.85373472095314f * ( a0 * a0 + h * h ) ;
var g = new float3 (
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.0f * dot ( m , g ) ;
}
Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.
- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
public mutable fields instead of record structs. Indexers and swizzles
(float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
GetHashCode stay value-based so vectors remain dictionary keys.
quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.
Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
private static float2 snoise_mod289 ( float2 value ) = > value - floor ( value * ( 1.0f / 289.0f ) ) * 289.0f ;
private static float3 snoise_mod289 ( float3 value ) = > value - floor ( value * ( 1.0f / 289.0f ) ) * 289.0f ;
private static float4 snoise_mod289 ( float4 value ) = > value - floor ( value * ( 1.0f / 289.0f ) ) * 289.0f ;
CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
private static float3 snoise_permute ( float3 value ) = > snoise_mod289 ( ( ( value * 34.0f ) + 10.0f ) * value ) ;
private static float4 snoise_permute ( float4 value ) = > snoise_mod289 ( ( ( value * 34.0f ) + 10.0f ) * value ) ;
// Analytic gradient of snoise(float3) (invariant 8), following the differentiation in
// webgl-noise src/noise3Dgrad.glsl (Ashima Arts / McEwan, MIT), which uses the same kernel
// radius^2 0.5, permutation (34x+10)x and scale 105 as snoise(float3) (stegu/webgl-noise
// 22434e04d7). Every local up through p0..p3 and x0..x3 below is snoise(float3)'s own derivation
// verbatim, so .w is that same field (NoiseGradTests.SnoiseGradValueIsBitEqualToSnoise pins the
// two bit-equal over a random sweep). Per corner, m0 = max(0.5 - dot(x,x), 0), so dm0/dx = -2x,
// and d(m0^4 * dot(p,x))/dx = 4*m0^3*(-2x)*dot(p,x) + m0^4*p = -8*m0^3*dot(p,x)*x + m0^4*p (p is
// that corner's own gradient constant, independent of x); summed over the four corners and
// scaled by the same 105 as the value.
2026-09-26 02:36:52 +02:00
public static float4 snoise_grad ( float3 value )
{
const float cx = 1.0f / 6.0f ;
const float cy = 1.0f / 3.0f ;
var i = floor ( value + dot ( value , new float3 ( cy , cy , cy ) ) ) ;
var x0 = value - i + dot ( i , new float3 ( cx , cx , cx ) ) ;
snoise tie rule: x over y, y over z, x over z (a total order) in C# and both HLSL copies
The cyclic non-strict step(x0.yzx, x0.xyz) made the four corners a non-simplex
where x0's components are equal: the origin and every (t, t, t) read a different
function (snoise(0) was -0.4358730, jumps up to 0.77). One strict compare, z
against x, gives simplexnoise1234's order. Off the tie set old and new return
the same bits.
NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal fails against the old
rule (0.054 within 1 ulp of (-53.6, -53.6, -53.6)). The origin golden is 0 and a
(1.25, 1.25, 1.25) diagonal row joins it. SNOISE_LIPSCHITZ and the GLSL follow
in the next commits, so the ratio pin and CommittedGlslEqualsLowering are red here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 03:07:02 +02:00
var g = new float3 ( step ( x0 . y , x0 . x ) , step ( x0 . z , x0 . y ) , 1.0f - step ( x0 . z , x0 . x ) ) ;
2026-09-26 02:36:52 +02:00
var l = 1.0f - g ;
var lzxy = new float3 ( l . z , l . x , l . y ) ;
var i1 = min ( g , lzxy ) ;
var i2 = max ( g , lzxy ) ;
var x1 = x0 - i1 + cx ;
var x2 = x0 - i2 + cy ;
var x3 = x0 - 0.5f ;
i = snoise_mod289 ( i ) ;
var p = snoise_permute ( snoise_permute ( snoise_permute (
i . z + new float4 ( 0.0f , i1 . z , i2 . z , 1.0f ) ) + i . y + new float4 ( 0.0f , i1 . y , i2 . y , 1.0f ) ) + i . x + new float4 ( 0.0f , i1 . x , i2 . x , 1.0f ) ) ;
const float n = 0.142857142857f ;
var ns = new float3 ( 2.0f * n , 0.5f * n - 1.0f , n ) ;
var j = p - 49.0f * floor ( p * ns . z * ns . z ) ;
var xs = floor ( j * ns . z ) ;
var ys = floor ( j - 7.0f * xs ) ;
var x = xs * ns . x + ns . y ;
var y = ys * ns . x + ns . y ;
var h = 1.0f - abs ( x ) - abs ( y ) ;
var b0 = new float4 ( x . x , x . y , y . x , y . y ) ;
var b1 = new float4 ( x . z , x . w , y . z , y . w ) ;
var s0 = floor ( b0 ) * 2.0f + 1.0f ;
var s1 = floor ( b1 ) * 2.0f + 1.0f ;
var sh = - step ( h , 0.0f ) ;
var a0 = new float4 ( b0 . x , b0 . z , b0 . y , b0 . w ) + new float4 ( s0 . x , s0 . z , s0 . y , s0 . w ) * new float4 ( sh . x , sh . x , sh . y , sh . y ) ;
var a1 = new float4 ( b1 . x , b1 . z , b1 . y , b1 . w ) + new float4 ( s1 . x , s1 . z , s1 . y , s1 . w ) * new float4 ( sh . z , sh . z , sh . w , sh . w ) ;
var p0 = new float3 ( a0 . x , a0 . y , h . x ) ;
var p1 = new float3 ( a0 . z , a0 . w , h . y ) ;
var p2 = new float3 ( a1 . x , a1 . y , h . z ) ;
var p3 = new float3 ( a1 . z , a1 . w , h . w ) ;
var norm = 1.79284291400159f - 0.85373472095314f * new float4 ( dot ( p0 , p0 ) , dot ( p1 , p1 ) , dot ( p2 , p2 ) , dot ( p3 , p3 ) ) ;
p0 * = norm . x ;
p1 * = norm . y ;
p2 * = norm . z ;
p3 * = norm . w ;
CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
var m0 = max ( 0.5f - new float4 ( dot ( x0 , x0 ) , dot ( x1 , x1 ) , dot ( x2 , x2 ) , dot ( x3 , x3 ) ) , 0.0f ) ;
2026-09-26 02:36:52 +02:00
var m2 = m0 * m0 ;
var m3 = m2 * m0 ;
var m4 = m2 * m2 ;
var px = new float4 ( dot ( p0 , x0 ) , dot ( p1 , x1 ) , dot ( p2 , x2 ) , dot ( p3 , x3 ) ) ;
CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
var value2 = 105.0f * dot ( m4 , px ) ;
2026-09-26 02:36:52 +02:00
var grad = - 8.0f * m3 . x * x0 * px . x + m4 . x * p0
+ - 8.0f * m3 . y * x1 * px . y + m4 . y * p1
+ - 8.0f * m3 . z * x2 * px . z + m4 . z * p2
+ - 8.0f * m3 . w * x3 * px . w + m4 . w * p3 ;
CultMath: snoise(float3) takes the upstream simplex-boundary fix
Kernel radius^2 0.6 -> 0.5, scale 42 -> 105, permute (34x+1)x -> (34x+10)x, per
stegu/webgl-noise 22434e04d7 (fix 21d9fe23d7, scale ff3b5d34ea, permute a3e6d57095).
snoise_grad, fbm_grad, ridged_grad follow. C#, both HLSL copies, design.md and
THIRD-PARTY-NOTICES.md change together. The 2D snoise shares the permute helper, so
its values change too. Golden values are regenerated in the next commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:13:34 +02:00
grad * = 105.0f ;
2026-09-26 02:36:52 +02:00
return new float4 ( grad , value2 ) ;
}
// Octave sum of snoise_grad (invariant 8). Each octave samples snoise_grad at p scaled by the
// running frequency, so by the chain rule its gradient scales by that same frequency
// (d/dp[n(f*p)] = f * grad_n(f*p)); amplitude and frequency both compound per octave (gain,
// lacunarity respectively), not applied once for the whole sum, so with 1 octave this is exactly
2026-09-26 03:05:08 +02:00
// snoise_grad (NoiseGradTests.FbmGradWithOneOctaveEqualsSnoiseGrad). octaves is clamped to
// [0, 16]: HlslSourceCompatibilityTests' bit-parity oracle drives every mirrored int parameter
// across the full int32 range, which is the right domain for pcg3d/pcg4d's O(1) hash inputs but
// would turn an unclamped octave count into a multi-billion-iteration loop for this mirror.
2026-09-26 02:36:52 +02:00
public static float4 fbm_grad ( float3 p , int octaves , float lacunarity , float gain )
{
octaves = clamp ( octaves , 0 , 16 ) ;
var amplitude = 1.0f ;
var frequency = 1.0f ;
var value = 0.0f ;
var gradient = new float3 ( 0.0f , 0.0f , 0.0f ) ;
for ( var i = 0 ; i < octaves ; i + + )
{
var n = snoise_grad ( p * frequency ) ;
value + = amplitude * n . w ;
gradient + = amplitude * frequency * new float3 ( n . x , n . y , n . z ) ;
frequency * = lacunarity ;
amplitude * = gain ;
}
return new float4 ( gradient , value ) ;
}
cultmath: add fbm, the value-only octave sum of snoise, with its HLSL and GLSL mirrors
math.fbm(p, octaves, lacunarity, gain) and math.fbm(p) (four octaves, lacunarity 2, gain 0.5);
cultmath_fbm in the shaders, the GLSL regenerated by GlslLowering, a golden-fixture family
appended, FbmTests for the rules.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 21:52:18 +00:00
// Value-only fractional Brownian motion: the octave sum of snoise, octave i at frequency lacunarity^i and
// amplitude gain^i. It is fbm_grad(p, octaves, lacunarity, gain).w without the gradient work (to float
// rounding: snoise and snoise_grad.w order their arithmetic differently), so a loop that only wants the height
// (a sphere trace) does not pay for the gradient. octaves clamps to [0, 16] as fbm_grad's does. The one-argument
// form is the stock four octaves, lacunarity 2, gain 1/2: its range is within +-1.875 times snoise's.
public static float fbm ( float3 p , int octaves , float lacunarity , float gain )
{
octaves = clamp ( octaves , 0 , 16 ) ;
var amplitude = 1.0f ;
var frequency = 1.0f ;
var value = 0.0f ;
for ( var i = 0 ; i < octaves ; i + + )
{
value + = amplitude * snoise ( p * frequency ) ;
frequency * = lacunarity ;
amplitude * = gain ;
}
return value ;
}
public static float fbm ( float3 value ) = > fbm ( value , 4 , 2.0f , 0.5f ) ;
2026-09-26 02:36:52 +02:00
// Ridged multifractal noise (Musgrave, Texturing and Modeling, ch. 16; the operator's dune term,
// 2026-09-25): each octave folds snoise_grad about zero, value = Sum a_i*(1 - |n_i|), gradient =
// -Sum a_i*f_i*sign(n_i)*grad(n_i), the same per-octave frequency scaling as fbm_grad's chain
// rule plus the fold's sign flip (d|n|/dp = sign(n)*dn/dp). The crease at n_i = 0 is real and
2026-09-26 03:05:08 +02:00
// deliberate, not a bug: NoiseGradTests excludes a band around it, the same way cellular excludes
2026-09-26 02:36:52 +02:00
// its F1 = F2 seam. sign follows this codebase's HLSL-matching convention (design.md: sign
// returns int, 0 at exactly 0), so the crease itself contributes no gradient rather than an
// arbitrary one. Same octave clamp as fbm_grad, for the same bit-parity-oracle reason.
public static float4 ridged_grad ( float3 p , int octaves , float lacunarity , float gain )
{
octaves = clamp ( octaves , 0 , 16 ) ;
var amplitude = 1.0f ;
var frequency = 1.0f ;
var value = 0.0f ;
var gradient = new float3 ( 0.0f , 0.0f , 0.0f ) ;
for ( var i = 0 ; i < octaves ; i + + )
{
var n = snoise_grad ( p * frequency ) ;
var s = ( float ) sign ( n . w ) ;
value + = amplitude * ( 1.0f - abs ( n . w ) ) ;
gradient + = - amplitude * frequency * s * new float3 ( n . x , n . y , n . z ) ;
frequency * = lacunarity ;
amplitude * = gain ;
}
return new float4 ( gradient , value ) ;
}
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
// Inigo Quilez, "smooth minimum" (https://iquilezles.org/articles/smin/): the quadratic-polynomial
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
// smin, carried to value-and-gradient form (invariant 8). h is affine in (b.w - a.w) inside the
// smoothing band, so differentiating value = lerp(b.w, a.w, h) - k*h*(1-h) with respect to
// position, the terms carrying dh/dp cancel exactly (dh/dp * [(a.w-b.w) - k + 2*k*h] and the
// bracket is identically zero given how h is built from a.w-b.w). What is left is exactly
// lerp(∇b, ∇a, h): the gradient blend is not an approximation, it is the analytic gradient.
// Outside the band (|a.w - b.w| >= k), h saturates to 0 or 1 and this is exactly min(a, b) with
// that input's own gradient, continuously (h and the correction term both reach the boundary at
// the same value from the smooth side, so there is no kink to exclude here the way cellular's
// F1 = F2 seam needs one).
public static float4 smin_grad ( float4 a , float4 b , float k )
{
var h = saturate ( 0.5f + 0.5f * ( b . w - a . w ) / k ) ;
var value = lerp ( b . w , a . w , h ) - k * h * ( 1.0f - h ) ;
var gradient = lerp ( new float3 ( b . x , b . y , b . z ) , new float3 ( a . x , a . y , a . z ) , h ) ;
return new float4 ( gradient , value ) ;
}
// Worley, "A Cellular Texture Basis Function" (SIGGRAPH 1996): F1/F2 and their analytic
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
// gradients, searched over the jittered neighbourhood of feature points. Feature points are
cellular: hash the integer cell coordinate, correct the F6 near-side proof
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:
- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
instead of the integer coordinate itself. Two adjacent cells' bit patterns
share more structure (same exponent, mantissa one increment apart) than
the integers do going into pcg3d's own mixing, and that structure survived
into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
cells against 15 degrees of freedom (critical value 37.7). Both call sites
now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
as before). This changes cellular's noise output — nothing has shipped, so
that is acceptable, but it is a real behavioural change, not a comment fix.
- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
picking the near-side neighbour on a FIXED axis is not always within
sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
exclusion). The comment is rewritten to the correct argument: choose the
axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
1/2 on that axis), and the near-side neighbour on THAT axis has squared
distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
strictly below 3 since the jitter never reaches the boundary. Margin
2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
sqrt(3), only in the unattainable corner limit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:09 +02:00
// selected with pcg3d over each neighbour's integer cell coordinate (design.md, "Integer
// hashing"), never the float32 bit pattern of that coordinate and never the sin-based hash.
2026-09-26 02:23:55 +02:00
// Hashing the integer cell coordinate directly is exact and runtime-independent, and it is what
// the HLSL mirror does, so C# and shader stay bit-for-bit aligned. What pins this is the oracle
// and mirror tests, not a uniformity measurement: reverting to hashing the float32 bit pattern
// (or the sin-based hash) is caught by ProductionSearchMatchesTheBruteForceOracleExactly,
// EveryMirrorFunctionMatchesCSharpMath, F1NeverExceedsF2, and the other tests that key off the
// integer-hash fixtures. The
cellular: hash the integer cell coordinate, correct the F6 near-side proof
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:
- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
instead of the integer coordinate itself. Two adjacent cells' bit patterns
share more structure (same exponent, mantissa one increment apart) than
the integers do going into pcg3d's own mixing, and that structure survived
into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
cells against 15 degrees of freedom (critical value 37.7). Both call sites
now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
as before). This changes cellular's noise output — nothing has shipped, so
that is acceptable, but it is a real behavioural change, not a comment fix.
- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
picking the near-side neighbour on a FIXED axis is not always within
sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
exclusion). The comment is rewritten to the correct argument: choose the
axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
1/2 on that axis), and the near-side neighbour on THAT axis has squared
distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
strictly below 3 since the jitter never reaches the boundary. Margin
2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
sqrt(3), only in the unattainable corner limit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:09 +02:00
// gradient of a distance field, del|p - c|, is the unit vector (p - c)/|p - c|; that is undefined exactly at
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
// a feature point (F1 = 0 or F2 = 0), so cellular follows the repo's existing degenerate-normal
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
// convention (GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle: guard the zero-length
// case and return the zero vector instead of the NaN a bare normalize would produce there).
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
//
// The search radius is 2 cells on every axis (5x5x5), not the classic 1-cell (3x3x3) radius,
// because 3x3x3 is not exact: measured 9 of 400,000 points in [-50,50]^3 land on the wrong F2
// (worst error 0.088), and past |p| of about 2^23 the float32 jitter can put two distinct cells'
// features on the same value, which 3x3x3 has no chance of noticing either way. 5x5x5 is exact,
// not merely safer, by the following argument:
//
// Let u = p - cell be the query's position inside its own cell (u in [0,1)^3 per component), and
// let a candidate feature at integer cell offset d = (dx, dy, dz) sit at d + j for some jitter j
// in [0,1)^3. Per axis, the displacement from p to that feature is (u_i - j_i) - d_i; since
// u_i - j_i is always strictly inside (-1, 1) regardless of the actual u_i and j_i values,
// |displacement_i| is always strictly greater than max(0, |d_i| - 1). So every cell with |d_i| >=
// 3 on some axis is strictly farther than 2 from p (that axis alone already gives max(0, 3-1) =
// 2). Now bound F1 and F2 from above using two real, always-present candidates: the query's own
// cell (d = 0), whose distance is always strictly less than sqrt(3) (every axis strictly under
cellular: hash the integer cell coordinate, correct the F6 near-side proof
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:
- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
instead of the integer coordinate itself. Two adjacent cells' bit patterns
share more structure (same exponent, mantissa one increment apart) than
the integers do going into pcg3d's own mixing, and that structure survived
into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
cells against 15 degrees of freedom (critical value 37.7). Both call sites
now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
as before). This changes cellular's noise output — nothing has shipped, so
that is acceptable, but it is a real behavioural change, not a comment fix.
- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
picking the near-side neighbour on a FIXED axis is not always within
sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
exclusion). The comment is rewritten to the correct argument: choose the
axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
1/2 on that axis), and the near-side neighbour on THAT axis has squared
distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
strictly below 3 since the jitter never reaches the boundary. Margin
2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
sqrt(3), only in the unattainable corner limit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:09 +02:00
// 1); and the near-side neighbour on whichever single axis's u_i sits closest to a cell
// boundary. Let m = min(u_i, 1 - u_i) on that chosen axis (m is in [0, 1/2] by construction,
// since it is the smaller of the two). Stepping one cell towards that nearer boundary puts the
// query within (1 + m) of the far face on the offset axis and within (1 - m) of the near face on
// each of the other two axes, so that neighbour's squared distance is at most
// (1 + m)^2 + 2*(1 - m)^2 = 3 - 2m + 3m^2, which is <= 3 for every m in [0, 1/2] (3 at m = 0,
2026-09-26 02:23:55 +02:00
// dipping to a minimum of 8/3 ~= 2.667 at m = 1/3, then rising back to 2.75 at m = 1/2), and
// strictly below 3 because the jitter never reaches exactly 0 or
cellular: hash the integer cell coordinate, correct the F6 near-side proof
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:
- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
instead of the integer coordinate itself. Two adjacent cells' bit patterns
share more structure (same exponent, mantissa one increment apart) than
the integers do going into pcg3d's own mixing, and that structure survived
into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
cells against 15 degrees of freedom (critical value 37.7). Both call sites
now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
as before). This changes cellular's noise output — nothing has shipped, so
that is acceptable, but it is a real behavioural change, not a comment fix.
- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
picking the near-side neighbour on a FIXED axis is not always within
sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
exclusion). The comment is rewritten to the correct argument: choose the
axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
1/2 on that axis), and the near-side neighbour on THAT axis has squared
distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
strictly below 3 since the jitter never reaches the boundary. Margin
2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
sqrt(3), only in the unattainable corner limit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:09 +02:00
// 1 (u_i - j_i stays strictly inside (-1, 1), so this bound, like the own-cell one, is never
// tight). F2 is at most the larger of these two real candidates (adding candidates to a set can
// only lower or hold its 2nd-smallest value), so F1 < sqrt(3) and F2 < sqrt(3) always: roughly
// 1.732 < 2, margin 2 - sqrt(3) ~= 0.268 (Soul's adversarial check, cut 2a-i: the worst
// realizable in-box F2 is exactly sqrt(3), reached only in the unattainable limit at a cell
// corner). Every cell outside the 5x5x5 block is farther than 2, hence farther than either F1 or
// F2, so the true global F1 and F2 are always inside it. (The matching argument for 3x3x3 only
// reaches radius 1, whose excluded cells start at distance > 1, which is less than the 1.732
// ceiling — the gap 3x3x3's failures live in.)
//
// An earlier version of this proof picked the near-side neighbour on a fixed axis rather than
// whichever axis sits closest to its boundary, and that version is wrong (Soul, cut 2a-i):
// u = (0.5, 0, 0) with the x-neighbour's jittered feature at (-1, 1 - 2^-24, 1 - 2^-24) gives a
// distance of about 2.06, outside the radius-2 exclusion the proof needs. Choosing the axis by
// proximity to its own boundary is what keeps m <= 1/2 and the bound at sqrt(3); x was the wrong
// axis to fix there because u.x = 0.5 is the point on the whole cube farthest from any boundary.
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
//
// The loop below still visits every one of the 125 cells in a fixed order, but skips the
// pcg3d/jitter/distance work for a cell whose distance cannot possibly beat the current f2: that
// per-cell lower bound (the max(0, |d_i| - 1) term above, combined across axes) depends only on
// the integer offset, never on p or the hash, so pruning a cell the unpruned loop would also
// have rejected changes no output bit (CellularExactSearchTests pins the pruned/unpruned
// equivalence and the exactness against a 7x7x7 brute-force oracle).
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
public static CultCellular cellular ( float3 p )
{
var cell = floor ( p ) ;
var f1 = 1.0e30f ;
var f2 = 1.0e30f ;
2026-09-25 19:21:37 +02:00
var c1 = new float3 ( 0.0f , 0.0f , 0.0f ) ;
var c2 = new float3 ( 0.0f , 0.0f , 0.0f ) ;
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
var cellId = new float3 ( 0.0f , 0.0f , 0.0f ) ;
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
for ( var dz = - 2 ; dz < = 2 ; dz + + )
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
{
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
var lz = max ( 0.0f , abs ( ( float ) dz ) - 1.0f ) ;
for ( var dy = - 2 ; dy < = 2 ; dy + + )
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
{
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
var ly = max ( 0.0f , abs ( ( float ) dy ) - 1.0f ) ;
for ( var dx = - 2 ; dx < = 2 ; dx + + )
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
{
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
var lx = max ( 0.0f , abs ( ( float ) dx ) - 1.0f ) ;
var lowerBound = sqrt ( lx * lx + ly * ly + lz * lz ) ;
if ( lowerBound > = f2 )
continue ;
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
var neighbor = cell + new float3 ( dx , dy , dz ) ;
cellular: hash the integer cell coordinate, correct the F6 near-side proof
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:
- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
instead of the integer coordinate itself. Two adjacent cells' bit patterns
share more structure (same exponent, mantissa one increment apart) than
the integers do going into pcg3d's own mixing, and that structure survived
into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
cells against 15 degrees of freedom (critical value 37.7). Both call sites
now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
as before). This changes cellular's noise output — nothing has shipped, so
that is acceptable, but it is a real behavioural change, not a comment fix.
- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
picking the near-side neighbour on a FIXED axis is not always within
sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
exclusion). The comment is rewritten to the correct argument: choose the
axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
1/2 on that axis), and the near-side neighbour on THAT axis has squared
distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
strictly below 3 since the jitter never reaches the boundary. Margin
2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
sqrt(3), only in the unattainable corner limit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:09 +02:00
var hash = pcg3d ( int3 ( neighbor ) ) ;
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
var jitter = new float3 ( cellular_unit ( hash . x ) , cellular_unit ( hash . y ) , cellular_unit ( hash . z ) ) ;
var feature = neighbor + jitter ;
var d = distance ( p , feature ) ;
if ( d < f1 )
{
f2 = f1 ; c2 = c1 ;
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
f1 = d ; c1 = feature ; cellId = neighbor ;
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
}
else if ( d < f2 )
{
f2 = d ; c2 = feature ;
}
}
}
}
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
// F1 = 0 is reachable (p sits exactly on a feature point). F2 = 0 needs two distinct cells'
// pcg3d-jittered features to land on the same float32 value in every component: unreachable
// at ordinary magnitudes, but confirmed reachable past roughly |p| = 2^23 (design.md,
// "Precision domain"), so both share the same degenerate-gradient guard.
2026-09-25 19:21:37 +02:00
var grad1 = f1 > 0.0f ? ( p - c1 ) / f1 : new float3 ( 0.0f , 0.0f , 0.0f ) ;
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
var grad2 = f2 > 0.0f ? ( p - c2 ) / f2 : new float3 ( 0.0f , 0.0f , 0.0f ) ;
cellular: hash the integer cell coordinate, correct the F6 near-side proof
Soul (cut 2a-i, pass 2) found two defects in cellular's own math.cs source
and its HLSL mirror:
- cellular hashed pcg3d/pcg4d over the FLOAT32 BIT PATTERN of each
integer-valued cell coordinate (pcg3d(neighbor), pcg4d(float4(cellId, 0)))
instead of the integer coordinate itself. Two adjacent cells' bit patterns
share more structure (same exponent, mantissa one increment apart) than
the integers do going into pcg3d's own mixing, and that structure survived
into the jitter: measured chi-square 203.8 for jitter.x over 1,000,000
cells against 15 degrees of freedom (critical value 37.7). Both call sites
now hash int3(neighbor)/int4(int3(cellId), 0) directly, in C# and in the
HLSL mirror (both shaders/CultMath.hlsl and the Unity copy, byte-identical
as before). This changes cellular's noise output — nothing has shipped, so
that is acceptable, but it is a real behavioural change, not a comment fix.
- The F6 exactness proof's "near-side neighbour" bound was wrong as written:
picking the near-side neighbour on a FIXED axis is not always within
sqrt(3) of the query (counterexample: u = (0.5, 0, 0) with the x-neighbour
jittered to (-1, 1-2^-24, 1-2^-24) gives ~2.06, past the radius-2
exclusion). The comment is rewritten to the correct argument: choose the
axis whose u_i sits closest to a cell boundary (m = min(u_i, 1-u_i) <=
1/2 on that axis), and the near-side neighbour on THAT axis has squared
distance at most (1+m)^2 + 2*(1-m)^2 = 3 - 2m + 3m^2 <= 3 on [0, 1/2],
strictly below 3 since the jitter never reaches the boundary. Margin
2 - sqrt(3) ~= 0.268 (Soul's adversarial check: worst in-box F2 is exactly
sqrt(3), only in the unattainable corner limit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:13:09 +02:00
// 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: sharing a
// hash between id and jitter would make id a deterministic function of the jitter it is
// supposed to be independent from, correlated across every cell (F4).
var idHash = pcg4d ( new int4 ( int3 ( cellId ) , 0 ) ) ;
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
return new CultCellular (
new float4 ( grad1 , f1 ) ,
new float4 ( grad2 - grad1 , f2 - f1 ) ,
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
cellular_unit ( idHash . w ) ) ;
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
}
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
// Maps a hash component's uint bit pattern to [0, 1) using the top 24 bits, so the result is
// always strictly less than 1: a plain divide-by-2^32 rounds 0xFFFFFF80..0xFFFFFFFF up to
// exactly 1.0f in float32, outside the documented range. (uint >> 8) has at most 24 significant
// bits, which float32's 24-bit mantissa represents exactly, and the division is by an exact power
// of two, so both the shift-and-convert and the divide are the same IEEE-754 round-to-nearest on
// dxc and C#, bit-exact on both sides.
internal static float cellular_unit ( int bits ) = > ( ( uint ) bits > > 8 ) * ( 1.0f / 16777216.0f ) ;
Add smin_grad and cellular value-and-gradient primitives to CultMath
smin_grad(float4,float4,float) carries Inigo Quilez's cubic-polynomial smooth
minimum to invariant 8's value-and-gradient form: the gradient blend lerp(b,a,h)
is the exact analytic derivative, not an approximation, since the h-derivative
terms cancel algebraically inside the smoothing band.
cellular(float3) is Worley's cellular texture basis function (SIGGRAPH 1996),
searched over the jittered 3x3x3 neighbourhood of pcg3d-selected feature
points. It returns the new CultCellular struct (nearest, edge, id), following
the repo's existing degenerate-normal convention
(GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle) to return a
finite zero gradient instead of NaN at a feature point.
Both are fresh implementations from the cited literature, not ports, so they
get a design.md/doc-comment citation instead of a THIRD-PARTY-NOTICES entry.
design.md gains an "Invariant 8: Value-and-Gradient Primitives" section
documenting both primitives' semantics and conventions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 19:17:39 +02:00
2026-06-10 21:22:43 +01:00
public static float value_noise ( float2 position )
{
var cell = floor ( position ) ;
var local = frac ( position ) ;
var u = local * local * ( 3.0f - 2.0f * local ) ;
var a = hash ( cell ) ;
var b = hash ( cell + new float2 ( 1.0f , 0.0f ) ) ;
var c = hash ( cell + new float2 ( 0.0f , 1.0f ) ) ;
var d = hash ( cell + new float2 ( 1.0f , 1.0f ) ) ;
return lerp ( lerp ( a , b , u . x ) , lerp ( c , d , u . x ) , u . y ) ;
}
public static float value_noise_bicubic ( float2 position )
{
var cell = floor ( position ) ;
var local = frac ( position ) ;
var y0 = catmullrom (
hash ( cell + new float2 ( - 1.0f , - 1.0f ) ) ,
hash ( cell + new float2 ( 0.0f , - 1.0f ) ) ,
hash ( cell + new float2 ( 1.0f , - 1.0f ) ) ,
hash ( cell + new float2 ( 2.0f , - 1.0f ) ) ,
local . x ) ;
var y1 = catmullrom (
hash ( cell + new float2 ( - 1.0f , 0.0f ) ) ,
hash ( cell + new float2 ( 0.0f , 0.0f ) ) ,
hash ( cell + new float2 ( 1.0f , 0.0f ) ) ,
hash ( cell + new float2 ( 2.0f , 0.0f ) ) ,
local . x ) ;
var y2 = catmullrom (
hash ( cell + new float2 ( - 1.0f , 1.0f ) ) ,
hash ( cell + new float2 ( 0.0f , 1.0f ) ) ,
hash ( cell + new float2 ( 1.0f , 1.0f ) ) ,
hash ( cell + new float2 ( 2.0f , 1.0f ) ) ,
local . x ) ;
var y3 = catmullrom (
hash ( cell + new float2 ( - 1.0f , 2.0f ) ) ,
hash ( cell + new float2 ( 0.0f , 2.0f ) ) ,
hash ( cell + new float2 ( 1.0f , 2.0f ) ) ,
hash ( cell + new float2 ( 2.0f , 2.0f ) ) ,
local . x ) ;
return catmullrom ( y0 , y1 , y2 , y3 , local . y ) ;
}
2026-05-08 01:49:44 +01:00
}