From ab6a2c6456ec4b48311857daf24494f76cd1ca78 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 12 Jul 2026 19:33:59 +0100 Subject: [PATCH] Complete portable CultMath runtime surface --- CultMath.slnx.meta | 7 + Directory.Build.props | 6 + Directory.Build.props.meta | 7 + README.md.meta | 7 + docs.meta | 8 ++ docs/design.md.meta | 7 + global.json.meta | 7 + native.meta | 8 ++ native/cultmath-core.meta | 8 ++ native/cultmath-core/Cargo.lock.meta | 7 + native/cultmath-core/Cargo.toml.meta | 7 + native/cultmath-core/src.meta | 8 ++ native/cultmath-core/src/lib.rs.meta | 7 + package.json | 14 ++ package.json.meta | 7 + shaders.meta | 8 ++ shaders/CultMath.hlsl | 84 +++++++++++ shaders/CultMath.hlsl.meta | 7 + src.meta | 8 ++ src/CultMath.meta | 8 ++ src/CultMath/BatchMath.cs | 128 +++++++++++++++-- src/CultMath/BatchMath.cs.meta | 2 + src/CultMath/Color32.cs.meta | 2 + src/CultMath/CultMath.asmdef | 14 ++ src/CultMath/CultMath.asmdef.meta | 7 + src/CultMath/CultMath.csproj | 3 +- src/CultMath/CultMath.csproj.meta | 7 + src/CultMath/CultMathTone.cs.meta | 2 + src/CultMath/IsExternalInit.cs | 5 + src/CultMath/IsExternalInit.cs.meta | 2 + src/CultMath/Random.cs | 62 ++++++++ src/CultMath/Random.cs.meta | 2 + src/CultMath/SphericalErosion.cs | 108 ++++++++++++++ src/CultMath/Voronoi.cs | 1 + src/CultMath/Voronoi.cs.meta | 2 + src/CultMath/bool2.cs | 18 +++ src/CultMath/bool2.cs.meta | 2 + src/CultMath/double2.cs | 25 ++++ src/CultMath/double2.cs.meta | 2 + src/CultMath/double3.cs | 36 +++++ src/CultMath/double3.cs.meta | 2 + src/CultMath/float2.cs | 4 +- src/CultMath/float2.cs.meta | 2 + src/CultMath/float3.cs | 49 ++++++- src/CultMath/float3.cs.meta | 2 + src/CultMath/float4.cs | 21 ++- src/CultMath/float4.cs.meta | 2 + src/CultMath/int2.cs | 29 ++++ src/CultMath/int2.cs.meta | 2 + src/CultMath/math.cs | 52 +++++++ src/CultMath/math.cs.meta | 2 + src/CultMath/quaternion.cs | 68 +++++++++ src/CultMath/quaternion.cs.meta | 2 + src/CultMath/rect.cs | 21 +++ src/CultMath/rect.cs.meta | 2 + tests.meta | 8 ++ tests/CultMath.Tests.meta | 8 ++ .../CultMath.Tests/CultMath.Tests.csproj.meta | 7 + tests/CultMath.Tests/HlslMirrorTests.cs | 9 ++ tests/CultMath.Tests/HlslMirrorTests.cs.meta | 2 + tests/CultMath.Tests/MathTests.cs | 134 ++++++++++++++++++ tests/CultMath.Tests/MathTests.cs.meta | 2 + tests/CultMath.Tests/SphericalErosionTests.cs | 27 ++++ tools.meta | 8 ++ tools/compile-hlsl-spirv.ps1.meta | 7 + tools/compile-spirv-metal.ps1.meta | 7 + tools/get-dxc.ps1.meta | 7 + tools/get-spirv-cross.ps1.meta | 7 + 68 files changed, 1130 insertions(+), 23 deletions(-) create mode 100644 CultMath.slnx.meta create mode 100644 Directory.Build.props create mode 100644 Directory.Build.props.meta create mode 100644 README.md.meta create mode 100644 docs.meta create mode 100644 docs/design.md.meta create mode 100644 global.json.meta create mode 100644 native.meta create mode 100644 native/cultmath-core.meta create mode 100644 native/cultmath-core/Cargo.lock.meta create mode 100644 native/cultmath-core/Cargo.toml.meta create mode 100644 native/cultmath-core/src.meta create mode 100644 native/cultmath-core/src/lib.rs.meta create mode 100644 package.json create mode 100644 package.json.meta create mode 100644 shaders.meta create mode 100644 shaders/CultMath.hlsl.meta create mode 100644 src.meta create mode 100644 src/CultMath.meta create mode 100644 src/CultMath/BatchMath.cs.meta create mode 100644 src/CultMath/Color32.cs.meta create mode 100644 src/CultMath/CultMath.asmdef create mode 100644 src/CultMath/CultMath.asmdef.meta create mode 100644 src/CultMath/CultMath.csproj.meta create mode 100644 src/CultMath/CultMathTone.cs.meta create mode 100644 src/CultMath/IsExternalInit.cs create mode 100644 src/CultMath/IsExternalInit.cs.meta create mode 100644 src/CultMath/Random.cs create mode 100644 src/CultMath/Random.cs.meta create mode 100644 src/CultMath/SphericalErosion.cs create mode 100644 src/CultMath/Voronoi.cs.meta create mode 100644 src/CultMath/bool2.cs create mode 100644 src/CultMath/bool2.cs.meta create mode 100644 src/CultMath/double2.cs create mode 100644 src/CultMath/double2.cs.meta create mode 100644 src/CultMath/double3.cs create mode 100644 src/CultMath/double3.cs.meta create mode 100644 src/CultMath/float2.cs.meta create mode 100644 src/CultMath/float3.cs.meta create mode 100644 src/CultMath/float4.cs.meta create mode 100644 src/CultMath/int2.cs create mode 100644 src/CultMath/int2.cs.meta create mode 100644 src/CultMath/math.cs.meta create mode 100644 src/CultMath/quaternion.cs create mode 100644 src/CultMath/quaternion.cs.meta create mode 100644 src/CultMath/rect.cs create mode 100644 src/CultMath/rect.cs.meta create mode 100644 tests.meta create mode 100644 tests/CultMath.Tests.meta create mode 100644 tests/CultMath.Tests/CultMath.Tests.csproj.meta create mode 100644 tests/CultMath.Tests/HlslMirrorTests.cs.meta create mode 100644 tests/CultMath.Tests/MathTests.cs.meta create mode 100644 tests/CultMath.Tests/SphericalErosionTests.cs create mode 100644 tools.meta create mode 100644 tools/compile-hlsl-spirv.ps1.meta create mode 100644 tools/compile-spirv-metal.ps1.meta create mode 100644 tools/get-dxc.ps1.meta create mode 100644 tools/get-spirv-cross.ps1.meta 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: