diff --git a/CultMath.slnx.meta b/CultMath.slnx.meta
new file mode 100644
index 0000000..a33f797
--- /dev/null
+++ b/CultMath.slnx.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d310c959fd7a1d24a981ea6766ee3c59
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..4df13bd
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+ $(MSBuildThisFileDirectory).tools\dotnet\bin\$(MSBuildProjectName)\
+ $(MSBuildThisFileDirectory).tools\dotnet\obj\$(MSBuildProjectName)\
+
+
diff --git a/Directory.Build.props.meta b/Directory.Build.props.meta
new file mode 100644
index 0000000..f41d119
--- /dev/null
+++ b/Directory.Build.props.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: e041df73afea0094cae9b8d5dcc5e4eb
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/README.md.meta b/README.md.meta
new file mode 100644
index 0000000..4411da7
--- /dev/null
+++ b/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 867daebafe0ce48479cf043461ee6bdf
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/docs.meta b/docs.meta
new file mode 100644
index 0000000..ee5fab2
--- /dev/null
+++ b/docs.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: de535b89253410b41b5989f712b80af4
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/docs/design.md.meta b/docs/design.md.meta
new file mode 100644
index 0000000..0d31ca6
--- /dev/null
+++ b/docs/design.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 07021a8be816e3448a9490c10845b0aa
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/global.json.meta b/global.json.meta
new file mode 100644
index 0000000..6c2c0ee
--- /dev/null
+++ b/global.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 28e4ce693c46cbd4b990c5790745d62f
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/native.meta b/native.meta
new file mode 100644
index 0000000..f413533
--- /dev/null
+++ b/native.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9d1cb2ac88c530b4d9da456523115e12
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/native/cultmath-core.meta b/native/cultmath-core.meta
new file mode 100644
index 0000000..48a2ef7
--- /dev/null
+++ b/native/cultmath-core.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a42d65478346c354c8b4f353c5581b49
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/native/cultmath-core/Cargo.lock.meta b/native/cultmath-core/Cargo.lock.meta
new file mode 100644
index 0000000..9ca3ec6
--- /dev/null
+++ b/native/cultmath-core/Cargo.lock.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f1c365f7eead2684ba621124e653bdab
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/native/cultmath-core/Cargo.toml.meta b/native/cultmath-core/Cargo.toml.meta
new file mode 100644
index 0000000..8875de0
--- /dev/null
+++ b/native/cultmath-core/Cargo.toml.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: cdd2d4eab053b764b9a06d36b080e43e
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/native/cultmath-core/src.meta b/native/cultmath-core/src.meta
new file mode 100644
index 0000000..8e5232d
--- /dev/null
+++ b/native/cultmath-core/src.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7de448b7e258d2c448bb7391051cc628
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/native/cultmath-core/src/lib.rs.meta b/native/cultmath-core/src/lib.rs.meta
new file mode 100644
index 0000000..2fa8aee
--- /dev/null
+++ b/native/cultmath-core/src/lib.rs.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b06c69c432a633f4584830d6b28d4473
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6da2a17
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "org.gamecult.cultmath",
+ "displayName": "CultMath",
+ "version": "0.1.0",
+ "unity": "2021.3",
+ "description": "Clean-room C# math primitives shaped for HLSL-style renderer and simulation code.",
+ "keywords": [
+ "GameCult",
+ "Math",
+ "Simulation"
+ ],
+ "license": "MPL-2.0",
+ "category": "Scripting"
+}
diff --git a/package.json.meta b/package.json.meta
new file mode 100644
index 0000000..23ed71c
--- /dev/null
+++ b/package.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 1abf066549a826c4ba7a30d6676735c0
+PackageManifestImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/shaders.meta b/shaders.meta
new file mode 100644
index 0000000..46f9d44
--- /dev/null
+++ b/shaders.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3bed74ebcba445543800218dbe185034
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/shaders/CultMath.hlsl b/shaders/CultMath.hlsl
index 3cbb3fd..07b2b8a 100644
--- a/shaders/CultMath.hlsl
+++ b/shaders/CultMath.hlsl
@@ -95,6 +95,18 @@ float3 cultmath_reflect(float3 incident, float3 normal_value)
return incident - 2.0 * dot(normal_value, incident) * normal_value;
}
+float2 cultmath_rotate(float2 value, float radians_value)
+{
+ float s = sin(radians_value);
+ float c = cos(radians_value);
+ return float2(value.x * c - value.y * s, value.x * s + value.y * c);
+}
+
+float2 cultmath_rotate_degrees(float2 value, float degrees_value)
+{
+ return cultmath_rotate(value, cultmath_radians(degrees_value));
+}
+
float cultmath_csum(float2 value) { return value.x + value.y; }
float cultmath_csum(float3 value) { return value.x + value.y + value.z; }
float cultmath_csum(float4 value) { return value.x + value.y + value.z + value.w; }
@@ -251,4 +263,76 @@ float cultmath_value_noise_texture_bicubic(Texture2D noise_texture, Sampl
return cultmath_catmullrom(row0, row1, row2, row3, local.y);
}
+struct CultMathSphericalErosionParameters
+{
+ float frequency; float amplitude; float lacunarity; float gain;
+ float slope_strength; float detail; float seed; int octaves;
+};
+
+struct CultMathSphericalErosionSample
+{
+ float height_offset; float ridge; float gully; float3 gradient;
+};
+
+float cultmath_spherical_cell_wave(float3 position, float3 flow, float3 cell, float seed)
+{
+ float3 pivot = cell + float3(cultmath_hash(cell + seed + 11.7), cultmath_hash(cell.yzx + seed + 37.1), cultmath_hash(cell.zyx + seed + 73.9));
+ return dot(position - pivot, flow) * 6.28318530718 + cultmath_hash(cell + seed + 101.3) * 6.28318530718;
+}
+
+float cultmath_spherical_directional_wave(float3 position, float3 flow, float seed)
+{
+ float3 cell = floor(position);
+ float3 local = cultmath_frac(position);
+ float3 weights = local * local * (3.0 - 2.0 * local);
+ float z0 = cultmath_lerp(
+ cultmath_lerp(cultmath_spherical_cell_wave(position, flow, cell + float3(0,0,0), seed), cultmath_spherical_cell_wave(position, flow, cell + float3(1,0,0), seed), weights.x),
+ cultmath_lerp(cultmath_spherical_cell_wave(position, flow, cell + float3(0,1,0), seed), cultmath_spherical_cell_wave(position, flow, cell + float3(1,1,0), seed), weights.x), weights.y);
+ float z1 = cultmath_lerp(
+ cultmath_lerp(cultmath_spherical_cell_wave(position, flow, cell + float3(0,0,1), seed), cultmath_spherical_cell_wave(position, flow, cell + float3(1,0,1), seed), weights.x),
+ cultmath_lerp(cultmath_spherical_cell_wave(position, flow, cell + float3(0,1,1), seed), cultmath_spherical_cell_wave(position, flow, cell + float3(1,1,1), seed), weights.x), weights.y);
+ return cultmath_lerp(z0, z1, weights.z);
+}
+
+float3 cultmath_spherical_stable_tangent(float3 position)
+{
+ float3 axis = abs(position.z) < 0.8 ? float3(0,0,1) : float3(0,1,0);
+ return cultmath_normalize(cross(axis, position));
+}
+
+CultMathSphericalErosionSample cultmath_spherical_erosion(float3 unit_position, float base_height, float3 base_gradient, CultMathSphericalErosionParameters p)
+{
+ float3 position = cultmath_normalize(unit_position);
+ float3 gradient = base_gradient - position * dot(base_gradient, position);
+ float slope = length(gradient);
+ float3 flow = slope > 1.0e-6 ? -gradient / slope : cultmath_spherical_stable_tangent(position);
+ float frequency = max(p.frequency, 0.001);
+ float amplitude = p.amplitude;
+ float combi_mask = 1.0 - pow(1.0 - cultmath_saturate(slope * p.slope_strength), 2.0);
+ float fade_target = cultmath_saturate(base_height * 0.5 + 0.5) * 2.0 - 1.0;
+ CultMathSphericalErosionSample result = (CultMathSphericalErosionSample)0;
+ [loop] for (int octave = 0; octave < min(max(p.octaves, 0), 8); octave++)
+ {
+ float phase = cultmath_spherical_directional_wave(position * frequency, flow, p.seed + octave * 19.19);
+ float wave = cos(phase);
+ float wave_slope = sin(phase);
+ float faded = cultmath_lerp(fade_target, wave, combi_mask);
+ result.height_offset += faded * amplitude;
+ result.ridge = max(result.ridge, cultmath_saturate(faded));
+ result.gully = max(result.gully, cultmath_saturate(-faded));
+ float3 across = cultmath_normalize(cross(position, flow));
+ gradient += across * (sign(wave_slope) * amplitude * frequency * combi_mask);
+ gradient -= position * dot(gradient, position);
+ slope = length(gradient);
+ flow = slope > 1.0e-6 ? -gradient / slope : cultmath_spherical_stable_tangent(position);
+ float new_mask = 1.0 - pow(1.0 - cultmath_saturate(abs(wave_slope) * 1.35), 2.0);
+ combi_mask = (1.0 - pow(1.0 - cultmath_saturate(combi_mask), max(p.detail, 0.001))) * new_mask;
+ fade_target = faded;
+ frequency *= max(p.lacunarity, 1.01);
+ amplitude *= cultmath_saturate(p.gain);
+ }
+ result.gradient = gradient;
+ return result;
+}
+
#endif
diff --git a/shaders/CultMath.hlsl.meta b/shaders/CultMath.hlsl.meta
new file mode 100644
index 0000000..d6b8542
--- /dev/null
+++ b/shaders/CultMath.hlsl.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 33174674795143c4a905408718f956f8
+ShaderIncludeImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src.meta b/src.meta
new file mode 100644
index 0000000..3e88fcb
--- /dev/null
+++ b/src.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 64f48d0cae0810e4ba9903e9094fe4c6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/CultMath.meta b/src/CultMath.meta
new file mode 100644
index 0000000..381b726
--- /dev/null
+++ b/src/CultMath.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e857b457b93397d4fb4ad8481cb72e4c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/CultMath/BatchMath.cs b/src/CultMath/BatchMath.cs
index df62e07..04c042d 100644
--- a/src/CultMath/BatchMath.cs
+++ b/src/CultMath/BatchMath.cs
@@ -1,3 +1,4 @@
+using System;
using System.Numerics;
namespace CultMath;
@@ -39,10 +40,19 @@ public static class BatchMath
var i = 0;
var lanes = Vector.Count;
+ var pxBuffer = new float[lanes];
+ var pyBuffer = new float[lanes];
+ var axBuffer = new float[lanes];
+ var ayBuffer = new float[lanes];
for (; i <= positionX.Length - lanes; i += lanes)
{
- var px = new Vector(positionX.Slice(i, lanes));
- var py = new Vector(positionY.Slice(i, lanes));
+ CopySpanToArray(positionX.Slice(i, lanes), pxBuffer);
+ CopySpanToArray(positionY.Slice(i, lanes), pyBuffer);
+ CopySpanToArray(accelerationX.Slice(i, lanes), axBuffer);
+ CopySpanToArray(accelerationY.Slice(i, lanes), ayBuffer);
+
+ var px = new Vector(pxBuffer);
+ var py = new Vector(pyBuffer);
var dx = centerXVector - px;
var dy = centerYVector - py;
var distanceSquared = Vector.Max(dx * dx + dy * dy, minimumDistanceSquared);
@@ -53,8 +63,10 @@ public static class BatchMath
var ax = Vector.ConditionalSelect(inRange, dx / distance * magnitude, zero);
var ay = Vector.ConditionalSelect(inRange, dy / distance * magnitude, zero);
- (new Vector(accelerationX.Slice(i, lanes)) + ax).CopyTo(accelerationX.Slice(i, lanes));
- (new Vector(accelerationY.Slice(i, lanes)) + ay).CopyTo(accelerationY.Slice(i, lanes));
+ (new Vector(axBuffer) + ax).CopyTo(axBuffer);
+ (new Vector(ayBuffer) + ay).CopyTo(ayBuffer);
+ CopyArrayToSpan(axBuffer, accelerationX.Slice(i, lanes));
+ CopyArrayToSpan(ayBuffer, accelerationY.Slice(i, lanes));
}
for (; i < positionX.Length; i++)
@@ -75,6 +87,35 @@ public static class BatchMath
}
}
+ public static void AddRadialFalloffAcceleration2D(
+ ReadOnlySpan positions,
+ float2 center,
+ float strength,
+ float radius,
+ Span acceleration)
+ {
+ ValidateEqualLengths(positions.Length, acceleration.Length);
+
+ if (radius <= 0.0f || strength == 0.0f || positions.Length == 0)
+ {
+ return;
+ }
+
+ for (var i = 0; i < positions.Length; i++)
+ {
+ var delta = center - positions[i];
+ var distanceSquared = MathF.Max(delta.x * delta.x + delta.y * delta.y, MinimumDistanceSquared);
+ var distance = MathF.Sqrt(distanceSquared);
+ if (distance > radius)
+ {
+ continue;
+ }
+
+ var falloff = MathF.Max(0.0f, 1.0f - distance / radius);
+ acceleration[i] += delta / distance * (strength * falloff);
+ }
+ }
+
public static void IntegrateSemiImplicitEuler2D(
float deltaTime,
ReadOnlySpan dynamicMask,
@@ -102,25 +143,44 @@ public static class BatchMath
var delta = new Vector(deltaTime);
var i = 0;
var lanes = Vector.Count;
+ var maskBuffer = new float[lanes];
+ var pxBuffer = new float[lanes];
+ var pyBuffer = new float[lanes];
+ var vxBuffer = new float[lanes];
+ var vyBuffer = new float[lanes];
+ var axBuffer = new float[lanes];
+ var ayBuffer = new float[lanes];
for (; i <= positionX.Length - lanes; i += lanes)
{
- var mask = new Vector(dynamicMask.Slice(i, lanes));
- var vx = new Vector(velocityX.Slice(i, lanes));
- var vy = new Vector(velocityY.Slice(i, lanes));
- var px = new Vector(positionX.Slice(i, lanes));
- var py = new Vector(positionY.Slice(i, lanes));
- var ax = new Vector(accelerationX.Slice(i, lanes));
- var ay = new Vector(accelerationY.Slice(i, lanes));
+ CopySpanToArray(dynamicMask.Slice(i, lanes), maskBuffer);
+ CopySpanToArray(velocityX.Slice(i, lanes), vxBuffer);
+ CopySpanToArray(velocityY.Slice(i, lanes), vyBuffer);
+ CopySpanToArray(positionX.Slice(i, lanes), pxBuffer);
+ CopySpanToArray(positionY.Slice(i, lanes), pyBuffer);
+ CopySpanToArray(accelerationX.Slice(i, lanes), axBuffer);
+ CopySpanToArray(accelerationY.Slice(i, lanes), ayBuffer);
+
+ var mask = new Vector(maskBuffer);
+ var vx = new Vector(vxBuffer);
+ var vy = new Vector(vyBuffer);
+ var px = new Vector(pxBuffer);
+ var py = new Vector(pyBuffer);
+ var ax = new Vector(axBuffer);
+ var ay = new Vector(ayBuffer);
vx += ax * delta * mask;
vy += ay * delta * mask;
px += vx * delta * mask;
py += vy * delta * mask;
- vx.CopyTo(velocityX.Slice(i, lanes));
- vy.CopyTo(velocityY.Slice(i, lanes));
- px.CopyTo(positionX.Slice(i, lanes));
- py.CopyTo(positionY.Slice(i, lanes));
+ vx.CopyTo(vxBuffer);
+ vy.CopyTo(vyBuffer);
+ px.CopyTo(pxBuffer);
+ py.CopyTo(pyBuffer);
+ CopyArrayToSpan(vxBuffer, velocityX.Slice(i, lanes));
+ CopyArrayToSpan(vyBuffer, velocityY.Slice(i, lanes));
+ CopyArrayToSpan(pxBuffer, positionX.Slice(i, lanes));
+ CopyArrayToSpan(pyBuffer, positionY.Slice(i, lanes));
}
for (; i < positionX.Length; i++)
@@ -133,6 +193,28 @@ public static class BatchMath
}
}
+ public static void IntegrateSemiImplicitEuler2D(
+ float deltaTime,
+ ReadOnlySpan dynamicMask,
+ Span position,
+ Span velocity,
+ ReadOnlySpan acceleration)
+ {
+ if (deltaTime <= 0.0f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(deltaTime), "Delta time must be positive.");
+ }
+
+ ValidateEqualLengths(dynamicMask.Length, position.Length, velocity.Length, acceleration.Length);
+
+ for (var i = 0; i < position.Length; i++)
+ {
+ var mask = dynamicMask[i];
+ velocity[i] += acceleration[i] * deltaTime * mask;
+ position[i] += velocity[i] * deltaTime * mask;
+ }
+ }
+
private static void ValidateEqualLengths(params int[] lengths)
{
var length = lengths[0];
@@ -144,4 +226,20 @@ public static class BatchMath
}
}
}
+
+ private static void CopySpanToArray(ReadOnlySpan source, float[] destination)
+ {
+ for (var i = 0; i < source.Length; i++)
+ {
+ destination[i] = source[i];
+ }
+ }
+
+ private static void CopyArrayToSpan(float[] source, Span destination)
+ {
+ for (var i = 0; i < destination.Length; i++)
+ {
+ destination[i] = source[i];
+ }
+ }
}
diff --git a/src/CultMath/BatchMath.cs.meta b/src/CultMath/BatchMath.cs.meta
new file mode 100644
index 0000000..fe7c603
--- /dev/null
+++ b/src/CultMath/BatchMath.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d4771320e190bdd408dd296c293a9d0e
\ No newline at end of file
diff --git a/src/CultMath/Color32.cs.meta b/src/CultMath/Color32.cs.meta
new file mode 100644
index 0000000..71e1ba5
--- /dev/null
+++ b/src/CultMath/Color32.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bfa14a9aacb8ac4448761568643f2bc6
\ No newline at end of file
diff --git a/src/CultMath/CultMath.asmdef b/src/CultMath/CultMath.asmdef
new file mode 100644
index 0000000..449549f
--- /dev/null
+++ b/src/CultMath/CultMath.asmdef
@@ -0,0 +1,14 @@
+{
+ "name": "CultMath",
+ "rootNamespace": "CultMath",
+ "references": [],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": true,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": true
+}
diff --git a/src/CultMath/CultMath.asmdef.meta b/src/CultMath/CultMath.asmdef.meta
new file mode 100644
index 0000000..5a26d2c
--- /dev/null
+++ b/src/CultMath/CultMath.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: f0b66b909d13e18468f7aa56221531e9
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/CultMath/CultMath.csproj b/src/CultMath/CultMath.csproj
index f43adce..01901b6 100644
--- a/src/CultMath/CultMath.csproj
+++ b/src/CultMath/CultMath.csproj
@@ -1,9 +1,10 @@
- net8.0
+ net8.0;netstandard2.1
enable
enable
+ latest
CultMath
CultMath
CultMath
diff --git a/src/CultMath/CultMath.csproj.meta b/src/CultMath/CultMath.csproj.meta
new file mode 100644
index 0000000..78fc304
--- /dev/null
+++ b/src/CultMath/CultMath.csproj.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 77e1a0a2512910f41885650a7d0d6f60
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/src/CultMath/CultMathTone.cs.meta b/src/CultMath/CultMathTone.cs.meta
new file mode 100644
index 0000000..f41d147
--- /dev/null
+++ b/src/CultMath/CultMathTone.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d4bfc66f9f7e992448f884c2c338d50a
\ No newline at end of file
diff --git a/src/CultMath/IsExternalInit.cs b/src/CultMath/IsExternalInit.cs
new file mode 100644
index 0000000..b05eb4a
--- /dev/null
+++ b/src/CultMath/IsExternalInit.cs
@@ -0,0 +1,5 @@
+namespace System.Runtime.CompilerServices;
+
+internal static class IsExternalInit
+{
+}
diff --git a/src/CultMath/IsExternalInit.cs.meta b/src/CultMath/IsExternalInit.cs.meta
new file mode 100644
index 0000000..ca761bc
--- /dev/null
+++ b/src/CultMath/IsExternalInit.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: de2b3c3e7f03c354aa2b84fbbc55b497
\ No newline at end of file
diff --git a/src/CultMath/Random.cs b/src/CultMath/Random.cs
new file mode 100644
index 0000000..9931a87
--- /dev/null
+++ b/src/CultMath/Random.cs
@@ -0,0 +1,62 @@
+namespace CultMath;
+
+public struct Random
+{
+ private const uint DefaultSeed = 0x6E624EB7u;
+
+ public uint state;
+
+ public Random(uint seed)
+ {
+ state = seed == 0 ? DefaultSeed : seed;
+ }
+
+ public static Random CreateFromIndex(uint index) => new(index + DefaultSeed);
+
+ public void InitState(uint seed)
+ {
+ state = seed == 0 ? DefaultSeed : seed;
+ }
+
+ public uint NextUInt()
+ {
+ var x = state == 0 ? DefaultSeed : state;
+ x ^= x << 13;
+ x ^= x >> 17;
+ x ^= x << 5;
+ state = x == 0 ? DefaultSeed : x;
+ return state;
+ }
+
+ public int NextInt(int max) => NextInt(0, max);
+
+ public int NextInt(int min, int max)
+ {
+ if (max <= min)
+ {
+ return min;
+ }
+
+ return min + (int)(NextUInt() % (uint)(max - min));
+ }
+
+ public float NextFloat() => (NextUInt() >> 8) * (1.0f / 16777216.0f);
+
+ public float NextFloat(float max) => NextFloat() * max;
+
+ public float NextFloat(float min, float max) => min + NextFloat() * (max - min);
+
+ public float2 NextFloat2() => new(NextFloat(), NextFloat());
+
+ public float2 NextFloat2(float min, float max) => new(NextFloat(min, max), NextFloat(min, max));
+
+ public float2 NextFloat2(float2 min, float2 max) => min + NextFloat2() * (max - min);
+
+ public float2 NextFloat2Direction()
+ {
+ var angle = NextFloat(0.0f, math.TAU);
+ return new float2(math.cos(angle), math.sin(angle));
+ }
+
+ public float3 NextFloat3(float3 min, float3 max) => min + new float3(NextFloat(), NextFloat(), NextFloat()) * (max - min);
+}
diff --git a/src/CultMath/Random.cs.meta b/src/CultMath/Random.cs.meta
new file mode 100644
index 0000000..55032de
--- /dev/null
+++ b/src/CultMath/Random.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7c1a557259026da4ca20b54264852c13
\ No newline at end of file
diff --git a/src/CultMath/SphericalErosion.cs b/src/CultMath/SphericalErosion.cs
new file mode 100644
index 0000000..93ddb0e
--- /dev/null
+++ b/src/CultMath/SphericalErosion.cs
@@ -0,0 +1,108 @@
+namespace CultMath;
+
+public readonly record struct SphericalErosionParameters(
+ float Frequency,
+ float Amplitude,
+ float Lacunarity,
+ float Gain,
+ float SlopeStrength,
+ float Detail,
+ float Seed,
+ int Octaves)
+{
+ public static SphericalErosionParameters Default => new(18.0f, 0.035f, 2.03f, 0.52f, 6.0f, 1.35f, 0.0f, 5);
+}
+
+public readonly record struct SphericalErosionSample(float HeightOffset, float Ridge, float Gully, float3 Gradient);
+
+///
+/// Stateless, point-evaluable erosion-like synthesis on a unit sphere.
+/// The matching shader body is cultmath_spherical_erosion in CultMath.hlsl.
+/// Position and gradient are planet-space; the gradient is projected tangent
+/// internally so cube faces and tile request order never enter the result.
+///
+public static class SphericalErosion
+{
+ public static SphericalErosionSample Sample(
+ float3 unitPosition,
+ float baseHeight,
+ float3 baseGradient,
+ SphericalErosionParameters parameters)
+ {
+ var position = math.normalize(unitPosition);
+ var gradient = baseGradient - position * math.dot(baseGradient, position);
+ var slope = math.length(gradient);
+ var flow = slope > 1.0e-6f ? -gradient / slope : StableTangent(position);
+ var frequency = math.max(parameters.Frequency, 0.001f);
+ var amplitude = parameters.Amplitude;
+ var combiMask = EaseOut(math.saturate(slope * parameters.SlopeStrength));
+ var fadeTarget = math.saturate(baseHeight * 0.5f + 0.5f) * 2.0f - 1.0f;
+ var heightOffset = 0.0f;
+ var ridge = 0.0f;
+ var gully = 0.0f;
+
+ var octaveCount = math.min(math.max(parameters.Octaves, 0), 8);
+ for (var octave = 0; octave < octaveCount; octave++)
+ {
+ var phase = DirectionalCellWave(position * frequency, flow, parameters.Seed + octave * 19.19f);
+ var wave = math.cos(phase);
+ var waveSlope = math.sin(phase);
+ var faded = math.lerp(fadeTarget, wave, combiMask);
+ heightOffset += faded * amplitude;
+ ridge = math.max(ridge, math.saturate(faded));
+ gully = math.max(gully, math.saturate(-faded));
+
+ var across = math.normalize(math.cross(position, flow));
+ gradient += across * (math.sign(waveSlope) * amplitude * frequency * combiMask);
+ gradient -= position * math.dot(gradient, position);
+ slope = math.length(gradient);
+ flow = slope > 1.0e-6f ? -gradient / slope : StableTangent(position);
+
+ var newMask = EaseOut(math.saturate(math.abs(waveSlope) * 1.35f));
+ combiMask = PowInverse(combiMask, parameters.Detail) * newMask;
+ fadeTarget = faded;
+ frequency *= math.max(parameters.Lacunarity, 1.01f);
+ amplitude *= math.saturate(parameters.Gain);
+ }
+
+ return new SphericalErosionSample(heightOffset, ridge, gully, gradient);
+ }
+
+ private static float DirectionalCellWave(float3 position, float3 flow, float seed)
+ {
+ var cell = math.floor(position);
+ var local = math.frac(position);
+ var weights = local * local * (3.0f - 2.0f * local);
+ var x00 = CellWave(position, flow, cell + new float3(0, 0, 0), seed);
+ var x10 = CellWave(position, flow, cell + new float3(1, 0, 0), seed);
+ var x01 = CellWave(position, flow, cell + new float3(0, 1, 0), seed);
+ var x11 = CellWave(position, flow, cell + new float3(1, 1, 0), seed);
+ var y00 = math.lerp(x00, x10, weights.x);
+ var y10 = math.lerp(x01, x11, weights.x);
+ var z0 = math.lerp(y00, y10, weights.y);
+ var x00z = CellWave(position, flow, cell + new float3(0, 0, 1), seed);
+ var x10z = CellWave(position, flow, cell + new float3(1, 0, 1), seed);
+ var x01z = CellWave(position, flow, cell + new float3(0, 1, 1), seed);
+ var x11z = CellWave(position, flow, cell + new float3(1, 1, 1), seed);
+ var z1 = math.lerp(math.lerp(x00z, x10z, weights.x), math.lerp(x01z, x11z, weights.x), weights.y);
+ return math.lerp(z0, z1, weights.z);
+ }
+
+ private static float CellWave(float3 position, float3 flow, float3 cell, float seed)
+ {
+ var pivot = cell + new float3(
+ math.hash(cell + seed + 11.7f),
+ math.hash(cell.yzx + seed + 37.1f),
+ math.hash(cell.zyx + seed + 73.9f));
+ return math.dot(position - pivot, flow) * math.TAU + math.hash(cell + seed + 101.3f) * math.TAU;
+ }
+
+ private static float3 StableTangent(float3 position)
+ {
+ var axis = math.abs(position.z) < 0.8f ? new float3(0, 0, 1) : new float3(0, 1, 0);
+ return math.normalize(math.cross(axis, position));
+ }
+
+ private static float EaseOut(float value) => 1.0f - (1.0f - value) * (1.0f - value);
+ private static float PowInverse(float value, float power) => 1.0f - math.pow(1.0f - math.saturate(value), math.max(power, 0.001f));
+}
diff --git a/src/CultMath/Voronoi.cs b/src/CultMath/Voronoi.cs
index 9791a8e..38b52d8 100644
--- a/src/CultMath/Voronoi.cs
+++ b/src/CultMath/Voronoi.cs
@@ -1,3 +1,4 @@
+using System;
using System.Runtime.InteropServices;
namespace CultMath;
diff --git a/src/CultMath/Voronoi.cs.meta b/src/CultMath/Voronoi.cs.meta
new file mode 100644
index 0000000..6c73808
--- /dev/null
+++ b/src/CultMath/Voronoi.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cb530f1428ac8ff43939b986986ae959
\ No newline at end of file
diff --git a/src/CultMath/bool2.cs b/src/CultMath/bool2.cs
new file mode 100644
index 0000000..95ab29e
--- /dev/null
+++ b/src/CultMath/bool2.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace CultMath;
+
+public record struct bool2(bool x, bool y)
+{
+ public static readonly bool2 @false = new(false, false);
+ public static readonly bool2 @true = new(true, true);
+
+ public bool this[int index] => index switch
+ {
+ 0 => x,
+ 1 => y,
+ _ => throw new ArgumentOutOfRangeException(nameof(index)),
+ };
+
+ public static implicit operator bool2(bool value) => new(value, value);
+}
diff --git a/src/CultMath/bool2.cs.meta b/src/CultMath/bool2.cs.meta
new file mode 100644
index 0000000..477ff14
--- /dev/null
+++ b/src/CultMath/bool2.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 725efb2725e987449a3782452f5a95e0
\ No newline at end of file
diff --git a/src/CultMath/double2.cs b/src/CultMath/double2.cs
new file mode 100644
index 0000000..000c3cb
--- /dev/null
+++ b/src/CultMath/double2.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace CultMath;
+
+public record struct double2(double x, double y)
+{
+ public static readonly double2 zero = new(0.0, 0.0);
+ public static readonly double2 one = new(1.0, 1.0);
+
+ public double this[int index] => index switch
+ {
+ 0 => x,
+ 1 => y,
+ _ => throw new ArgumentOutOfRangeException(nameof(index)),
+ };
+
+ public static implicit operator double2(double value) => new(value, value);
+
+ public static double2 operator +(double2 value) => value;
+ public static double2 operator -(double2 value) => new(-value.x, -value.y);
+ public static double2 operator +(double2 left, double2 right) => new(left.x + right.x, left.y + right.y);
+ public static double2 operator -(double2 left, double2 right) => new(left.x - right.x, left.y - right.y);
+ public static double2 operator *(double2 left, double2 right) => new(left.x * right.x, left.y * right.y);
+ public static double2 operator /(double2 left, double2 right) => new(left.x / right.x, left.y / right.y);
+}
diff --git a/src/CultMath/double2.cs.meta b/src/CultMath/double2.cs.meta
new file mode 100644
index 0000000..61243b2
--- /dev/null
+++ b/src/CultMath/double2.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bbe739a5f0216c3459762d2b5ee7401a
\ No newline at end of file
diff --git a/src/CultMath/double3.cs b/src/CultMath/double3.cs
new file mode 100644
index 0000000..b142750
--- /dev/null
+++ b/src/CultMath/double3.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace CultMath;
+
+public record struct double3(double x, double y, double z)
+{
+ public static readonly double3 zero = new(0.0, 0.0, 0.0);
+ public static readonly double3 one = new(1.0, 1.0, 1.0);
+
+ public double2 xy
+ {
+ get => new(x, y);
+ set
+ {
+ x = value.x;
+ y = value.y;
+ }
+ }
+
+ public double this[int index] => index switch
+ {
+ 0 => x,
+ 1 => y,
+ 2 => z,
+ _ => throw new ArgumentOutOfRangeException(nameof(index)),
+ };
+
+ public static implicit operator double3(double value) => new(value, value, value);
+
+ public static double3 operator +(double3 value) => value;
+ public static double3 operator -(double3 value) => new(-value.x, -value.y, -value.z);
+ public static double3 operator +(double3 left, double3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z);
+ public static double3 operator -(double3 left, double3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z);
+ public static double3 operator *(double3 left, double3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z);
+ public static double3 operator /(double3 left, double3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z);
+}
diff --git a/src/CultMath/double3.cs.meta b/src/CultMath/double3.cs.meta
new file mode 100644
index 0000000..302d13d
--- /dev/null
+++ b/src/CultMath/double3.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1cbaafd219703ee43ae279aa5cbf407f
\ No newline at end of file
diff --git a/src/CultMath/float2.cs b/src/CultMath/float2.cs
index 1f1d66b..5298494 100644
--- a/src/CultMath/float2.cs
+++ b/src/CultMath/float2.cs
@@ -1,6 +1,8 @@
+using System;
+
namespace CultMath;
-public readonly record struct float2(float x, float y)
+public record struct float2(float x, float y)
{
public static readonly float2 zero = new(0.0f, 0.0f);
public static readonly float2 one = new(1.0f, 1.0f);
diff --git a/src/CultMath/float2.cs.meta b/src/CultMath/float2.cs.meta
new file mode 100644
index 0000000..c82f69a
--- /dev/null
+++ b/src/CultMath/float2.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: fa066a0aed5494f499d462a2ebc51e50
\ No newline at end of file
diff --git a/src/CultMath/float3.cs b/src/CultMath/float3.cs
index 70cfa02..e9833ae 100644
--- a/src/CultMath/float3.cs
+++ b/src/CultMath/float3.cs
@@ -1,6 +1,8 @@
+using System;
+
namespace CultMath;
-public readonly record struct float3(float x, float y, float z)
+public record struct float3(float x, float y, float z)
{
public static readonly float3 zero = new(0.0f, 0.0f, 0.0f);
public static readonly float3 one = new(1.0f, 1.0f, 1.0f);
@@ -10,9 +12,48 @@ public readonly record struct float3(float x, float y, float z)
{
}
- public float2 xy => new(x, y);
- public float2 xz => new(x, z);
- public float2 yz => new(y, z);
+ public float2 xy
+ {
+ get => new(x, y);
+ set
+ {
+ x = value.x;
+ y = value.y;
+ }
+ }
+
+ public float2 xz
+ {
+ get => new(x, z);
+ set
+ {
+ x = value.x;
+ z = value.y;
+ }
+ }
+
+ public float2 yz
+ {
+ get => new(y, z);
+ set
+ {
+ y = value.x;
+ z = value.y;
+ }
+ }
+
+ public float2 zy
+ {
+ get => new(z, y);
+ set
+ {
+ z = value.x;
+ y = value.y;
+ }
+ }
+
+ public float3 yzx => new(y, z, x);
+ public float3 zyx => new(z, y, x);
public float this[int index] => index switch
{
diff --git a/src/CultMath/float3.cs.meta b/src/CultMath/float3.cs.meta
new file mode 100644
index 0000000..3c649c4
--- /dev/null
+++ b/src/CultMath/float3.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 58a355dd1cf6b9f48a7595eea0c0e1f2
\ No newline at end of file
diff --git a/src/CultMath/float4.cs b/src/CultMath/float4.cs
index c7bfb40..b6eed09 100644
--- a/src/CultMath/float4.cs
+++ b/src/CultMath/float4.cs
@@ -1,6 +1,8 @@
+using System;
+
namespace CultMath;
-public readonly record struct float4(float x, float y, float z, float w)
+public record struct float4(float x, float y, float z, float w)
{
public static readonly float4 zero = new(0.0f, 0.0f, 0.0f, 0.0f);
public static readonly float4 one = new(1.0f, 1.0f, 1.0f, 1.0f);
@@ -15,7 +17,22 @@ public readonly record struct float4(float x, float y, float z, float w)
{
}
- public float2 xy => new(x, y);
+ public float4(float x, float3 yzw)
+ : this(x, yzw.x, yzw.y, yzw.z)
+ {
+ }
+
+ public float2 xy
+ {
+ get => new(x, y);
+ set
+ {
+ x = value.x;
+ y = value.y;
+ }
+ }
+ public float3 xyw => new(x, y, w);
+ public float3 yzx => new(y, z, x);
public float3 xyz => new(x, y, z);
public float this[int index] => index switch
diff --git a/src/CultMath/float4.cs.meta b/src/CultMath/float4.cs.meta
new file mode 100644
index 0000000..206444e
--- /dev/null
+++ b/src/CultMath/float4.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: fe24fbdab33538849bc6be0afbb0a692
\ No newline at end of file
diff --git a/src/CultMath/int2.cs b/src/CultMath/int2.cs
new file mode 100644
index 0000000..7c2f350
--- /dev/null
+++ b/src/CultMath/int2.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace CultMath;
+
+public record struct int2(int x, int y)
+{
+ public static readonly int2 zero = new(0, 0);
+ public static readonly int2 one = new(1, 1);
+
+ public int this[int index] => index switch
+ {
+ 0 => x,
+ 1 => y,
+ _ => throw new ArgumentOutOfRangeException(nameof(index)),
+ };
+
+ public static implicit operator int2(int value) => new(value, value);
+ public static implicit operator float2(int2 value) => new(value.x, value.y);
+ public static explicit operator System.Numerics.Vector2(int2 value) => new(value.x, value.y);
+ public static explicit operator int2(System.Numerics.Vector2 value) => new((int)value.X, (int)value.Y);
+
+ public static int2 operator +(int2 value) => value;
+ public static int2 operator -(int2 value) => new(-value.x, -value.y);
+ public static int2 operator +(int2 left, int2 right) => new(left.x + right.x, left.y + right.y);
+ public static int2 operator -(int2 left, int2 right) => new(left.x - right.x, left.y - right.y);
+ public static int2 operator *(int2 left, int2 right) => new(left.x * right.x, left.y * right.y);
+ public static int2 operator /(int2 left, int2 right) => new(left.x / right.x, left.y / right.y);
+ public static int2 operator %(int2 left, int2 right) => new(left.x % right.x, left.y % right.y);
+}
diff --git a/src/CultMath/int2.cs.meta b/src/CultMath/int2.cs.meta
new file mode 100644
index 0000000..5920328
--- /dev/null
+++ b/src/CultMath/int2.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 577b174a998aaa4478ec26e7a6782f46
\ No newline at end of file
diff --git a/src/CultMath/math.cs b/src/CultMath/math.cs
index 817aa7f..874554a 100644
--- a/src/CultMath/math.cs
+++ b/src/CultMath/math.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace CultMath;
public static class math
@@ -8,13 +10,20 @@ public static class math
public static float2 float2(float x, float y) => new(x, y);
public static float2 float2(float value) => new(value, value);
+ 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);
public static float3 float3(float x, float y, float z) => new(x, y, z);
public static float3 float3(float2 xy, float z) => new(xy, 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(float2 xy, float z, float w) => new(xy, z, w);
public static float4 float4(float3 xyz, float w) => new(xyz, w);
+ public static float4 float4(float x, float3 yzw) => new(x, yzw);
public static float4 float4(float value) => new(value, value, value, value);
+ public static rect rect(float2 min, float2 max) => new(min, max);
+ public static rect rect(float minX, float minY, float maxX, float maxY) => new(minX, minY, maxX, maxY);
public static float radians(float degrees) => degrees * (PI / 180.0f);
public static float degrees(float radians) => radians * (180.0f / PI);
@@ -26,16 +35,26 @@ public static class math
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);
+ public static float pow(float value, float power) => MathF.Pow(value, power);
+ public static bool isinf(float value) => float.IsInfinity(value);
public static float exp(float value) => MathF.Exp(value);
+ public static double exp(double value) => Math.Exp(value);
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));
+ 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));
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));
+ public static float sign(float value) => MathF.Sign(value);
+ public static float2 sign(float2 value) => new(sign(value.x), sign(value.y));
+ public static float3 sign(float3 value) => new(sign(value.x), sign(value.y), sign(value.z));
+ public static float4 sign(float4 value) => new(sign(value.x), sign(value.y), sign(value.z), sign(value.w));
+
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));
@@ -52,11 +71,13 @@ public static class math
public static float4 frac(float4 value) => value - floor(value);
public static float min(float left, float right) => MathF.Min(left, right);
+ public static int min(int left, int right) => Math.Min(left, right);
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));
public static float max(float left, float right) => MathF.Max(left, right);
+ public static int max(int left, int right) => Math.Max(left, right);
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));
@@ -72,9 +93,20 @@ public static class math
public static float4 saturate(float4 value) => clamp(value, 0.0f, 1.0f);
public static float lerp(float start, float end, float amount) => start + (end - start) * amount;
+ public static double lerp(double start, double end, double amount) => start + (end - start) * amount;
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;
+ 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);
public static float step(float edge, float value) => value < edge ? 0.0f : 1.0f;
public static float2 step(float2 edge, float2 value) => new(step(edge.x, value.x), step(edge.y, value.y));
@@ -130,10 +162,30 @@ public static class math
public static float2 normalize(float2 value) => value / MathF.Max(length(value), 1.0e-20f);
public static float3 normalize(float3 value) => value / MathF.Max(length(value), 1.0e-20f);
public static float4 normalize(float4 value) => value / MathF.Max(length(value), 1.0e-20f);
+ 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);
+ }
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;
+ 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));
+
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;
diff --git a/src/CultMath/math.cs.meta b/src/CultMath/math.cs.meta
new file mode 100644
index 0000000..9222af7
--- /dev/null
+++ b/src/CultMath/math.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0d9b4cacc40d169429ee743d19577cb7
\ No newline at end of file
diff --git a/src/CultMath/quaternion.cs b/src/CultMath/quaternion.cs
new file mode 100644
index 0000000..fe8dda8
--- /dev/null
+++ b/src/CultMath/quaternion.cs
@@ -0,0 +1,68 @@
+namespace CultMath;
+
+public record struct quaternion(float x, float y, float z, float w)
+{
+ public static readonly quaternion identity = new(0.0f, 0.0f, 0.0f, 1.0f);
+
+ public static quaternion LookRotation(float3 forward, float3 up)
+ {
+ forward = math.normalize(forward);
+ up = math.normalize(up);
+
+ var right = math.normalize(math.cross(up, forward));
+ up = math.cross(forward, right);
+
+ var m00 = right.x;
+ var m01 = up.x;
+ var m02 = forward.x;
+ var m10 = right.y;
+ var m11 = up.y;
+ var m12 = forward.y;
+ var m20 = right.z;
+ var m21 = up.z;
+ var m22 = forward.z;
+
+ var trace = m00 + m11 + m22;
+ if (trace > 0.0f)
+ {
+ var s = math.sqrt(trace + 1.0f) * 2.0f;
+ return math.normalize(new quaternion(
+ (m21 - m12) / s,
+ (m02 - m20) / s,
+ (m10 - m01) / s,
+ 0.25f * s));
+ }
+
+ if (m00 > m11 && m00 > m22)
+ {
+ var s = math.sqrt(1.0f + m00 - m11 - m22) * 2.0f;
+ return math.normalize(new quaternion(
+ 0.25f * s,
+ (m01 + m10) / s,
+ (m02 + m20) / s,
+ (m21 - m12) / s));
+ }
+
+ if (m11 > m22)
+ {
+ var s = math.sqrt(1.0f + m11 - m00 - m22) * 2.0f;
+ return math.normalize(new quaternion(
+ (m01 + m10) / s,
+ 0.25f * s,
+ (m12 + m21) / s,
+ (m02 - m20) / s));
+ }
+
+ {
+ var s = math.sqrt(1.0f + m22 - m00 - m11) * 2.0f;
+ return math.normalize(new quaternion(
+ (m02 + m20) / s,
+ (m12 + m21) / s,
+ 0.25f * s,
+ (m10 - m01) / s));
+ }
+ }
+
+ public static implicit operator quaternion(float4 value) => new(value.x, value.y, value.z, value.w);
+ public static implicit operator float4(quaternion value) => new(value.x, value.y, value.z, value.w);
+}
diff --git a/src/CultMath/quaternion.cs.meta b/src/CultMath/quaternion.cs.meta
new file mode 100644
index 0000000..ae6e8a1
--- /dev/null
+++ b/src/CultMath/quaternion.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 12f3627d9250dc348be9c1a1056ec7d0
\ No newline at end of file
diff --git a/src/CultMath/rect.cs b/src/CultMath/rect.cs
new file mode 100644
index 0000000..67b210b
--- /dev/null
+++ b/src/CultMath/rect.cs
@@ -0,0 +1,21 @@
+namespace CultMath;
+
+public readonly record struct rect(float2 min, float2 max)
+{
+ public rect(float minX, float minY, float maxX, float maxY)
+ : this(math.float2(math.min(minX, maxX), math.min(minY, maxY)),
+ math.float2(math.max(minX, maxX), math.max(minY, maxY)))
+ {
+ }
+
+ public float2 size => max - min;
+ public float2 center => (min + max) * 0.5f;
+
+ public bool Contains(float2 point)
+ {
+ return point.x >= min.x &&
+ point.x <= max.x &&
+ point.y >= min.y &&
+ point.y <= max.y;
+ }
+}
diff --git a/src/CultMath/rect.cs.meta b/src/CultMath/rect.cs.meta
new file mode 100644
index 0000000..9f7598e
--- /dev/null
+++ b/src/CultMath/rect.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b00dc5f108967db47ac2f54a47033e33
\ No newline at end of file
diff --git a/tests.meta b/tests.meta
new file mode 100644
index 0000000..a35356c
--- /dev/null
+++ b/tests.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4ca61dedbfb3c5d46b0acaf23202b5c2
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/tests/CultMath.Tests.meta b/tests/CultMath.Tests.meta
new file mode 100644
index 0000000..3f1451c
--- /dev/null
+++ b/tests/CultMath.Tests.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: bcee65f13f85daa45b0cf3fdaa901779
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/tests/CultMath.Tests/CultMath.Tests.csproj.meta b/tests/CultMath.Tests/CultMath.Tests.csproj.meta
new file mode 100644
index 0000000..709184b
--- /dev/null
+++ b/tests/CultMath.Tests/CultMath.Tests.csproj.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 636051db121e19146a0baf92b790415c
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/tests/CultMath.Tests/HlslMirrorTests.cs b/tests/CultMath.Tests/HlslMirrorTests.cs
index 91a95f4..ed9119b 100644
--- a/tests/CultMath.Tests/HlslMirrorTests.cs
+++ b/tests/CultMath.Tests/HlslMirrorTests.cs
@@ -4,6 +4,14 @@ namespace CultMath.Tests;
public sealed class HlslMirrorTests
{
+ [Fact]
+ public void HlslMirrorPublishesSphericalErosionKernel()
+ {
+ var source = File.ReadAllText(FindHlslMirror());
+ Assert.Contains("cultmath_spherical_erosion", source);
+ Assert.Contains("CultMathSphericalErosionParameters", source);
+ }
+
[Fact]
public void HlslMirrorPublishesCultMathPrimitives()
{
@@ -23,6 +31,7 @@ public sealed class HlslMirrorTests
"cultmath_distance",
"cultmath_normalize",
"cultmath_reflect",
+ "cultmath_rotate",
"cultmath_csum",
"cultmath_decay",
"cultmath_damp",
diff --git a/tests/CultMath.Tests/HlslMirrorTests.cs.meta b/tests/CultMath.Tests/HlslMirrorTests.cs.meta
new file mode 100644
index 0000000..1fcf44b
--- /dev/null
+++ b/tests/CultMath.Tests/HlslMirrorTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3d7eb9df849ea0148a71419bbfe2352c
\ No newline at end of file
diff --git a/tests/CultMath.Tests/MathTests.cs b/tests/CultMath.Tests/MathTests.cs
index 44dd129..bb816d4 100644
--- a/tests/CultMath.Tests/MathTests.cs
+++ b/tests/CultMath.Tests/MathTests.cs
@@ -23,6 +23,7 @@ public sealed class MathTests
[Fact]
public void VectorConstructorsAcceptLowerDimensionVectors()
{
+ Assert.Equal(new int2(1, 2), math.int2(1, 2));
Assert.Equal(new float2(1.0f, 2.0f), math.float2(1.0f, 2.0f));
Assert.Equal(new float3(1.0f, 1.0f, 1.0f), math.float3(1.0f));
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), math.float4(new float3(1.0f, 2.0f, 3.0f), 4.0f));
@@ -31,6 +32,112 @@ public sealed class MathTests
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), new float4(new float3(1.0f, 2.0f, 3.0f), 4.0f));
}
+ [Fact]
+ public void RectNormalizesBoundsAndContainsPoints()
+ {
+ var viewport = math.rect(10.0f, 20.0f, -5.0f, -2.0f);
+
+ Assert.Equal(new float2(-5.0f, -2.0f), viewport.min);
+ Assert.Equal(new float2(10.0f, 20.0f), viewport.max);
+ Assert.Equal(new float2(15.0f, 22.0f), viewport.size);
+ Assert.Equal(new float2(2.5f, 9.0f), viewport.center);
+ Assert.True(viewport.Contains(new float2(0.0f, 0.0f)));
+ Assert.False(viewport.Contains(new float2(11.0f, 0.0f)));
+ }
+
+ [Fact]
+ public void VectorsExposeCommonReadOnlySwizzles()
+ {
+ Assert.Equal(new float3(2.0f, 3.0f, 1.0f), new float3(1.0f, 2.0f, 3.0f).yzx);
+ Assert.Equal(new float3(3.0f, 2.0f, 1.0f), new float3(1.0f, 2.0f, 3.0f).zyx);
+ Assert.Equal(new float2(3.0f, 2.0f), new float3(1.0f, 2.0f, 3.0f).zy);
+ Assert.Equal(new float3(1.0f, 2.0f, 4.0f), new float4(1.0f, 2.0f, 3.0f, 4.0f).xyw);
+ Assert.Equal(new float3(2.0f, 3.0f, 1.0f), new float4(1.0f, 2.0f, 3.0f, 4.0f).yzx);
+ Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), new float4(1.0f, new float3(2.0f, 3.0f, 4.0f)));
+ Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), math.float4(1.0f, new float3(2.0f, 3.0f, 4.0f)));
+ }
+
+ [Fact]
+ public void VectorsSupportUnityStyleFieldAndSwizzleMutation()
+ {
+ var position = new float3(1.0f, 2.0f, 3.0f);
+ position.y = 5.0f;
+ position.xz = new float2(8.0f, 13.0f);
+
+ Assert.Equal(new float3(8.0f, 5.0f, 13.0f), position);
+
+ var bounds = new float4(1.0f, 2.0f, 3.0f, 4.0f);
+ bounds.xy = new float2(-1.0f, -2.0f);
+
+ Assert.Equal(new float4(-1.0f, -2.0f, 3.0f, 4.0f), bounds);
+ }
+
+ [Fact]
+ public void DoubleVectorsSupportDampedMath()
+ {
+ var value = new double3(1.0, 2.0, 3.0);
+ value.xy = new double2(5.0, 8.0);
+
+ Assert.Equal(new double3(5.0, 8.0, 3.0), value);
+ Assert.Equal(new double2(Math.Exp(-2.0), Math.Exp(-4.0)), math.exp(new double2(-2.0, -4.0)));
+ Assert.Equal(new double3(2.0, 4.0, 6.0), math.lerp(new double3(0.0, 0.0, 0.0), new double3(4.0, 8.0, 12.0), new double3(0.5, 0.5, 0.5)));
+ }
+
+ [Fact]
+ public void Int2SupportsGridArithmeticAndFloatPromotion()
+ {
+ var cell = new int2(2, 3);
+
+ Assert.Equal(new int2(3, 5), cell + new int2(1, 2));
+ Assert.Equal(new int2(4, 6), cell * new int2(2, 2));
+ Assert.Equal(3, cell[1]);
+
+ float2 promoted = cell;
+ Assert.Equal(new float2(2.0f, 3.0f), promoted);
+ }
+
+ [Fact]
+ public void Bool2SupportsHullGridStyleStorage()
+ {
+ bool2 conductivity = true;
+
+ conductivity.y = false;
+
+ Assert.True(conductivity.x);
+ Assert.False(conductivity[1]);
+ Assert.Equal(new bool2(false, true), math.bool2(false, true));
+ }
+
+ [Fact]
+ public void QuaternionLookRotationProducesUnitOrientation()
+ {
+ var rotation = quaternion.LookRotation(new float3(0.0f, 0.0f, 1.0f), new float3(0.0f, 1.0f, 0.0f));
+
+ Assert.Equal(quaternion.identity, rotation);
+ Assert.Equal(1.0f, math.length((float4)quaternion.LookRotation(new float3(1.0f, 0.0f, 0.0f), new float3(0.0f, 1.0f, 0.0f))), precision: 5);
+ }
+
+ [Fact]
+ public void RandomIsDeterministicAndUnityShaped()
+ {
+ var a = new Random(1234);
+ var b = new Random(1234);
+
+ Assert.Equal(a.NextUInt(), b.NextUInt());
+ Assert.Equal(a.NextFloat(), b.NextFloat());
+
+ var bounded = a.NextFloat(-2.0f, 4.0f);
+ Assert.True(bounded >= -2.0f && bounded < 4.0f);
+
+ var direction = a.NextFloat2Direction();
+ Assert.Equal(1.0f, math.length(direction), precision: 5);
+
+ var point = a.NextFloat3(new float3(-1.0f, -2.0f, -3.0f), new float3(1.0f, 2.0f, 3.0f));
+ Assert.True(point.x >= -1.0f && point.x < 1.0f);
+ Assert.True(point.y >= -2.0f && point.y < 2.0f);
+ Assert.True(point.z >= -3.0f && point.z < 3.0f);
+ }
+
[Fact]
public void NormalizeProducesUnitLength()
{
@@ -55,12 +162,39 @@ public sealed class MathTests
Assert.Equal(-1.0f, math.cos(math.PI), precision: 5);
Assert.Equal(1.0f, math.tan(math.PI * 0.25f), precision: 5);
Assert.Equal(math.HALF_PI, math.atan2(1.0f, 0.0f), precision: 5);
+ Assert.Equal(2, math.min(2, 5));
+ Assert.Equal(5, math.max(2, 5));
+ Assert.True(math.isinf(float.PositiveInfinity));
+ }
+
+ [Fact]
+ public void SignIsComponentWiseForSimulationVectors()
+ {
+ Assert.Equal(-1.0f, math.sign(-12.0f));
+ Assert.Equal(0.0f, math.sign(0.0f));
+ Assert.Equal(1.0f, math.sign(12.0f));
+ Assert.Equal(new float3(-1.0f, 0.0f, 1.0f), math.sign(new float3(-2.0f, 0.0f, 4.0f)));
+ }
+
+ [Fact]
+ public void RotateTurnsFloat2CounterClockwiseInRadians()
+ {
+ var rotated = math.rotate(new float2(1.0f, 0.0f), math.HALF_PI);
+
+ Assert.Equal(0.0f, rotated.x, precision: 5);
+ Assert.Equal(1.0f, rotated.y, precision: 5);
+
+ var rotatedDegrees = math.rotate_degrees(new float2(0.0f, 1.0f), 90.0f);
+ Assert.Equal(-1.0f, rotatedDegrees.x, precision: 5);
+ Assert.Equal(0.0f, rotatedDegrees.y, precision: 5);
}
[Fact]
public void AetheriaPrimitivesRemainSmallAndDeterministic()
{
Assert.Equal(6.0f, math.csum(new float3(1.0f, 2.0f, 3.0f)));
+ Assert.Equal(0.25f, math.unlerp(10.0f, 30.0f, 15.0f));
+ Assert.Equal(new float2(0.25f, 0.75f), math.unlerp(new float2(10.0f, 10.0f), new float2(30.0f, 30.0f), new float2(15.0f, 25.0f)));
Assert.Equal(MathF.Exp(-2.0f), math.decay(1.0f, 2.0f, 1.0f), precision: 5);
Assert.Equal(1.0f - MathF.Exp(-2.0f), math.damp(0.0f, 1.0f, 2.0f, 1.0f), precision: 5);
diff --git a/tests/CultMath.Tests/MathTests.cs.meta b/tests/CultMath.Tests/MathTests.cs.meta
new file mode 100644
index 0000000..277df5a
--- /dev/null
+++ b/tests/CultMath.Tests/MathTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c7a709e5345da214aa650e8c796fcaff
\ No newline at end of file
diff --git a/tests/CultMath.Tests/SphericalErosionTests.cs b/tests/CultMath.Tests/SphericalErosionTests.cs
new file mode 100644
index 0000000..8ae0485
--- /dev/null
+++ b/tests/CultMath.Tests/SphericalErosionTests.cs
@@ -0,0 +1,27 @@
+namespace CultMath.Tests;
+
+public sealed class SphericalErosionTests
+{
+ [Fact]
+ public void SampleIsDeterministicAndScaleInvariant()
+ {
+ var parameters = SphericalErosionParameters.Default with { Seed = 42.0f };
+ var a = SphericalErosion.Sample(new float3(0.31f, -0.74f, 0.59f), 0.2f, new float3(0.4f, -0.1f, 0.2f), parameters);
+ var b = SphericalErosion.Sample(new float3(3.1f, -7.4f, 5.9f), 0.2f, new float3(0.4f, -0.1f, 0.2f), parameters);
+
+ Assert.Equal(a, b);
+ Assert.InRange(a.HeightOffset, -0.08f, 0.08f);
+ Assert.InRange(a.Ridge, 0.0f, 1.0f);
+ Assert.InRange(a.Gully, 0.0f, 1.0f);
+ }
+
+ [Fact]
+ public void NearbySamplesRemainContinuousAcrossAChartBoundary()
+ {
+ var parameters = SphericalErosionParameters.Default;
+ var left = SphericalErosion.Sample(math.normalize(new float3(1.0f, 0.99999f, 0.2f)), 0.1f, new float3(0.2f, -0.1f, 0.05f), parameters);
+ var right = SphericalErosion.Sample(math.normalize(new float3(0.99999f, 1.0f, 0.2f)), 0.1f, new float3(0.2f, -0.1f, 0.05f), parameters);
+
+ Assert.InRange(math.abs(left.HeightOffset - right.HeightOffset), 0.0f, 0.0002f);
+ }
+}
diff --git a/tools.meta b/tools.meta
new file mode 100644
index 0000000..3594fa6
--- /dev/null
+++ b/tools.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 697162910ffd9614ba9655d31af9afa4
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/tools/compile-hlsl-spirv.ps1.meta b/tools/compile-hlsl-spirv.ps1.meta
new file mode 100644
index 0000000..31e7948
--- /dev/null
+++ b/tools/compile-hlsl-spirv.ps1.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b47e7016a99a5d148bc44c399c13c4d3
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/tools/compile-spirv-metal.ps1.meta b/tools/compile-spirv-metal.ps1.meta
new file mode 100644
index 0000000..2e7e74c
--- /dev/null
+++ b/tools/compile-spirv-metal.ps1.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 6150c8f461e471b4c8641fd32d1984ef
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/tools/get-dxc.ps1.meta b/tools/get-dxc.ps1.meta
new file mode 100644
index 0000000..c9ab0d8
--- /dev/null
+++ b/tools/get-dxc.ps1.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 86d0b9a1006042745b7fff523831049c
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/tools/get-spirv-cross.ps1.meta b/tools/get-spirv-cross.ps1.meta
new file mode 100644
index 0000000..12ccdd8
--- /dev/null
+++ b/tools/get-spirv-cross.ps1.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: dcf41da07502f4b439f8fc5ad25d16c9
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant: