Complete portable CultMath runtime surface

This commit is contained in:
Meta Vens 2026-07-12 19:33:59 +01:00
commit ab6a2c6456
68 changed files with 1130 additions and 23 deletions

7
CultMath.slnx.meta Normal file
View file

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

6
Directory.Build.props Normal file
View file

@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<BaseOutputPath>$(MSBuildThisFileDirectory).tools\dotnet\bin\$(MSBuildProjectName)\</BaseOutputPath>
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory).tools\dotnet\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
</PropertyGroup>
</Project>

View file

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

7
README.md.meta Normal file
View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 867daebafe0ce48479cf043461ee6bdf
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
docs.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: de535b89253410b41b5989f712b80af4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

7
docs/design.md.meta Normal file
View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 07021a8be816e3448a9490c10845b0aa
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

7
global.json.meta Normal file
View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 28e4ce693c46cbd4b990c5790745d62f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
native.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9d1cb2ac88c530b4d9da456523115e12
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a42d65478346c354c8b4f353c5581b49
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

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

View file

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

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7de448b7e258d2c448bb7391051cc628
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

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

14
package.json Normal file
View file

@ -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"
}

7
package.json.meta Normal file
View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1abf066549a826c4ba7a30d6676735c0
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
shaders.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3bed74ebcba445543800218dbe185034
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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<float> 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

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 33174674795143c4a905408718f956f8
ShaderIncludeImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
src.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 64f48d0cae0810e4ba9903e9094fe4c6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
src/CultMath.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e857b457b93397d4fb4ad8481cb72e4c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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<float>.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<float>(positionX.Slice(i, lanes));
var py = new Vector<float>(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<float>(pxBuffer);
var py = new Vector<float>(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<float>(accelerationX.Slice(i, lanes)) + ax).CopyTo(accelerationX.Slice(i, lanes));
(new Vector<float>(accelerationY.Slice(i, lanes)) + ay).CopyTo(accelerationY.Slice(i, lanes));
(new Vector<float>(axBuffer) + ax).CopyTo(axBuffer);
(new Vector<float>(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<float2> positions,
float2 center,
float strength,
float radius,
Span<float2> 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<float> dynamicMask,
@ -102,25 +143,44 @@ public static class BatchMath
var delta = new Vector<float>(deltaTime);
var i = 0;
var lanes = Vector<float>.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<float>(dynamicMask.Slice(i, lanes));
var vx = new Vector<float>(velocityX.Slice(i, lanes));
var vy = new Vector<float>(velocityY.Slice(i, lanes));
var px = new Vector<float>(positionX.Slice(i, lanes));
var py = new Vector<float>(positionY.Slice(i, lanes));
var ax = new Vector<float>(accelerationX.Slice(i, lanes));
var ay = new Vector<float>(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<float>(maskBuffer);
var vx = new Vector<float>(vxBuffer);
var vy = new Vector<float>(vyBuffer);
var px = new Vector<float>(pxBuffer);
var py = new Vector<float>(pyBuffer);
var ax = new Vector<float>(axBuffer);
var ay = new Vector<float>(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<float> dynamicMask,
Span<float2> position,
Span<float2> velocity,
ReadOnlySpan<float2> 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<float> source, float[] destination)
{
for (var i = 0; i < source.Length; i++)
{
destination[i] = source[i];
}
}
private static void CopyArrayToSpan(float[] source, Span<float> destination)
{
for (var i = 0; i < destination.Length; i++)
{
destination[i] = source[i];
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d4771320e190bdd408dd296c293a9d0e

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bfa14a9aacb8ac4448761568643f2bc6

View file

@ -0,0 +1,14 @@
{
"name": "CultMath",
"rootNamespace": "CultMath",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": true
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f0b66b909d13e18468f7aa56221531e9
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;netstandard2.1</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<RootNamespace>CultMath</RootNamespace>
<AssemblyName>CultMath</AssemblyName>
<PackageId>CultMath</PackageId>

View file

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

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d4bfc66f9f7e992448f884c2c338d50a

View file

@ -0,0 +1,5 @@
namespace System.Runtime.CompilerServices;
internal static class IsExternalInit
{
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: de2b3c3e7f03c354aa2b84fbbc55b497

62
src/CultMath/Random.cs Normal file
View file

@ -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);
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7c1a557259026da4ca20b54264852c13

View file

@ -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);
/// <summary>
/// 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.
/// </summary>
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));
}

View file

@ -1,3 +1,4 @@
using System;
using System.Runtime.InteropServices;
namespace CultMath;

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cb530f1428ac8ff43939b986986ae959

18
src/CultMath/bool2.cs Normal file
View file

@ -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);
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 725efb2725e987449a3782452f5a95e0

25
src/CultMath/double2.cs Normal file
View file

@ -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);
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bbe739a5f0216c3459762d2b5ee7401a

36
src/CultMath/double3.cs Normal file
View file

@ -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);
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1cbaafd219703ee43ae279aa5cbf407f

View file

@ -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);

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fa066a0aed5494f499d462a2ebc51e50

View file

@ -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
{

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 58a355dd1cf6b9f48a7595eea0c0e1f2

View file

@ -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

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fe24fbdab33538849bc6be0afbb0a692

29
src/CultMath/int2.cs Normal file
View file

@ -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);
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 577b174a998aaa4478ec26e7a6782f46

View file

@ -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;

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0d9b4cacc40d169429ee743d19577cb7

View file

@ -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);
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 12f3627d9250dc348be9c1a1056ec7d0

21
src/CultMath/rect.cs Normal file
View file

@ -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;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b00dc5f108967db47ac2f54a47033e33

8
tests.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4ca61dedbfb3c5d46b0acaf23202b5c2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bcee65f13f85daa45b0cf3fdaa901779
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

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

View file

@ -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",

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3d7eb9df849ea0148a71419bbfe2352c

View file

@ -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);

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c7a709e5345da214aa650e8c796fcaff

View file

@ -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);
}
}

8
tools.meta Normal file
View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 697162910ffd9614ba9655d31af9afa4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

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

View file

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

7
tools/get-dxc.ps1.meta Normal file
View file

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

View file

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