Merge CultMath HLSL parity into CultCache store routing
Some checks failed
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (push) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (push) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (push) Has been cancelled

Brings codex/cultmath-hlsl-parity (d4c43ed) into the Cut 6 branch so Aetheria's
CultMath adoption and the CultCache Studio land on one line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-14 14:57:01 +02:00
commit 1b95dd67ee
53 changed files with 5403 additions and 216 deletions

View file

@ -16,7 +16,9 @@ shader semantics.
- `float2`, `float3`, `float4` - `float2`, `float3`, `float4`
- component-wise arithmetic - component-wise arithmetic
- scalar-to-vector splats - scalar-to-vector splats
- swizzles for common lanes - every HLSL swizzle (`xyzw` and `rgba`, repeats readable, non-repeating
masks writable) and every HLSL mixed constructor, generated by
`tools/generate-swizzles.ps1`
- conversions to and from `System.Numerics` - conversions to and from `System.Numerics`
- `Random`, a compact deterministic XORShift stream with scalar and vector draws - `Random`, a compact deterministic XORShift stream with scalar and vector draws
- `BatchMath` SoA kernels over `Span<float>` / `ReadOnlySpan<float>` using - `BatchMath` SoA kernels over `Span<float>` / `ReadOnlySpan<float>` using

View file

@ -1,9 +1,9 @@
# Third-Party Notices # Third-Party Notices
## 2D Simplex Noise ## 2D and 3D Simplex Noise
`math.simplex_noise` and `cultmath_simplex_noise` port the textureless 2D `math.snoise` and `cultmath_snoise` port the textureless 2D and 3D
simplex-noise function by Ian McEwan and Ashima Arts, as distributed by Stefan simplex-noise functions by Ian McEwan and Ashima Arts, as distributed by Stefan
Gustavson's `webgl-noise` project and Unity Mathematics. Gustavson's `webgl-noise` project and Unity Mathematics.
- Copyright 2011 Ashima Arts. - Copyright 2011 Ashima Arts.

View file

@ -3,22 +3,136 @@
CultMath exists to make renderer math readable in every place it needs to live: CultMath exists to make renderer math readable in every place it needs to live:
Rust hot kernels, C# host code, and HLSL shader code. Rust hot kernels, C# host code, and HLSL shader code.
## Parity Target: HLSL
HLSL is the semantic authority for every type and intrinsic HLSL defines. The
goal is source compatibility: HLSL-shaped math compiles as C# under
`using static CultMath.math;`. C# forces float literal suffixes (`0.5f`); the
rest of the spelling should survive the move.
That makes these HLSL rules, not options:
- Every swizzle exists. `float2/3/4`, `int2/3/4`, and `bool2/3/4` expose every
read swizzle of length 2 to 4, repeats included (`v.wwxy`), in both `xyzw` and
`rgba` naming (never mixed), plus single-component `r/g/b/a`. `double2/3` get
lengths 2 and 3 only, because there is no `double4`. Components and swizzles are
writable: `v.xz = f2;`, `c.rgb *= 0.5f;`, and `v[1] = s;` work. Setters exist
only for non-repeating masks, because HLSL rejects `v.xx = ...`.
- Every HLSL mixed constructor exists, as a struct constructor and as a `math`
function: `float4(float2, float2)`, `float4(float, float2, float)`,
`float3(float, float2)`, the copy form `float3(float3)`, and the rest, for the
float, int, and bool families (double gets constructors only; a `math.double3`
function would hide the `double3` type name).
- Swizzles and mixed constructors are generated. `tools/generate-swizzles.ps1`
writes the checked-in `src/CultMath/Swizzles.g.cs` (partial structs plus a
partial `math`). Edit the generator, rerun it, and commit both; never edit the
output by hand.
- Comparison operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) are component-wise and
return `bool2`/`bool3`/`bool4`. Reduce them with `any` and `all`.
`Equals`/`GetHashCode` stay value-based so vectors still work as dictionary
keys.
- `select(condition, whenTrue, whenFalse)` uses HLSL's argument order.
- Matrices are row-major: constructors take rows, `m[i]` returns a reference to
row i (so `m[1][2] = s;` writes one element), `_mRC` is row R column C, and
products go through `mul` (`mul(m, v)` treats `v` as a column, `mul(v, m)` as a
row). `*` on matrices would be component-wise in HLSL, so CultMath does not
define it.
- `m[r][c] = v` writes through locals, array elements, fields of classes, and
ref locals. Through a readonly field, an `in` parameter, a property
(auto-properties included), or a static readonly value such as
`float3x3.identity`, C# takes a defensive copy: the write compiles and is
silently lost. Copy the matrix to a local, write to it, and assign it back.
- Scalar/vector arithmetic and comparisons have explicit overloads on both
sides rather than leaning on implicit scalar splat.
- Intrinsics return HLSL's types and edge cases: `sign` returns `int`/`intN`
(NaN gives 0); `step(y, x)` is `x < y ? 0 : 1` (NaN gives 1), which is how dxc
lowers it, in C# and in the HLSL mirror; float and double `min` and `max`, and
float `clamp` and `saturate`, follow the DXIL `FMin`/`FMax`/`Saturate` operations dxc emits for
them: `min(a, b)` is `a < b ? a : b`, `max(a, b)` is `a >= b ? a : b`, a NaN
operand returns the other (so `saturate(NaN)` is 0), `clamp(x, a, b)` is
`min(max(x, a), b)`, and `saturate(x)` is `min(1, max(0, x))`, which maps -0
to +0; `any`/`all` accept numeric vectors (a component is true when it is
not 0); `min`, `max`, and `abs` on int vectors return int vectors.
dxc marks float arithmetic and compares `fast` (no NaNs) unless a value is
`precise`, so drivers may optimize NaN handling away and NaN results on real
GPUs are not guaranteed. CultMath defines the CPU result by dxc's lowering and
the DXIL operation spec (DirectXShaderCompiler `docs/DXIL.rst`).
NaN does not reliably propagate through CultMath, just as it does not on the
GPU. Because `min`, `max`, and `saturate` return the non-NaN operand, some
functions turn NaN or infinity into numbers: `smoothstep(a, a, a)` is 0;
infinite inputs to `smoothstep` and the intercept helpers give numbers; the
bezier, `smoothstep01`, and `smootherstep` functions return finite values for
NaN input; and `normalize((NaN, 2.41))` is `(NaN, 2.41e20)`, because the NaN
length is clamped to the epsilon and the other component is divided by it.
Callers that must detect bad data should check `isnan` before calling these
functions.
Deferred until a consumer needs them: `float4x4`, `transpose`, `determinant`,
and HLSL's one-based `_11`..`_33` element names.
### HLSL Target: Source Transformations
`HlslSourceCompatibilityTests` compiles the real `shaders/CultMath.hlsl` with
Roslyn against CultMath. These transformations are the complete list; if a body
needs anything else, the test fails:
1. Include guards (`#ifndef`/`#define`/`#endif`) are dropped.
2. Functions taking `Texture2D`/`SamplerState` are dropped; GPU resources have
no CultMath analog.
3. File-scope `static const` becomes `const`.
4. Floating literals gain `f` (`0.5` becomes `0.5f`).
5. The file body is wrapped in a class, since C# has no free functions; HLSL
functions become private instance methods.
HLSL constructs that stay outside the target and must not appear in the mirror:
`const` locals of vector type (C# `const` is primitives only; write a plain
local) and writes through a swizzle component (`v.xy.x = s`, which C# rejects as
modifying a temporary).
The HLSL side is checked too: `tools/get-dxc.ps1` fetches Microsoft's
DirectXShaderCompiler, and
`dxc -T lib_6_3 -HV 2021 shaders/CultMath.hlsl` must compile cleanly.
The mirror test compares bit patterns (any NaN equals any NaN; -0 differs from
0). It proves the text of `CultMath.hlsl` computes the same float32 results as
C# `math` on the CPU, not that a GPU agrees: driver `sin` precision alone makes
`cultmath_hash` and value noise differ bit for bit on hardware. The mirror is
compiled under `using static CultMath.math;`, so its intrinsics are C# `math`
itself: the test proves the composition in the text, not the intrinsic rules,
which only `HlslSemanticsTests` pins.
Where HLSL is silent, CultMath keeps its own decisions and does not defer to
Unity.Mathematics: `normalize` is safe (divides by `max(length, 1e-20)`),
`hash` returns float, and `Random` is CultMath's own xorshift32. Engine-shaped
helpers that HLSL lacks (`float2x2.Rotate`, `float3x3.Euler`, `quaternion`,
`snoise`) enter only when a consumer needs them, with their semantics stated in
code.
Known C# friction: under `using static CultMath.math;` the constructor
functions (`float3(...)`, `float3x3(...)`) hide the type names in member access,
so static members need a qualified type (`CultMath.float3x3.Euler(...)`).
## Rules ## Rules
- Keep type names HLSL-shaped: `float2`, `float3`, `float4`, and `math`. - Keep type names HLSL-shaped: `float2`, `float3`, `float4`, `float3x3`, and `math`.
- Keep Rust as the owner of native hot-path kernels and parity fixtures. - Keep Rust as the owner of native hot-path kernels and parity fixtures.
- Keep C#, shader, and scripting surfaces as runtime wrappers, not competing - Keep C#, shader, and scripting surfaces as runtime wrappers, not competing
math authorities. math authorities.
- Keep `shaders/CultMath.hlsl` as the canonical shader mirror for shared - Keep `shaders/CultMath.hlsl` as the canonical shader mirror for shared
intrinsics. Project-local shader helpers should move here once more than one CultMath functions HLSL does not already provide (HLSL intrinsics such as
organ needs them or once CPU/GPU parity matters. `mul`, `any`, and `log` need no mirror). Project-local shader helpers should
move here once more than one organ needs them or once CPU/GPU parity matters.
- Implement public shader semantics directly; do not copy Unity.Mathematics. - Implement public shader semantics directly; do not copy Unity.Mathematics.
- Prefer small immutable value types. - Prefer plain mutable value types with public component fields, as HLSL does.
- Prefer component-wise overloads over clever generic machinery until the API - Prefer component-wise overloads over clever generic machinery until the API
earns that complexity. earns that complexity.
- Keep the surface boring and exact. Noise primitives are allowed when they are - Keep the surface boring and exact. Fill what a consumer uses, guided by HLSL
source-grounded, shader-shaped, and shared by more than one project. Matrices, semantics; noise primitives, matrices, quaternions, and packing helpers need an
quaternions, and packing helpers still need an owner before they enter. owner before they enter.
- Keep the core assembly engine-free. Unity conversions live in the Unity
package's `CultMath.UnityBridge` assembly.
## Non-Goals ## Non-Goals

View file

@ -27,8 +27,10 @@ if (Test-Path -LiteralPath $outputRoot) {
Remove-Item -LiteralPath $outputRoot -Recurse -Force Remove-Item -LiteralPath $outputRoot -Recurse -Force
} }
dotnet build $projectPath -c $Configuration -f netstandard2.1 -o $buildRoot ` # The tracked DLL and pdb are committed beside their source, so neither may name a commit: Source Link
--disable-build-servers -p:UseSharedCompilation=false -p:ContinuousIntegrationBuild=true -m:1 # writes the commit SHA into the pdb, and the DLL carries that pdb's content id.
dotnet build $projectPath -c $Configuration -f netstandard2.1 -o $buildRoot --no-incremental `
--disable-build-servers -p:UseSharedCompilation=false -p:ContinuousIntegrationBuild=true -p:EnableSourceLink=false -m:1
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "CultMath Unity assembly build failed with exit code $LASTEXITCODE" throw "CultMath Unity assembly build failed with exit code $LASTEXITCODE"
} }
@ -52,6 +54,10 @@ $requiredTemplateFiles = @(
"Runtime\Plugins\CultMath.dll.meta", "Runtime\Plugins\CultMath.dll.meta",
"Runtime\Plugins\CultMath.pdb", "Runtime\Plugins\CultMath.pdb",
"Runtime\Plugins\CultMath.pdb.meta", "Runtime\Plugins\CultMath.pdb.meta",
"Runtime\UnityBridge\CultMath.UnityBridge.asmdef",
"Runtime\UnityBridge\CultMath.UnityBridge.asmdef.meta",
"Runtime\UnityBridge\UnityConversions.cs",
"Runtime\UnityBridge\UnityConversions.cs.meta",
"Shaders\CultMath.hlsl", "Shaders\CultMath.hlsl",
"Shaders\CultMath.hlsl.meta" "Shaders\CultMath.hlsl.meta"
) )
@ -62,7 +68,7 @@ foreach ($relativePath in $requiredTemplateFiles) {
} }
$unityAssetFiles = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File | Where-Object { $unityAssetFiles = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File | Where-Object {
$_.Extension -in @(".asmdef", ".dll", ".pdb", ".hlsl") $_.Extension -in @(".asmdef", ".cs", ".dll", ".pdb", ".hlsl")
}) })
$missingAssetMetas = @($unityAssetFiles | Where-Object { -not (Test-Path -LiteralPath ($_.FullName + ".meta")) }) $missingAssetMetas = @($unityAssetFiles | Where-Object { -not (Test-Path -LiteralPath ($_.FullName + ".meta")) })
$missingDirectoryMetas = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -Directory | Where-Object { $missingDirectoryMetas = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -Directory | Where-Object {
@ -78,9 +84,18 @@ $pluginMeta = Get-Content -LiteralPath ($trackedAssembly + ".meta") -Raw
if ($pluginMeta -notmatch '(?m)^\s*isExplicitlyReferenced:\s*0\s*$') { if ($pluginMeta -notmatch '(?m)^\s*isExplicitlyReferenced:\s*0\s*$') {
throw "CultMath.dll must be auto-referenced; explicit-only plugins do not reach dependent Unity asmdefs" throw "CultMath.dll must be auto-referenced; explicit-only plugins do not reach dependent Unity asmdefs"
} }
$facadeDefinitions = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File -Filter "*.asmdef") # The precompiled DLL is the only authority for CultMath's numeric types. The one
if ($facadeDefinitions.Count -ne 0) { # compiled assembly allowed beside it is the UnityEngine conversion bridge, which
throw "CultMath's precompiled DLL is the Unity assembly authority; empty asmdef facades are forbidden" # references that DLL and must not re-declare core sources.
$bridgeRoot = Join-Path $templateRoot "Runtime\UnityBridge"
$bridgeDefinition = Join-Path $bridgeRoot "CultMath.UnityBridge.asmdef"
$definitions = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File -Filter "*.asmdef")
if ($definitions.Count -ne 1 -or $definitions[0].FullName -ne $bridgeDefinition) {
throw "CultMath's Unity package may define only the Runtime\UnityBridge assembly; found: $($definitions.FullName -join ', ')"
}
$bridge = Get-Content -LiteralPath $bridgeDefinition -Raw | ConvertFrom-Json
if ($bridge.noEngineReferences -or -not $bridge.overrideReferences -or @($bridge.precompiledReferences) -notcontains "CultMath.dll") {
throw "CultMath.UnityBridge must reference UnityEngine and the precompiled CultMath.dll"
} }
$expectedFileVersion = "$($manifest.version).0" $expectedFileVersion = "$($manifest.version).0"
$trackedFileVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($trackedAssembly).FileVersion $trackedFileVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($trackedAssembly).FileVersion
@ -98,22 +113,47 @@ foreach ($pair in @(@($freshAssembly, $trackedAssembly), @($freshSymbols, $track
} }
$trackedSourceFiles = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File -Filter "*.cs") $trackedSourceFiles = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File -Filter "*.cs")
if ($trackedSourceFiles.Count -ne 0) { $straySources = @($trackedSourceFiles | Where-Object { $_.DirectoryName -ne $bridgeRoot })
throw "CultMath Unity package must not compile repository C# sources: $($trackedSourceFiles.FullName -join ', ')" if ($trackedSourceFiles.Count -eq 0 -or $straySources.Count -ne 0) {
throw "CultMath Unity package may compile only bridge sources under Runtime\UnityBridge: $($straySources.FullName -join ', ')"
}
# Check what the bridge declares, not what its files are called: it may declare types only in
# CultMath.UnityBridge and never a type name the core CultMath sources declare.
$declarationPattern = '\b(?:struct|class|enum|interface|record|delegate\s+[\w<>.,\s]+?)\s+(\w+)'
function Get-CodeWithoutComments([string]$path) {
$text = Get-Content -LiteralPath $path -Raw
return [regex]::Replace($text, '(?s)/\*.*?\*/|//[^\r\n]*', '')
}
$coreTypeNames = @(Get-ChildItem -LiteralPath (Join-Path $repoRoot "src\CultMath") -File -Filter "*.cs" | ForEach-Object {
[regex]::Matches((Get-CodeWithoutComments $_.FullName), $declarationPattern) | ForEach-Object { $_.Groups[1].Value }
} | Sort-Object -Unique)
if ($coreTypeNames -notcontains "float3" -or $coreTypeNames -notcontains "math") {
throw "Could not read CultMath core type declarations; the bridge check would be vacuous"
}
foreach ($source in $trackedSourceFiles) {
$code = Get-CodeWithoutComments $source.FullName
$redeclared = @([regex]::Matches($code, $declarationPattern) | ForEach-Object { $_.Groups[1].Value } | Where-Object { $coreTypeNames -contains $_ })
$foreignNamespaces = @([regex]::Matches($code, '\bnamespace\s+([\w.]+)') | ForEach-Object { $_.Groups[1].Value } | Where-Object { $_ -ne "CultMath.UnityBridge" })
if ($redeclared.Count -ne 0 -or $foreignNamespaces.Count -ne 0) {
throw "CultMath.UnityBridge source $($source.Name) re-declares core types [$($redeclared -join ', ')] or uses namespaces [$($foreignNamespaces -join ', ')]; the precompiled DLL owns CultMath's types"
}
} }
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
Copy-Item -Path (Join-Path $templateRoot "*") -Destination $outputRoot -Recurse -Force Copy-Item -Path (Join-Path $templateRoot "*") -Destination $outputRoot -Recurse -Force
$stagedAssemblies = @(Get-ChildItem -LiteralPath (Join-Path $outputRoot "Runtime\Plugins") -Filter "*.dll") $stagedAssemblies = @(Get-ChildItem -LiteralPath (Join-Path $outputRoot "Runtime\Plugins") -Filter "*.dll")
$stagedBridgeRoot = Join-Path $outputRoot "Runtime\UnityBridge"
$stagedSources = @(Get-ChildItem -LiteralPath $outputRoot -Recurse -File -Filter "*.cs") $stagedSources = @(Get-ChildItem -LiteralPath $outputRoot -Recurse -File -Filter "*.cs")
$stagedFacades = @(Get-ChildItem -LiteralPath $outputRoot -Recurse -File -Filter "*.asmdef") $stagedDefinitions = @(Get-ChildItem -LiteralPath $outputRoot -Recurse -File -Filter "*.asmdef")
if ($stagedAssemblies.Count -ne 1 -or $stagedAssemblies[0].Name -ne "CultMath.dll" -or $stagedSources.Count -ne 0 -or $stagedFacades.Count -ne 0) { $stagedStraySources = @($stagedSources | Where-Object { $_.DirectoryName -ne $stagedBridgeRoot })
throw "CultMath package must contain one owned auto-referenced DLL, no C# source files, and no asmdef facade" if ($stagedAssemblies.Count -ne 1 -or $stagedAssemblies[0].Name -ne "CultMath.dll" -or $stagedStraySources.Count -ne 0 -or $stagedDefinitions.Count -ne 1) {
throw "CultMath package must contain one owned auto-referenced DLL and only the UnityBridge assembly as source"
} }
Write-Host "CultMath Unity package: $outputRoot" Write-Host "CultMath Unity package: $outputRoot"
Write-Host "Package: $($manifest.name)@$($manifest.version)" Write-Host "Package: $($manifest.name)@$($manifest.version)"
Write-Host "Managed assemblies: $($stagedAssemblies.Count)" Write-Host "Managed assemblies: $($stagedAssemblies.Count)"
Write-Host "C# source files: $($stagedSources.Count)" Write-Host "Bridge C# source files: $($stagedSources.Count)"
Write-Host "Asmdef facades: $($stagedFacades.Count)" Write-Host "Asmdef definitions: $($stagedDefinitions.Count)"

View file

@ -26,10 +26,11 @@ float2 cultmath_clamp(float2 value, float2 minimum, float2 maximum) { return min
float3 cultmath_clamp(float3 value, float3 minimum, float3 maximum) { return min(max(value, minimum), maximum); } float3 cultmath_clamp(float3 value, float3 minimum, float3 maximum) { return min(max(value, minimum), maximum); }
float4 cultmath_clamp(float4 value, float4 minimum, float4 maximum) { return min(max(value, minimum), maximum); } float4 cultmath_clamp(float4 value, float4 minimum, float4 maximum) { return min(max(value, minimum), maximum); }
float cultmath_saturate(float value) { return cultmath_clamp(value, 0.0, 1.0); } // DXIL Saturate is FMin(1, FMax(0, x)); dxc lowers min/max to the same FMin/FMax, NaN rules included.
float2 cultmath_saturate(float2 value) { return cultmath_clamp(value, float2(0.0, 0.0), float2(1.0, 1.0)); } float cultmath_saturate(float value) { return min(1.0, max(0.0, value)); }
float3 cultmath_saturate(float3 value) { return cultmath_clamp(value, float3(0.0, 0.0, 0.0), float3(1.0, 1.0, 1.0)); } float2 cultmath_saturate(float2 value) { return min(float2(1.0, 1.0), max(float2(0.0, 0.0), value)); }
float4 cultmath_saturate(float4 value) { return cultmath_clamp(value, float4(0.0, 0.0, 0.0, 0.0), float4(1.0, 1.0, 1.0, 1.0)); } float3 cultmath_saturate(float3 value) { return min(float3(1.0, 1.0, 1.0), max(float3(0.0, 0.0, 0.0), value)); }
float4 cultmath_saturate(float4 value) { return min(float4(1.0, 1.0, 1.0, 1.0), max(float4(0.0, 0.0, 0.0, 0.0), value)); }
float cultmath_lerp(float start, float end, float amount) { return start + (end - start) * amount; } float cultmath_lerp(float start, float end, float amount) { return start + (end - start) * amount; }
float2 cultmath_lerp(float2 start, float2 end, float2 amount) { return start + (end - start) * amount; } float2 cultmath_lerp(float2 start, float2 end, float2 amount) { return start + (end - start) * amount; }
@ -73,13 +74,66 @@ float cultmath_smootherstep(float value)
return value * value * value * (value * (value * 6.0 - 15.0) + 10.0); return value * value * value * (value * (value * 6.0 - 15.0) + 10.0);
} }
float2 cultmath_simplex_mod289(float2 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } float2 cultmath_snoise_mod289(float2 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
float3 cultmath_simplex_mod289(float3 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } float3 cultmath_snoise_mod289(float3 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
float3 cultmath_simplex_permute(float3 value) { return cultmath_simplex_mod289(((value * 34.0) + 1.0) * value); } float4 cultmath_snoise_mod289(float4 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
float3 cultmath_snoise_permute(float3 value) { return cultmath_snoise_mod289(((value * 34.0) + 1.0) * value); }
float4 cultmath_snoise_permute(float4 value) { return cultmath_snoise_mod289(((value * 34.0) + 1.0) * value); }
float cultmath_simplex_noise(float2 value) // Ashima Arts / Ian McEwan 3D simplex noise (MIT), same float32 evaluation
// order as the C# math.snoise(float3) mirror.
float cultmath_snoise(float3 value)
{ {
const float4 c = float4( float2 c = float2(1.0 / 6.0, 1.0 / 3.0);
float3 i = floor(value + dot(value, c.yyy));
float3 x0 = value - i + dot(i, c.xxx);
float3 g = step(x0.yzx, x0.xyz);
float3 l = 1.0 - g;
float3 i1 = min(g.xyz, l.zxy);
float3 i2 = max(g.xyz, l.zxy);
float3 x1 = x0 - i1 + c.xxx;
float3 x2 = x0 - i2 + c.yyy;
float3 x3 = x0 - 0.5;
i = cultmath_snoise_mod289(i);
float4 p = cultmath_snoise_permute(cultmath_snoise_permute(cultmath_snoise_permute(
i.z + float4(0.0, i1.z, i2.z, 1.0)) + i.y + float4(0.0, i1.y, i2.y, 1.0)) + i.x + float4(0.0, i1.x, i2.x, 1.0));
const float n_ = 0.142857142857;
float3 ns = float3(2.0 * n_, 0.5 * n_ - 1.0, n_);
float4 j = p - 49.0 * floor(p * ns.z * ns.z);
float4 x_ = floor(j * ns.z);
float4 y_ = floor(j - 7.0 * x_);
float4 x = x_ * ns.x + ns.yyyy;
float4 y = y_ * ns.x + ns.yyyy;
float4 h = 1.0 - abs(x) - abs(y);
float4 b0 = float4(x.xy, y.xy);
float4 b1 = float4(x.zw, y.zw);
float4 s0 = floor(b0) * 2.0 + 1.0;
float4 s1 = floor(b1) * 2.0 + 1.0;
float4 sh = -step(h, 0.0);
float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
float4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
float3 p0 = float3(a0.xy, h.x);
float3 p1 = float3(a0.zw, h.y);
float3 p2 = float3(a1.xy, h.z);
float3 p3 = float3(a1.zw, h.w);
float4 norm = 1.79284291400159 - 0.85373472095314 * float4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
float4 m = max(0.6 - float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 42.0 * dot(m * m, float4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
float cultmath_snoise(float2 value)
{
float4 c = float4(
0.211324865405187, 0.211324865405187,
0.366025403784439, 0.366025403784439,
-0.577350269189626, -0.577350269189626,
@ -89,9 +143,9 @@ float cultmath_simplex_noise(float2 value)
float2 i1 = x0.x > x0.y ? float2(1.0, 0.0) : float2(0.0, 1.0); float2 i1 = x0.x > x0.y ? float2(1.0, 0.0) : float2(0.0, 1.0);
float4 x12 = float4(x0.x + c.x - i1.x, x0.y + c.x - i1.y, x0.x + c.z, x0.y + c.z); float4 x12 = float4(x0.x + c.x - i1.x, x0.y + c.x - i1.y, x0.x + c.z, x0.y + c.z);
i = cultmath_simplex_mod289(i); i = cultmath_snoise_mod289(i);
float3 p = cultmath_simplex_permute( float3 p = cultmath_snoise_permute(
cultmath_simplex_permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0)); cultmath_snoise_permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0));
float3 m = max(0.5 - float3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); float3 m = max(0.5 - float3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
m *= m; m *= m;
m *= m; m *= m;

View file

@ -8,12 +8,14 @@
<RootNamespace>CultMath</RootNamespace> <RootNamespace>CultMath</RootNamespace>
<AssemblyName>CultMath</AssemblyName> <AssemblyName>CultMath</AssemblyName>
<PackageId>GameCult.Math</PackageId> <PackageId>GameCult.Math</PackageId>
<Version>0.1.2</Version> <Version>0.2.0</Version>
<Description>Clean-room C# math shaped for HLSL-style renderer code.</Description> <Description>Clean-room C# math shaped for HLSL-style renderer code.</Description>
<Authors>GameCult</Authors> <Authors>GameCult</Authors>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<NoWarn>$(NoWarn);CS8981</NoWarn> <NoWarn>$(NoWarn);CS8981</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- The Unity DLL is committed beside its source, so its bytes must not name a commit. -->
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -58,5 +58,9 @@ public struct Random
return new float2(math.cos(angle), math.sin(angle)); return new float2(math.cos(angle), math.sin(angle));
} }
public float3 NextFloat3(float3 min, float3 max) => min + new float3(NextFloat(), NextFloat(), NextFloat()) * (max - min); public float3 NextFloat3() => new(NextFloat(), NextFloat(), NextFloat());
public float3 NextFloat3(float3 max) => NextFloat3() * max;
public float3 NextFloat3(float3 min, float3 max) => min + NextFloat3() * (max - min);
} }

File diff suppressed because it is too large Load diff

View file

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

View file

@ -0,0 +1,10 @@
#if !NET7_0_OR_GREATER
namespace System.Diagnostics.CodeAnalysis;
// Compiler-recognized polyfill so netstandard2.1 builds can return refs into a struct's
// own fields (the matrix row indexer). net7.0+ supplies the real attribute.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class UnscopedRefAttribute : Attribute
{
}
#endif

View file

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

View file

@ -2,17 +2,50 @@ using System;
namespace CultMath; namespace CultMath;
public record struct bool2(bool x, bool y) public partial struct bool2 : IEquatable<bool2>
{ {
public static readonly bool2 @false = new(false, false); public static readonly bool2 @false = new(false, false);
public static readonly bool2 @true = new(true, true); public static readonly bool2 @true = new(true, true);
public bool this[int index] => index switch public bool x;
public bool y;
public bool2(bool x, bool y)
{ {
0 => x, this.x = x;
1 => y, this.y = y;
_ => throw new ArgumentOutOfRangeException(nameof(index)), }
};
public bool this[int index]
{
readonly get => index switch
{
0 => x,
1 => y,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator bool2(bool value) => new(value, value); public static implicit operator bool2(bool value) => new(value, value);
public static bool2 operator !(bool2 value) => new(!value.x, !value.y);
public static bool2 operator &(bool2 left, bool2 right) => new(left.x & right.x, left.y & right.y);
public static bool2 operator |(bool2 left, bool2 right) => new(left.x | right.x, left.y | right.y);
public static bool2 operator ^(bool2 left, bool2 right) => new(left.x ^ right.x, left.y ^ right.y);
public static bool2 operator ==(bool2 left, bool2 right) => new(left.x == right.x, left.y == right.y);
public static bool2 operator !=(bool2 left, bool2 right) => new(left.x != right.x, left.y != right.y);
public readonly bool Equals(bool2 other) => x == other.x && y == other.y;
public override readonly bool Equals(object? obj) => obj is bool2 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y);
public override readonly string ToString() => $"bool2({x}, {y})";
} }

View file

@ -0,0 +1,55 @@
using System;
namespace CultMath;
public partial struct bool3 : IEquatable<bool3>
{
public static readonly bool3 @false = new(false, false, false);
public static readonly bool3 @true = new(true, true, true);
public bool x;
public bool y;
public bool z;
public bool3(bool x, bool y, bool z)
{
this.x = x;
this.y = y;
this.z = z;
}
public bool this[int index]
{
readonly get => index switch
{
0 => x,
1 => y,
2 => z,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
case 2: z = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator bool3(bool value) => new(value, value, value);
public static bool3 operator !(bool3 value) => new(!value.x, !value.y, !value.z);
public static bool3 operator &(bool3 left, bool3 right) => new(left.x & right.x, left.y & right.y, left.z & right.z);
public static bool3 operator |(bool3 left, bool3 right) => new(left.x | right.x, left.y | right.y, left.z | right.z);
public static bool3 operator ^(bool3 left, bool3 right) => new(left.x ^ right.x, left.y ^ right.y, left.z ^ right.z);
public static bool3 operator ==(bool3 left, bool3 right) => new(left.x == right.x, left.y == right.y, left.z == right.z);
public static bool3 operator !=(bool3 left, bool3 right) => new(left.x != right.x, left.y != right.y, left.z != right.z);
public readonly bool Equals(bool3 other) => x == other.x && y == other.y && z == other.z;
public override readonly bool Equals(object? obj) => obj is bool3 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y, z);
public override readonly string ToString() => $"bool3({x}, {y}, {z})";
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 323ec670fc374c8ba4d87b4bf4c91f34

View file

@ -0,0 +1,59 @@
using System;
namespace CultMath;
public partial struct bool4 : IEquatable<bool4>
{
public static readonly bool4 @false = new(false, false, false, false);
public static readonly bool4 @true = new(true, true, true, true);
public bool x;
public bool y;
public bool z;
public bool w;
public bool4(bool x, bool y, bool z, bool w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
public bool this[int index]
{
readonly get => index switch
{
0 => x,
1 => y,
2 => z,
3 => w,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
case 2: z = value; break;
case 3: w = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator bool4(bool value) => new(value, value, value, value);
public static bool4 operator !(bool4 value) => new(!value.x, !value.y, !value.z, !value.w);
public static bool4 operator &(bool4 left, bool4 right) => new(left.x & right.x, left.y & right.y, left.z & right.z, left.w & right.w);
public static bool4 operator |(bool4 left, bool4 right) => new(left.x | right.x, left.y | right.y, left.z | right.z, left.w | right.w);
public static bool4 operator ^(bool4 left, bool4 right) => new(left.x ^ right.x, left.y ^ right.y, left.z ^ right.z, left.w ^ right.w);
public static bool4 operator ==(bool4 left, bool4 right) => new(left.x == right.x, left.y == right.y, left.z == right.z, left.w == right.w);
public static bool4 operator !=(bool4 left, bool4 right) => new(left.x != right.x, left.y != right.y, left.z != right.z, left.w != right.w);
public readonly bool Equals(bool4 other) => x == other.x && y == other.y && z == other.z && w == other.w;
public override readonly bool Equals(object? obj) => obj is bool4 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y, z, w);
public override readonly string ToString() => $"bool4({x}, {y}, {z}, {w})";
}

View file

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

View file

@ -2,17 +2,38 @@ using System;
namespace CultMath; namespace CultMath;
public record struct double2(double x, double y) public partial struct double2 : IEquatable<double2>
{ {
public static readonly double2 zero = new(0.0, 0.0); public static readonly double2 zero = new(0.0, 0.0);
public static readonly double2 one = new(1.0, 1.0); public static readonly double2 one = new(1.0, 1.0);
public double this[int index] => index switch public double x;
public double y;
public double2(double x, double y)
{ {
0 => x, this.x = x;
1 => y, this.y = y;
_ => throw new ArgumentOutOfRangeException(nameof(index)), }
};
public double this[int index]
{
readonly get => index switch
{
0 => x,
1 => y,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator double2(double value) => new(value, value); public static implicit operator double2(double value) => new(value, value);
@ -22,4 +43,16 @@ public record struct double2(double x, double 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);
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 bool2 operator <(double2 left, double2 right) => new(left.x < right.x, left.y < right.y);
public static bool2 operator >(double2 left, double2 right) => new(left.x > right.x, left.y > right.y);
public static bool2 operator <=(double2 left, double2 right) => new(left.x <= right.x, left.y <= right.y);
public static bool2 operator >=(double2 left, double2 right) => new(left.x >= right.x, left.y >= right.y);
public static bool2 operator ==(double2 left, double2 right) => new(left.x == right.x, left.y == right.y);
public static bool2 operator !=(double2 left, double2 right) => new(left.x != right.x, left.y != right.y);
public readonly bool Equals(double2 other) => x.Equals(other.x) && y.Equals(other.y);
public override readonly bool Equals(object? obj) => obj is double2 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y);
public override readonly string ToString() => FormattableString.Invariant($"double2({x:R}, {y:R})");
} }

View file

@ -2,28 +2,42 @@ using System;
namespace CultMath; namespace CultMath;
public record struct double3(double x, double y, double z) public partial struct double3 : IEquatable<double3>
{ {
public static readonly double3 zero = new(0.0, 0.0, 0.0); 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 static readonly double3 one = new(1.0, 1.0, 1.0);
public double2 xy public double x;
public double y;
public double z;
public double3(double x, double y, double z)
{ {
get => new(x, y); this.x = x;
set this.y = y;
{ this.z = z;
x = value.x;
y = value.y;
}
} }
public double this[int index] => index switch public double this[int index]
{ {
0 => x, readonly get => index switch
1 => y, {
2 => z, 0 => x,
_ => throw new ArgumentOutOfRangeException(nameof(index)), 1 => y,
}; 2 => z,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
case 2: z = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator double3(double value) => new(value, value, value); public static implicit operator double3(double value) => new(value, value, value);
@ -33,4 +47,16 @@ public record struct double3(double x, double y, double 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);
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 bool3 operator <(double3 left, double3 right) => new(left.x < right.x, left.y < right.y, left.z < right.z);
public static bool3 operator >(double3 left, double3 right) => new(left.x > right.x, left.y > right.y, left.z > right.z);
public static bool3 operator <=(double3 left, double3 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z);
public static bool3 operator >=(double3 left, double3 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z);
public static bool3 operator ==(double3 left, double3 right) => new(left.x == right.x, left.y == right.y, left.z == right.z);
public static bool3 operator !=(double3 left, double3 right) => new(left.x != right.x, left.y != right.y, left.z != right.z);
public readonly bool Equals(double3 other) => x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z);
public override readonly bool Equals(object? obj) => obj is double3 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y, z);
public override readonly string ToString() => FormattableString.Invariant($"double3({x:R}, {y:R}, {z:R})");
} }

View file

@ -2,17 +2,38 @@ using System;
namespace CultMath; namespace CultMath;
public record struct float2(float x, float y) public partial struct float2 : IEquatable<float2>
{ {
public static readonly float2 zero = new(0.0f, 0.0f); public static readonly float2 zero = new(0.0f, 0.0f);
public static readonly float2 one = new(1.0f, 1.0f); public static readonly float2 one = new(1.0f, 1.0f);
public float this[int index] => index switch public float x;
public float y;
public float2(float x, float y)
{ {
0 => x, this.x = x;
1 => y, this.y = y;
_ => throw new ArgumentOutOfRangeException(nameof(index)), }
};
public float this[int index]
{
readonly get => index switch
{
0 => x,
1 => y,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator float2(float value) => new(value, value); public static implicit operator float2(float value) => new(value, value);
public static explicit operator System.Numerics.Vector2(float2 value) => new(value.x, value.y); public static explicit operator System.Numerics.Vector2(float2 value) => new(value.x, value.y);
@ -20,9 +41,44 @@ public record struct float2(float x, float y)
public static float2 operator +(float2 value) => value; public static float2 operator +(float2 value) => value;
public static float2 operator -(float2 value) => new(-value.x, -value.y); public static float2 operator -(float2 value) => new(-value.x, -value.y);
public static float2 operator +(float2 left, float2 right) => new(left.x + right.x, left.y + right.y); public static float2 operator +(float2 left, float2 right) => new(left.x + right.x, left.y + right.y);
public static float2 operator +(float2 left, float right) => new(left.x + right, left.y + right);
public static float2 operator +(float left, float2 right) => new(left + right.x, left + right.y);
public static float2 operator -(float2 left, float2 right) => new(left.x - right.x, left.y - right.y); public static float2 operator -(float2 left, float2 right) => new(left.x - right.x, left.y - right.y);
public static float2 operator -(float2 left, float right) => new(left.x - right, left.y - right);
public static float2 operator -(float left, float2 right) => new(left - right.x, left - right.y);
public static float2 operator *(float2 left, float2 right) => new(left.x * right.x, left.y * right.y); public static float2 operator *(float2 left, float2 right) => new(left.x * right.x, left.y * right.y);
public static float2 operator *(float2 left, float right) => new(left.x * right, left.y * right);
public static float2 operator *(float left, float2 right) => new(left * right.x, left * right.y);
public static float2 operator /(float2 left, float2 right) => new(left.x / right.x, left.y / right.y); public static float2 operator /(float2 left, float2 right) => new(left.x / right.x, left.y / right.y);
public static float2 operator /(float2 left, float right) => new(left.x / right, left.y / right);
public static float2 operator /(float left, float2 right) => new(left / right.x, left / right.y);
public static float2 operator %(float2 left, float2 right) => new(left.x % right.x, left.y % right.y); public static float2 operator %(float2 left, float2 right) => new(left.x % right.x, left.y % right.y);
public static float2 operator %(float2 left, float right) => new(left.x % right, left.y % right);
public static float2 operator %(float left, float2 right) => new(left % right.x, left % right.y);
public static bool2 operator <(float2 left, float2 right) => new(left.x < right.x, left.y < right.y);
public static bool2 operator <(float2 left, float right) => new(left.x < right, left.y < right);
public static bool2 operator <(float left, float2 right) => new(left < right.x, left < right.y);
public static bool2 operator >(float2 left, float2 right) => new(left.x > right.x, left.y > right.y);
public static bool2 operator >(float2 left, float right) => new(left.x > right, left.y > right);
public static bool2 operator >(float left, float2 right) => new(left > right.x, left > right.y);
public static bool2 operator <=(float2 left, float2 right) => new(left.x <= right.x, left.y <= right.y);
public static bool2 operator <=(float2 left, float right) => new(left.x <= right, left.y <= right);
public static bool2 operator <=(float left, float2 right) => new(left <= right.x, left <= right.y);
public static bool2 operator >=(float2 left, float2 right) => new(left.x >= right.x, left.y >= right.y);
public static bool2 operator >=(float2 left, float right) => new(left.x >= right, left.y >= right);
public static bool2 operator >=(float left, float2 right) => new(left >= right.x, left >= right.y);
public static bool2 operator ==(float2 left, float2 right) => new(left.x == right.x, left.y == right.y);
public static bool2 operator ==(float2 left, float right) => new(left.x == right, left.y == right);
public static bool2 operator ==(float left, float2 right) => new(left == right.x, left == right.y);
public static bool2 operator !=(float2 left, float2 right) => new(left.x != right.x, left.y != right.y);
public static bool2 operator !=(float2 left, float right) => new(left.x != right, left.y != right);
public static bool2 operator !=(float left, float2 right) => new(left != right.x, left != right.y);
public readonly bool Equals(float2 other) => x.Equals(other.x) && y.Equals(other.y);
public override readonly bool Equals(object? obj) => obj is float2 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y);
public override readonly string ToString() => FormattableString.Invariant($"float2({x:R}, {y:R})");
} }

View file

@ -0,0 +1,67 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace CultMath;
/// <summary>
/// HLSL float2x2: row-major. Constructors take rows, <c>m[i]</c> is a writable reference to
/// row i (so <c>m[1][0] = s</c> works), and <c>_mRC</c> names row R, column C exactly as HLSL
/// does. Use <see cref="math.mul(float2x2, float2)"/> for products; there is no <c>*</c>
/// matrix product because HLSL's <c>*</c> is component-wise.
/// </summary>
public struct float2x2 : IEquatable<float2x2>
{
public static readonly float2x2 identity = new(1.0f, 0.0f, 0.0f, 1.0f);
public static readonly float2x2 zero = new(0.0f, 0.0f, 0.0f, 0.0f);
private float2 _row0;
private float2 _row1;
public float2x2(float m00, float m01, float m10, float m11)
{
_row0 = new float2(m00, m01);
_row1 = new float2(m10, m11);
}
public float2x2(float2 row0, float2 row1)
{
_row0 = row0;
_row1 = row1;
}
public float _m00 { readonly get => _row0.x; set => _row0.x = value; }
public float _m01 { readonly get => _row0.y; set => _row0.y = value; }
public float _m10 { readonly get => _row1.x; set => _row1.x = value; }
public float _m11 { readonly get => _row1.y; set => _row1.y = value; }
/// <remarks>Writes through a readonly field, <c>in</c> parameter, property, or <c>identity</c> hit a defensive copy and are lost; write to a local and assign it back.</remarks>
[UnscopedRef]
public ref float2 this[int row]
{
get
{
switch (row)
{
case 0: return ref _row0;
case 1: return ref _row1;
default: throw new ArgumentOutOfRangeException(nameof(row));
}
}
}
/// <summary>
/// Counter-clockwise rotation for column vectors: <c>mul(Rotate(a), v)</c> equals
/// <c>math.rotate(v, a)</c>; <c>mul(v, Rotate(a))</c> rotates the other way.
/// </summary>
public static float2x2 Rotate(float radians)
{
var s = MathF.Sin(radians);
var c = MathF.Cos(radians);
return new float2x2(c, -s, s, c);
}
public readonly bool Equals(float2x2 other) => _row0.Equals(other._row0) && _row1.Equals(other._row1);
public override readonly bool Equals(object? obj) => obj is float2x2 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(_row0, _row1);
public override readonly string ToString() => FormattableString.Invariant($"float2x2({_row0.x:R}, {_row0.y:R}, {_row1.x:R}, {_row1.y:R})");
}

View file

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

View file

@ -2,78 +2,87 @@ using System;
namespace CultMath; namespace CultMath;
public record struct float3(float x, float y, float z) public partial struct float3 : IEquatable<float3>
{ {
public static readonly float3 zero = new(0.0f, 0.0f, 0.0f); public static readonly float3 zero = new(0.0f, 0.0f, 0.0f);
public static readonly float3 one = new(1.0f, 1.0f, 1.0f); public static readonly float3 one = new(1.0f, 1.0f, 1.0f);
public float3(float2 xy, float z) public float x;
: this(xy.x, xy.y, z) public float y;
public float z;
public float3(float x, float y, float z)
{ {
this.x = x;
this.y = y;
this.z = z;
} }
public float2 xy public float this[int index]
{ {
get => new(x, y); readonly get => index switch
{
0 => x,
1 => y,
2 => z,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set set
{ {
x = value.x; switch (index)
y = value.y; {
case 0: x = value; break;
case 1: y = value; break;
case 2: z = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
} }
} }
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
{
0 => x,
1 => y,
2 => z,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public static implicit operator float3(float value) => new(value, value, value); public static implicit operator float3(float value) => new(value, value, value);
public static explicit operator System.Numerics.Vector3(float3 value) => new(value.x, value.y, value.z); public static explicit operator System.Numerics.Vector3(float3 value) => new(value.x, value.y, value.z);
public static explicit operator float3(System.Numerics.Vector3 value) => new(value.X, value.Y, value.Z); public static explicit operator float3(System.Numerics.Vector3 value) => new(value.X, value.Y, value.Z);
public override readonly string ToString() => $"float3({x:R}, {y:R}, {z:R})";
public static float3 operator +(float3 value) => value; public static float3 operator +(float3 value) => value;
public static float3 operator -(float3 value) => new(-value.x, -value.y, -value.z); public static float3 operator -(float3 value) => new(-value.x, -value.y, -value.z);
public static float3 operator +(float3 left, float3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z); public static float3 operator +(float3 left, float3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z);
public static float3 operator +(float3 left, float right) => new(left.x + right, left.y + right, left.z + right);
public static float3 operator +(float left, float3 right) => new(left + right.x, left + right.y, left + right.z);
public static float3 operator -(float3 left, float3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z); public static float3 operator -(float3 left, float3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z);
public static float3 operator -(float3 left, float right) => new(left.x - right, left.y - right, left.z - right);
public static float3 operator -(float left, float3 right) => new(left - right.x, left - right.y, left - right.z);
public static float3 operator *(float3 left, float3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z); public static float3 operator *(float3 left, float3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z);
public static float3 operator *(float3 left, float right) => new(left.x * right, left.y * right, left.z * right);
public static float3 operator *(float left, float3 right) => new(left * right.x, left * right.y, left * right.z);
public static float3 operator /(float3 left, float3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z); public static float3 operator /(float3 left, float3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z);
public static float3 operator /(float3 left, float right) => new(left.x / right, left.y / right, left.z / right);
public static float3 operator /(float left, float3 right) => new(left / right.x, left / right.y, left / right.z);
public static float3 operator %(float3 left, float3 right) => new(left.x % right.x, left.y % right.y, left.z % right.z); public static float3 operator %(float3 left, float3 right) => new(left.x % right.x, left.y % right.y, left.z % right.z);
public static float3 operator %(float3 left, float right) => new(left.x % right, left.y % right, left.z % right);
public static float3 operator %(float left, float3 right) => new(left % right.x, left % right.y, left % right.z);
public static bool3 operator <(float3 left, float3 right) => new(left.x < right.x, left.y < right.y, left.z < right.z);
public static bool3 operator <(float3 left, float right) => new(left.x < right, left.y < right, left.z < right);
public static bool3 operator <(float left, float3 right) => new(left < right.x, left < right.y, left < right.z);
public static bool3 operator >(float3 left, float3 right) => new(left.x > right.x, left.y > right.y, left.z > right.z);
public static bool3 operator >(float3 left, float right) => new(left.x > right, left.y > right, left.z > right);
public static bool3 operator >(float left, float3 right) => new(left > right.x, left > right.y, left > right.z);
public static bool3 operator <=(float3 left, float3 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z);
public static bool3 operator <=(float3 left, float right) => new(left.x <= right, left.y <= right, left.z <= right);
public static bool3 operator <=(float left, float3 right) => new(left <= right.x, left <= right.y, left <= right.z);
public static bool3 operator >=(float3 left, float3 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z);
public static bool3 operator >=(float3 left, float right) => new(left.x >= right, left.y >= right, left.z >= right);
public static bool3 operator >=(float left, float3 right) => new(left >= right.x, left >= right.y, left >= right.z);
public static bool3 operator ==(float3 left, float3 right) => new(left.x == right.x, left.y == right.y, left.z == right.z);
public static bool3 operator ==(float3 left, float right) => new(left.x == right, left.y == right, left.z == right);
public static bool3 operator ==(float left, float3 right) => new(left == right.x, left == right.y, left == right.z);
public static bool3 operator !=(float3 left, float3 right) => new(left.x != right.x, left.y != right.y, left.z != right.z);
public static bool3 operator !=(float3 left, float right) => new(left.x != right, left.y != right, left.z != right);
public static bool3 operator !=(float left, float3 right) => new(left != right.x, left != right.y, left != right.z);
public readonly bool Equals(float3 other) => x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z);
public override readonly bool Equals(object? obj) => obj is float3 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y, z);
public override readonly string ToString() => FormattableString.Invariant($"float3({x:R}, {y:R}, {z:R})");
} }

View file

@ -0,0 +1,116 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace CultMath;
/// <summary>
/// HLSL float3x3: row-major. Constructors take rows, <c>m[i]</c> is a writable reference to
/// row i (so <c>m[1][2] = s</c> works), and <c>_mRC</c> names row R, column C exactly as HLSL
/// does. Use <see cref="math.mul(float3x3, float3)"/> for products; there is no <c>*</c>
/// matrix product because HLSL's <c>*</c> is component-wise.
/// </summary>
public struct float3x3 : IEquatable<float3x3>
{
public static readonly float3x3 identity = new(1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
public static readonly float3x3 zero = new(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
private float3 _row0;
private float3 _row1;
private float3 _row2;
public float3x3(float m00, float m01, float m02, float m10, float m11, float m12, float m20, float m21, float m22)
{
_row0 = new float3(m00, m01, m02);
_row1 = new float3(m10, m11, m12);
_row2 = new float3(m20, m21, m22);
}
public float3x3(float3 row0, float3 row1, float3 row2)
{
_row0 = row0;
_row1 = row1;
_row2 = row2;
}
public float _m00 { readonly get => _row0.x; set => _row0.x = value; }
public float _m01 { readonly get => _row0.y; set => _row0.y = value; }
public float _m02 { readonly get => _row0.z; set => _row0.z = value; }
public float _m10 { readonly get => _row1.x; set => _row1.x = value; }
public float _m11 { readonly get => _row1.y; set => _row1.y = value; }
public float _m12 { readonly get => _row1.z; set => _row1.z = value; }
public float _m20 { readonly get => _row2.x; set => _row2.x = value; }
public float _m21 { readonly get => _row2.y; set => _row2.y = value; }
public float _m22 { readonly get => _row2.z; set => _row2.z = value; }
/// <remarks>Writes through a readonly field, <c>in</c> parameter, property, or <c>identity</c> hit a defensive copy and are lost; write to a local and assign it back.</remarks>
[UnscopedRef]
public ref float3 this[int row]
{
get
{
switch (row)
{
case 0: return ref _row0;
case 1: return ref _row1;
case 2: return ref _row2;
default: throw new ArgumentOutOfRangeException(nameof(row));
}
}
}
/// <summary>Rotation about +X for column vectors (<c>mul(m, v)</c>).</summary>
public static float3x3 RotateX(float radians)
{
var s = MathF.Sin(radians);
var c = MathF.Cos(radians);
return new float3x3(1.0f, 0.0f, 0.0f, 0.0f, c, -s, 0.0f, s, c);
}
/// <summary>Rotation about +Y for column vectors (<c>mul(m, v)</c>).</summary>
public static float3x3 RotateY(float radians)
{
var s = MathF.Sin(radians);
var c = MathF.Cos(radians);
return new float3x3(c, 0.0f, s, 0.0f, 1.0f, 0.0f, -s, 0.0f, c);
}
/// <summary>Rotation about +Z for column vectors (<c>mul(m, v)</c>).</summary>
public static float3x3 RotateZ(float radians)
{
var s = MathF.Sin(radians);
var c = MathF.Cos(radians);
return new float3x3(c, -s, 0.0f, s, c, 0.0f, 0.0f, 0.0f, 1.0f);
}
/// <summary>
/// Extrinsic Euler rotation in radians. The order names the axis applied first:
/// <see cref="math.RotationOrder.YXZ"/> rotates about Y, then X, then Z, so the
/// matrix is <c>mul(RotateZ, mul(RotateX, RotateY))</c> for column vectors.
/// </summary>
public static float3x3 Euler(float3 xyz, math.RotationOrder order = math.RotationOrder.Default)
{
var x = RotateX(xyz.x);
var y = RotateY(xyz.y);
var z = RotateZ(xyz.z);
return order switch
{
math.RotationOrder.XYZ => math.mul(z, math.mul(y, x)),
math.RotationOrder.XZY => math.mul(y, math.mul(z, x)),
math.RotationOrder.YXZ => math.mul(z, math.mul(x, y)),
math.RotationOrder.YZX => math.mul(x, math.mul(z, y)),
math.RotationOrder.ZXY => math.mul(y, math.mul(x, z)),
math.RotationOrder.ZYX => math.mul(x, math.mul(y, z)),
_ => throw new ArgumentOutOfRangeException(nameof(order)),
};
}
public static float3x3 Euler(float x, float y, float z, math.RotationOrder order = math.RotationOrder.Default) =>
Euler(new float3(x, y, z), order);
public readonly bool Equals(float3x3 other) => _row0.Equals(other._row0) && _row1.Equals(other._row1) && _row2.Equals(other._row2);
public override readonly bool Equals(object? obj) => obj is float3x3 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(_row0, _row1, _row2);
public override readonly string ToString() =>
FormattableString.Invariant($"float3x3({_row0.x:R}, {_row0.y:R}, {_row0.z:R}, {_row1.x:R}, {_row1.y:R}, {_row1.z:R}, {_row2.x:R}, {_row2.y:R}, {_row2.z:R})");
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9ca36a9bfff144a9b51478a6d7b19c8e

View file

@ -2,47 +2,46 @@ using System;
namespace CultMath; namespace CultMath;
public record struct float4(float x, float y, float z, float w) public partial struct float4 : IEquatable<float4>
{ {
public static readonly float4 zero = new(0.0f, 0.0f, 0.0f, 0.0f); 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); public static readonly float4 one = new(1.0f, 1.0f, 1.0f, 1.0f);
public float4(float2 xy, float z, float w) public float x;
: this(xy.x, xy.y, z, w) public float y;
public float z;
public float w;
public float4(float x, float y, float z, float w)
{ {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
} }
public float4(float3 xyz, float w) public float this[int index]
: this(xyz.x, xyz.y, xyz.z, w)
{ {
} readonly get => index switch
{
public float4(float x, float3 yzw) 0 => x,
: this(x, yzw.x, yzw.y, yzw.z) 1 => y,
{ 2 => z,
} 3 => w,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
public float2 xy };
{
get => new(x, y);
set set
{ {
x = value.x; switch (index)
y = value.y; {
case 0: x = value; break;
case 1: y = value; break;
case 2: z = value; break;
case 3: w = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
} }
} }
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
{
0 => x,
1 => y,
2 => z,
3 => w,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public static implicit operator float4(float value) => new(value, value, value, value); public static implicit operator float4(float value) => new(value, value, value, value);
public static explicit operator System.Numerics.Vector4(float4 value) => new(value.x, value.y, value.z, value.w); public static explicit operator System.Numerics.Vector4(float4 value) => new(value.x, value.y, value.z, value.w);
@ -50,9 +49,44 @@ public record struct float4(float x, float y, float z, float w)
public static float4 operator +(float4 value) => value; public static float4 operator +(float4 value) => value;
public static float4 operator -(float4 value) => new(-value.x, -value.y, -value.z, -value.w); public static float4 operator -(float4 value) => new(-value.x, -value.y, -value.z, -value.w);
public static float4 operator +(float4 left, float4 right) => new(left.x + right.x, left.y + right.y, left.z + right.z, left.w + right.w); public static float4 operator +(float4 left, float4 right) => new(left.x + right.x, left.y + right.y, left.z + right.z, left.w + right.w);
public static float4 operator +(float4 left, float right) => new(left.x + right, left.y + right, left.z + right, left.w + right);
public static float4 operator +(float left, float4 right) => new(left + right.x, left + right.y, left + right.z, left + right.w);
public static float4 operator -(float4 left, float4 right) => new(left.x - right.x, left.y - right.y, left.z - right.z, left.w - right.w); public static float4 operator -(float4 left, float4 right) => new(left.x - right.x, left.y - right.y, left.z - right.z, left.w - right.w);
public static float4 operator -(float4 left, float right) => new(left.x - right, left.y - right, left.z - right, left.w - right);
public static float4 operator -(float left, float4 right) => new(left - right.x, left - right.y, left - right.z, left - right.w);
public static float4 operator *(float4 left, float4 right) => new(left.x * right.x, left.y * right.y, left.z * right.z, left.w * right.w); public static float4 operator *(float4 left, float4 right) => new(left.x * right.x, left.y * right.y, left.z * right.z, left.w * right.w);
public static float4 operator *(float4 left, float right) => new(left.x * right, left.y * right, left.z * right, left.w * right);
public static float4 operator *(float left, float4 right) => new(left * right.x, left * right.y, left * right.z, left * right.w);
public static float4 operator /(float4 left, float4 right) => new(left.x / right.x, left.y / right.y, left.z / right.z, left.w / right.w); public static float4 operator /(float4 left, float4 right) => new(left.x / right.x, left.y / right.y, left.z / right.z, left.w / right.w);
public static float4 operator /(float4 left, float right) => new(left.x / right, left.y / right, left.z / right, left.w / right);
public static float4 operator /(float left, float4 right) => new(left / right.x, left / right.y, left / right.z, left / right.w);
public static float4 operator %(float4 left, float4 right) => new(left.x % right.x, left.y % right.y, left.z % right.z, left.w % right.w); public static float4 operator %(float4 left, float4 right) => new(left.x % right.x, left.y % right.y, left.z % right.z, left.w % right.w);
public static float4 operator %(float4 left, float right) => new(left.x % right, left.y % right, left.z % right, left.w % right);
public static float4 operator %(float left, float4 right) => new(left % right.x, left % right.y, left % right.z, left % right.w);
public static bool4 operator <(float4 left, float4 right) => new(left.x < right.x, left.y < right.y, left.z < right.z, left.w < right.w);
public static bool4 operator <(float4 left, float right) => new(left.x < right, left.y < right, left.z < right, left.w < right);
public static bool4 operator <(float left, float4 right) => new(left < right.x, left < right.y, left < right.z, left < right.w);
public static bool4 operator >(float4 left, float4 right) => new(left.x > right.x, left.y > right.y, left.z > right.z, left.w > right.w);
public static bool4 operator >(float4 left, float right) => new(left.x > right, left.y > right, left.z > right, left.w > right);
public static bool4 operator >(float left, float4 right) => new(left > right.x, left > right.y, left > right.z, left > right.w);
public static bool4 operator <=(float4 left, float4 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z, left.w <= right.w);
public static bool4 operator <=(float4 left, float right) => new(left.x <= right, left.y <= right, left.z <= right, left.w <= right);
public static bool4 operator <=(float left, float4 right) => new(left <= right.x, left <= right.y, left <= right.z, left <= right.w);
public static bool4 operator >=(float4 left, float4 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z, left.w >= right.w);
public static bool4 operator >=(float4 left, float right) => new(left.x >= right, left.y >= right, left.z >= right, left.w >= right);
public static bool4 operator >=(float left, float4 right) => new(left >= right.x, left >= right.y, left >= right.z, left >= right.w);
public static bool4 operator ==(float4 left, float4 right) => new(left.x == right.x, left.y == right.y, left.z == right.z, left.w == right.w);
public static bool4 operator ==(float4 left, float right) => new(left.x == right, left.y == right, left.z == right, left.w == right);
public static bool4 operator ==(float left, float4 right) => new(left == right.x, left == right.y, left == right.z, left == right.w);
public static bool4 operator !=(float4 left, float4 right) => new(left.x != right.x, left.y != right.y, left.z != right.z, left.w != right.w);
public static bool4 operator !=(float4 left, float right) => new(left.x != right, left.y != right, left.z != right, left.w != right);
public static bool4 operator !=(float left, float4 right) => new(left != right.x, left != right.y, left != right.z, left != right.w);
public readonly bool Equals(float4 other) => x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z) && w.Equals(other.w);
public override readonly bool Equals(object? obj) => obj is float4 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y, z, w);
public override readonly string ToString() => FormattableString.Invariant($"float4({x:R}, {y:R}, {z:R}, {w:R})");
} }

View file

@ -2,17 +2,38 @@ using System;
namespace CultMath; namespace CultMath;
public record struct int2(int x, int y) public partial struct int2 : IEquatable<int2>
{ {
public static readonly int2 zero = new(0, 0); public static readonly int2 zero = new(0, 0);
public static readonly int2 one = new(1, 1); public static readonly int2 one = new(1, 1);
public int this[int index] => index switch public int x;
public int y;
public int2(int x, int y)
{ {
0 => x, this.x = x;
1 => y, this.y = y;
_ => throw new ArgumentOutOfRangeException(nameof(index)), }
};
public int this[int index]
{
readonly get => index switch
{
0 => x,
1 => y,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator int2(int value) => new(value, value); public static implicit operator int2(int value) => new(value, value);
public static implicit operator float2(int2 value) => new(value.x, value.y); public static implicit operator float2(int2 value) => new(value.x, value.y);
@ -26,4 +47,16 @@ public record struct int2(int x, int 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); public static int2 operator %(int2 left, int2 right) => new(left.x % right.x, left.y % right.y);
public static bool2 operator <(int2 left, int2 right) => new(left.x < right.x, left.y < right.y);
public static bool2 operator >(int2 left, int2 right) => new(left.x > right.x, left.y > right.y);
public static bool2 operator <=(int2 left, int2 right) => new(left.x <= right.x, left.y <= right.y);
public static bool2 operator >=(int2 left, int2 right) => new(left.x >= right.x, left.y >= right.y);
public static bool2 operator ==(int2 left, int2 right) => new(left.x == right.x, left.y == right.y);
public static bool2 operator !=(int2 left, int2 right) => new(left.x != right.x, left.y != right.y);
public readonly bool Equals(int2 other) => x == other.x && y == other.y;
public override readonly bool Equals(object? obj) => obj is int2 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y);
public override readonly string ToString() => FormattableString.Invariant($"int2({x}, {y})");
} }

View file

@ -0,0 +1,64 @@
using System;
namespace CultMath;
public partial struct int3 : IEquatable<int3>
{
public static readonly int3 zero = new(0, 0, 0);
public static readonly int3 one = new(1, 1, 1);
public int x;
public int y;
public int z;
public int3(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
public int this[int index]
{
readonly get => index switch
{
0 => x,
1 => y,
2 => z,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
case 2: z = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator int3(int value) => new(value, value, value);
public static implicit operator float3(int3 value) => new(value.x, value.y, value.z);
public static int3 operator +(int3 value) => value;
public static int3 operator -(int3 value) => new(-value.x, -value.y, -value.z);
public static int3 operator +(int3 left, int3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z);
public static int3 operator -(int3 left, int3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z);
public static int3 operator *(int3 left, int3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z);
public static int3 operator /(int3 left, int3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z);
public static int3 operator %(int3 left, int3 right) => new(left.x % right.x, left.y % right.y, left.z % right.z);
public static bool3 operator <(int3 left, int3 right) => new(left.x < right.x, left.y < right.y, left.z < right.z);
public static bool3 operator >(int3 left, int3 right) => new(left.x > right.x, left.y > right.y, left.z > right.z);
public static bool3 operator <=(int3 left, int3 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z);
public static bool3 operator >=(int3 left, int3 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z);
public static bool3 operator ==(int3 left, int3 right) => new(left.x == right.x, left.y == right.y, left.z == right.z);
public static bool3 operator !=(int3 left, int3 right) => new(left.x != right.x, left.y != right.y, left.z != right.z);
public readonly bool Equals(int3 other) => x == other.x && y == other.y && z == other.z;
public override readonly bool Equals(object? obj) => obj is int3 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y, z);
public override readonly string ToString() => FormattableString.Invariant($"int3({x}, {y}, {z})");
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 42ee98aace354db19b3ae87bdcbc1df9

View file

@ -0,0 +1,68 @@
using System;
namespace CultMath;
public partial struct int4 : IEquatable<int4>
{
public static readonly int4 zero = new(0, 0, 0, 0);
public static readonly int4 one = new(1, 1, 1, 1);
public int x;
public int y;
public int z;
public int w;
public int4(int x, int y, int z, int w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
public int this[int index]
{
readonly get => index switch
{
0 => x,
1 => y,
2 => z,
3 => w,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
set
{
switch (index)
{
case 0: x = value; break;
case 1: y = value; break;
case 2: z = value; break;
case 3: w = value; break;
default: throw new ArgumentOutOfRangeException(nameof(index));
}
}
}
public static implicit operator int4(int value) => new(value, value, value, value);
public static implicit operator float4(int4 value) => new(value.x, value.y, value.z, value.w);
public static int4 operator +(int4 value) => value;
public static int4 operator -(int4 value) => new(-value.x, -value.y, -value.z, -value.w);
public static int4 operator +(int4 left, int4 right) => new(left.x + right.x, left.y + right.y, left.z + right.z, left.w + right.w);
public static int4 operator -(int4 left, int4 right) => new(left.x - right.x, left.y - right.y, left.z - right.z, left.w - right.w);
public static int4 operator *(int4 left, int4 right) => new(left.x * right.x, left.y * right.y, left.z * right.z, left.w * right.w);
public static int4 operator /(int4 left, int4 right) => new(left.x / right.x, left.y / right.y, left.z / right.z, left.w / right.w);
public static int4 operator %(int4 left, int4 right) => new(left.x % right.x, left.y % right.y, left.z % right.z, left.w % right.w);
public static bool4 operator <(int4 left, int4 right) => new(left.x < right.x, left.y < right.y, left.z < right.z, left.w < right.w);
public static bool4 operator >(int4 left, int4 right) => new(left.x > right.x, left.y > right.y, left.z > right.z, left.w > right.w);
public static bool4 operator <=(int4 left, int4 right) => new(left.x <= right.x, left.y <= right.y, left.z <= right.z, left.w <= right.w);
public static bool4 operator >=(int4 left, int4 right) => new(left.x >= right.x, left.y >= right.y, left.z >= right.z, left.w >= right.w);
public static bool4 operator ==(int4 left, int4 right) => new(left.x == right.x, left.y == right.y, left.z == right.z, left.w == right.w);
public static bool4 operator !=(int4 left, int4 right) => new(left.x != right.x, left.y != right.y, left.z != right.z, left.w != right.w);
public readonly bool Equals(int4 other) => x == other.x && y == other.y && z == other.z && w == other.w;
public override readonly bool Equals(object? obj) => obj is int4 other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y, z, w);
public override readonly string ToString() => FormattableString.Invariant($"int4({x}, {y}, {z}, {w})");
}

View file

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

View file

@ -2,26 +2,56 @@ using System;
namespace CultMath; namespace CultMath;
public static class math public static partial class math
{ {
public const float PI = MathF.PI; public const float PI = MathF.PI;
public const float TAU = MathF.PI * 2.0f; public const float TAU = MathF.PI * 2.0f;
public const float HALF_PI = MathF.PI * 0.5f; public const float HALF_PI = MathF.PI * 0.5f;
/// <summary>
/// Extrinsic Euler order for <see cref="CultMath.float3x3.Euler(CultMath.float3, RotationOrder)"/>:
/// the first letter is the axis applied first. Nested here so <c>using static CultMath.math;</c>
/// brings it into scope with the rest of the shader vocabulary.
/// </summary>
public enum RotationOrder : byte
{
XYZ,
XZY,
YXZ,
YZX,
ZXY,
ZYX,
Default = ZXY,
}
public static float2 float2(float x, float y) => new(x, y); public static float2 float2(float x, float y) => new(x, y);
public static float2 float2(float value) => new(value, value); 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 x, bool y) => new(x, y);
public static bool2 bool2(bool value) => new(value, value); 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 x, int y) => new(x, y);
public static int2 int2(int value) => new(value, value); public static int2 int2(int value) => new(value, value);
// Mixed constructor functions (float4(float2, float2), ...) are generated in Swizzles.g.cs.
public static float3 float3(float x, float y, float z) => new(x, y, z); 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 float3 float3(float value) => new(value, value, value);
public static float4 float4(float x, float y, float z, float w) => new(x, y, z, w); public static float4 float4(float 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 float4 float4(float value) => new(value, value, value, value);
public static bool3 bool3(bool x, bool y, bool z) => new(x, y, z);
public static bool3 bool3(bool value) => new(value, value, value);
public static bool4 bool4(bool x, bool y, bool z, bool w) => new(x, y, z, w);
public static bool4 bool4(bool value) => new(value, value, value, value);
public static int3 int3(int x, int y, int z) => new(x, y, z);
public static int3 int3(int value) => new(value, value, value);
public static int4 int4(int x, int y, int z, int w) => new(x, y, z, w);
public static int4 int4(int value) => new(value, value, value, value);
// Matrix constructor functions take rows, as HLSL does. Under `using static CultMath.math;`
// these method groups hide the type names in member access, so static members need a
// qualified type: `CultMath.float3x3.Euler(...)`, `CultMath.float2x2.Rotate(...)`.
public static float2x2 float2x2(float m00, float m01, float m10, float m11) => new(m00, m01, m10, m11);
public static float2x2 float2x2(float2 row0, float2 row1) => new(row0, row1);
public static float3x3 float3x3(float m00, float m01, float m02, float m10, float m11, float m12, float m20, float m21, float m22) =>
new(m00, m01, m02, m10, m11, m12, m20, m21, m22);
public static float3x3 float3x3(float3 row0, float3 row1, float3 row2) => new(row0, row1, row2);
public static float radians(float degrees) => degrees * (PI / 180.0f); public static float radians(float degrees) => degrees * (PI / 180.0f);
public static float degrees(float radians) => radians * (180.0f / PI); public static float degrees(float radians) => radians * (180.0f / PI);
public static float sin(float value) => MathF.Sin(value); public static float sin(float value) => MathF.Sin(value);
@ -46,11 +76,20 @@ public static class math
public static float2 abs(float2 value) => new(abs(value.x), abs(value.y)); 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 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 float4 abs(float4 value) => new(abs(value.x), abs(value.y), abs(value.z), abs(value.w));
public static int abs(int value) => Math.Abs(value);
public static int2 abs(int2 value) => new(abs(value.x), abs(value.y));
public static int3 abs(int3 value) => new(abs(value.x), abs(value.y), abs(value.z));
public static int4 abs(int4 value) => new(abs(value.x), abs(value.y), abs(value.z), abs(value.w));
public static float sign(float value) => MathF.Sign(value); // HLSL sign returns int. NaN compares false both ways, so it yields 0 instead of throwing.
public static float2 sign(float2 value) => new(sign(value.x), sign(value.y)); public static int sign(float value) => value > 0.0f ? 1 : value < 0.0f ? -1 : 0;
public static float3 sign(float3 value) => new(sign(value.x), sign(value.y), sign(value.z)); public static int2 sign(float2 value) => new(sign(value.x), sign(value.y));
public static float4 sign(float4 value) => new(sign(value.x), sign(value.y), sign(value.z), sign(value.w)); public static int3 sign(float3 value) => new(sign(value.x), sign(value.y), sign(value.z));
public static int4 sign(float4 value) => new(sign(value.x), sign(value.y), sign(value.z), sign(value.w));
public static int sign(int value) => value > 0 ? 1 : value < 0 ? -1 : 0;
public static int2 sign(int2 value) => new(sign(value.x), sign(value.y));
public static int3 sign(int3 value) => new(sign(value.x), sign(value.y), sign(value.z));
public static int4 sign(int4 value) => new(sign(value.x), sign(value.y), sign(value.z), sign(value.w));
public static float floor(float value) => MathF.Floor(value); public static float floor(float value) => MathF.Floor(value);
public static float2 floor(float2 value) => new(floor(value.x), floor(value.y)); public static float2 floor(float2 value) => new(floor(value.x), floor(value.y));
@ -67,27 +106,36 @@ public static class math
public static float3 frac(float3 value) => value - floor(value); public static float3 frac(float3 value) => value - floor(value);
public static float4 frac(float4 value) => value - floor(value); public static float4 frac(float4 value) => value - floor(value);
public static float min(float left, float right) => MathF.Min(left, right); // dxc lowers float min/max/clamp to DXIL FMin/FMax (saturate to Saturate, defined as FMin(1, FMax(0, x))).
// DXIL.rst: FMin(a, b) is a < b ? a : b, FMax(a, b) is a >= b ? a : b, and a NaN operand returns the other.
public static float min(float left, float right) => left < right || float.IsNaN(right) ? left : right;
public static int min(int left, int right) => Math.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 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 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 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 int2 min(int2 left, int2 right) => new(min(left.x, right.x), min(left.y, right.y));
public static int3 min(int3 left, int3 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z));
public static int4 min(int4 left, int4 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z), min(left.w, right.w));
public static float max(float left, float right) => MathF.Max(left, right); public static float max(float left, float right) => left >= right || float.IsNaN(right) ? left : right;
public static int max(int left, int right) => Math.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 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 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)); 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));
public static int2 max(int2 left, int2 right) => new(max(left.x, right.x), max(left.y, right.y));
public static int3 max(int3 left, int3 right) => new(max(left.x, right.x), max(left.y, right.y), max(left.z, right.z));
public static int4 max(int4 left, int4 right) => new(max(left.x, right.x), max(left.y, right.y), max(left.z, right.z), max(left.w, right.w));
public static float clamp(float value, float minimum, float maximum) => min(max(value, minimum), maximum); public static float clamp(float value, float minimum, float maximum) => min(max(value, minimum), maximum);
public static float2 clamp(float2 value, float2 minimum, float2 maximum) => min(max(value, minimum), maximum); public static float2 clamp(float2 value, float2 minimum, float2 maximum) => min(max(value, minimum), maximum);
public static float3 clamp(float3 value, float3 minimum, float3 maximum) => min(max(value, minimum), maximum); public static float3 clamp(float3 value, float3 minimum, float3 maximum) => min(max(value, minimum), maximum);
public static float4 clamp(float4 value, float4 minimum, float4 maximum) => min(max(value, minimum), maximum); public static float4 clamp(float4 value, float4 minimum, float4 maximum) => min(max(value, minimum), maximum);
public static float saturate(float value) => clamp(value, 0.0f, 1.0f); // DXIL Saturate is FMin(1, FMax(0, x)); that operand order, unlike clamp(x, 0, 1), maps -0 to +0.
public static float2 saturate(float2 value) => clamp(value, 0.0f, 1.0f); public static float saturate(float value) => min(1.0f, max(0.0f, value));
public static float3 saturate(float3 value) => clamp(value, 0.0f, 1.0f); public static float2 saturate(float2 value) => min(1.0f, max(0.0f, value));
public static float4 saturate(float4 value) => clamp(value, 0.0f, 1.0f); public static float3 saturate(float3 value) => min(1.0f, max(0.0f, value));
public static float4 saturate(float4 value) => min(1.0f, max(0.0f, value));
public static float lerp(float start, float end, float amount) => start + (end - start) * amount; 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 double lerp(double start, double end, double amount) => start + (end - start) * amount;
@ -105,6 +153,7 @@ public static class math
public static double2 unlerp(double2 start, double2 end, double2 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 double3 unlerp(double3 start, double3 end, double3 value) => (value - start) / (end - start);
// dxc lowers step(y, x) to x < y ? 0 : 1 (fcmp olt, then select), so a NaN on either side yields 1.
public static float step(float edge, float value) => value < edge ? 0.0f : 1.0f; 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)); public static float2 step(float2 edge, float2 value) => new(step(edge.x, value.x), step(edge.y, value.y));
public static float3 step(float3 edge, float3 value) => new(step(edge.x, value.x), step(edge.y, value.y), step(edge.z, value.z)); public static float3 step(float3 edge, float3 value) => new(step(edge.x, value.x), step(edge.y, value.y), step(edge.z, value.z));
@ -156,9 +205,9 @@ public static class math
public static float distance(float3 left, float3 right) => length(left - right); public static float distance(float3 left, float3 right) => length(left - right);
public static float distance(float4 left, float4 right) => length(left - right); public static float distance(float4 left, float4 right) => length(left - right);
public static float2 normalize(float2 value) => value / MathF.Max(length(value), 1.0e-20f); public static float2 normalize(float2 value) => value / max(length(value), 1.0e-20f);
public static float3 normalize(float3 value) => value / MathF.Max(length(value), 1.0e-20f); public static float3 normalize(float3 value) => value / max(length(value), 1.0e-20f);
public static float4 normalize(float4 value) => value / MathF.Max(length(value), 1.0e-20f); public static float4 normalize(float4 value) => value / max(length(value), 1.0e-20f);
public static quaternion normalize(quaternion value) 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); var length = MathF.Sqrt(value.x * value.x + value.y * value.y + value.z * value.z + value.w * value.w);
@ -324,9 +373,164 @@ public static class math
public static float hash(float2 value) => hash(dot(value, new float2(127.1f, 311.7f))); public static float hash(float2 value) => hash(dot(value, new float2(127.1f, 311.7f)));
public static float hash(float3 value) => hash(dot(value, new float3(127.1f, 311.7f, 74.7f))); public static float hash(float3 value) => hash(dot(value, new float3(127.1f, 311.7f, 74.7f)));
public static float log(float value) => MathF.Log(value);
public static float2 log(float2 value) => new(log(value.x), log(value.y));
public static float3 log(float3 value) => new(log(value.x), log(value.y), log(value.z));
public static float4 log(float4 value) => new(log(value.x), log(value.y), log(value.z), log(value.w));
public static float2 pow(float2 value, float2 power) => new(pow(value.x, power.x), pow(value.y, power.y));
public static float3 pow(float3 value, float3 power) => new(pow(value.x, power.x), pow(value.y, power.y), pow(value.z, power.z));
public static float4 pow(float4 value, float4 power) => new(pow(value.x, power.x), pow(value.y, power.y), pow(value.z, power.z), pow(value.w, power.w));
public static float2 sqrt(float2 value) => new(sqrt(value.x), sqrt(value.y));
public static float3 sqrt(float3 value) => new(sqrt(value.x), sqrt(value.y), sqrt(value.z));
public static float4 sqrt(float4 value) => new(sqrt(value.x), sqrt(value.y), sqrt(value.z), sqrt(value.w));
public static float2 sin(float2 value) => new(sin(value.x), sin(value.y));
public static float3 sin(float3 value) => new(sin(value.x), sin(value.y), sin(value.z));
public static float4 sin(float4 value) => new(sin(value.x), sin(value.y), sin(value.z), sin(value.w));
public static float2 cos(float2 value) => new(cos(value.x), cos(value.y));
public static float3 cos(float3 value) => new(cos(value.x), cos(value.y), cos(value.z));
public static float4 cos(float4 value) => new(cos(value.x), cos(value.y), cos(value.z), cos(value.w));
public static float2 acos(float2 value) => new(acos(value.x), acos(value.y));
public static float3 acos(float3 value) => new(acos(value.x), acos(value.y), acos(value.z));
public static float4 acos(float4 value) => new(acos(value.x), acos(value.y), acos(value.z), acos(value.w));
public static float2 atan2(float2 y, float2 x) => new(atan2(y.x, x.x), atan2(y.y, x.y));
public static float3 atan2(float3 y, float3 x) => new(atan2(y.x, x.x), atan2(y.y, x.y), atan2(y.z, x.z));
public static float4 atan2(float4 y, float4 x) => new(atan2(y.x, x.x), atan2(y.y, x.y), atan2(y.z, x.z), atan2(y.w, x.w));
// DXIL FMin/FMax take double overloads with the same rules as float (see min/max above).
public static double min(double left, double right) => left < right || double.IsNaN(right) ? left : right;
public static double max(double left, double right) => left >= right || double.IsNaN(right) ? left : right;
public static bool any(bool2 value) => value.x || value.y;
public static bool any(bool3 value) => value.x || value.y || value.z;
public static bool any(bool4 value) => value.x || value.y || value.z || value.w;
public static bool all(bool2 value) => value.x && value.y;
public static bool all(bool3 value) => value.x && value.y && value.z;
public static bool all(bool4 value) => value.x && value.y && value.z && value.w;
// HLSL any/all on numeric vectors test components against zero (NaN != 0 counts as true).
public static bool any(float2 value) => value.x != 0.0f || value.y != 0.0f;
public static bool any(float3 value) => value.x != 0.0f || value.y != 0.0f || value.z != 0.0f;
public static bool any(float4 value) => value.x != 0.0f || value.y != 0.0f || value.z != 0.0f || value.w != 0.0f;
public static bool all(float2 value) => value.x != 0.0f && value.y != 0.0f;
public static bool all(float3 value) => value.x != 0.0f && value.y != 0.0f && value.z != 0.0f;
public static bool all(float4 value) => value.x != 0.0f && value.y != 0.0f && value.z != 0.0f && value.w != 0.0f;
public static bool any(int2 value) => value.x != 0 || value.y != 0;
public static bool any(int3 value) => value.x != 0 || value.y != 0 || value.z != 0;
public static bool any(int4 value) => value.x != 0 || value.y != 0 || value.z != 0 || value.w != 0;
public static bool all(int2 value) => value.x != 0 && value.y != 0;
public static bool all(int3 value) => value.x != 0 && value.y != 0 && value.z != 0;
public static bool all(int4 value) => value.x != 0 && value.y != 0 && value.z != 0 && value.w != 0;
// HLSL 2021 argument order: select(condition, whenTrue, whenFalse).
// Unity.Mathematics' select(falseValue, trueValue, condition) is the reverse.
public static float select(bool condition, float whenTrue, float whenFalse) => condition ? whenTrue : whenFalse;
public static float2 select(bool2 condition, float2 whenTrue, float2 whenFalse) =>
new(condition.x ? whenTrue.x : whenFalse.x, condition.y ? whenTrue.y : whenFalse.y);
public static float3 select(bool3 condition, float3 whenTrue, float3 whenFalse) =>
new(condition.x ? whenTrue.x : whenFalse.x, condition.y ? whenTrue.y : whenFalse.y, condition.z ? whenTrue.z : whenFalse.z);
public static float4 select(bool4 condition, float4 whenTrue, float4 whenFalse) =>
new(condition.x ? whenTrue.x : whenFalse.x, condition.y ? whenTrue.y : whenFalse.y, condition.z ? whenTrue.z : whenFalse.z, condition.w ? whenTrue.w : whenFalse.w);
// HLSL mul: mul(m, v) treats v as a column vector, mul(v, m) as a row vector.
public static float mul(float left, float right) => left * right;
public static float2 mul(float2x2 m, float2 v) =>
new(m._m00 * v.x + m._m01 * v.y, m._m10 * v.x + m._m11 * v.y);
public static float2 mul(float2 v, float2x2 m) =>
new(v.x * m._m00 + v.y * m._m10, v.x * m._m01 + v.y * m._m11);
public static float2x2 mul(float2x2 a, float2x2 b) =>
new(
a._m00 * b._m00 + a._m01 * b._m10, a._m00 * b._m01 + a._m01 * b._m11,
a._m10 * b._m00 + a._m11 * b._m10, a._m10 * b._m01 + a._m11 * b._m11);
public static float3 mul(float3x3 m, float3 v) =>
new(
m._m00 * v.x + m._m01 * v.y + m._m02 * v.z,
m._m10 * v.x + m._m11 * v.y + m._m12 * v.z,
m._m20 * v.x + m._m21 * v.y + m._m22 * v.z);
public static float3 mul(float3 v, float3x3 m) =>
new(
v.x * m._m00 + v.y * m._m10 + v.z * m._m20,
v.x * m._m01 + v.y * m._m11 + v.z * m._m21,
v.x * m._m02 + v.y * m._m12 + v.z * m._m22);
public static float3x3 mul(float3x3 a, float3x3 b) =>
new(
a._m00 * b._m00 + a._m01 * b._m10 + a._m02 * b._m20,
a._m00 * b._m01 + a._m01 * b._m11 + a._m02 * b._m21,
a._m00 * b._m02 + a._m01 * b._m12 + a._m02 * b._m22,
a._m10 * b._m00 + a._m11 * b._m10 + a._m12 * b._m20,
a._m10 * b._m01 + a._m11 * b._m11 + a._m12 * b._m21,
a._m10 * b._m02 + a._m11 * b._m12 + a._m12 * b._m22,
a._m20 * b._m00 + a._m21 * b._m10 + a._m22 * b._m20,
a._m20 * b._m01 + a._m21 * b._m11 + a._m22 * b._m21,
a._m20 * b._m02 + a._m21 * b._m12 + a._m22 * b._m22);
// Ashima Arts / Ian McEwan 3D simplex noise (MIT), mirrored by cultmath_snoise(float3)
// in shaders/CultMath.hlsl with the same float32 evaluation order.
public static float snoise(float3 value)
{
const float cx = 1.0f / 6.0f;
const float cy = 1.0f / 3.0f;
var i = floor(value + dot(value, new float3(cy, cy, cy)));
var x0 = value - i + dot(i, new float3(cx, cx, cx));
var g = step(new float3(x0.y, x0.z, x0.x), x0);
var l = 1.0f - g;
var lzxy = new float3(l.z, l.x, l.y);
var i1 = min(g, lzxy);
var i2 = max(g, lzxy);
var x1 = x0 - i1 + cx;
var x2 = x0 - i2 + cy;
var x3 = x0 - 0.5f;
i = snoise_mod289(i);
var p = snoise_permute(snoise_permute(snoise_permute(
i.z + new float4(0.0f, i1.z, i2.z, 1.0f)) + i.y + new float4(0.0f, i1.y, i2.y, 1.0f)) + i.x + new float4(0.0f, i1.x, i2.x, 1.0f));
const float n = 0.142857142857f;
var ns = new float3(2.0f * n, 0.5f * n - 1.0f, n);
var j = p - 49.0f * floor(p * ns.z * ns.z);
var xs = floor(j * ns.z);
var ys = floor(j - 7.0f * xs);
var x = xs * ns.x + ns.y;
var y = ys * ns.x + ns.y;
var h = 1.0f - abs(x) - abs(y);
var b0 = new float4(x.x, x.y, y.x, y.y);
var b1 = new float4(x.z, x.w, y.z, y.w);
var s0 = floor(b0) * 2.0f + 1.0f;
var s1 = floor(b1) * 2.0f + 1.0f;
var sh = -step(h, 0.0f);
var a0 = new float4(b0.x, b0.z, b0.y, b0.w) + new float4(s0.x, s0.z, s0.y, s0.w) * new float4(sh.x, sh.x, sh.y, sh.y);
var a1 = new float4(b1.x, b1.z, b1.y, b1.w) + new float4(s1.x, s1.z, s1.y, s1.w) * new float4(sh.z, sh.z, sh.w, sh.w);
var p0 = new float3(a0.x, a0.y, h.x);
var p1 = new float3(a0.z, a0.w, h.y);
var p2 = new float3(a1.x, a1.y, h.z);
var p3 = new float3(a1.z, a1.w, h.w);
var norm = 1.79284291400159f - 0.85373472095314f * new float4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
var m = max(0.6f - new float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0f);
m = m * m;
return 42.0f * dot(m * m, new float4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
// Ashima Arts / Ian McEwan 2D simplex noise, kept component-explicit so // Ashima Arts / Ian McEwan 2D simplex noise, kept component-explicit so
// the C# and HLSL mirrors preserve the same float32 evaluation order. // the C# and HLSL mirrors preserve the same float32 evaluation order.
public static float simplex_noise(float2 value) public static float snoise(float2 value)
{ {
var c = new float4( var c = new float4(
0.211324865405187f, 0.211324865405187f,
@ -342,8 +546,8 @@ public static class math
x0.x + c.z, x0.x + c.z,
x0.y + c.z); x0.y + c.z);
i = simplex_mod289(i); i = snoise_mod289(i);
var p = simplex_permute(simplex_permute(i.y + new float3(0.0f, i1.y, 1.0f)) + i.x + new float3(0.0f, i1.x, 1.0f)); var p = snoise_permute(snoise_permute(i.y + new float3(0.0f, i1.y, 1.0f)) + i.x + new float3(0.0f, i1.x, 1.0f));
var m = max(0.5f - new float3( var m = max(0.5f - new float3(
dot(x0, x0), dot(x0, x0),
x12.x * x12.x + x12.y * x12.y, x12.x * x12.x + x12.y * x12.y,
@ -364,9 +568,11 @@ public static class math
return 130.0f * dot(m, g); return 130.0f * dot(m, g);
} }
private static float2 simplex_mod289(float2 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f; private static float2 snoise_mod289(float2 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f;
private static float3 simplex_mod289(float3 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f; private static float3 snoise_mod289(float3 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f;
private static float3 simplex_permute(float3 value) => simplex_mod289(((value * 34.0f) + 1.0f) * value); private static float4 snoise_mod289(float4 value) => value - floor(value * (1.0f / 289.0f)) * 289.0f;
private static float3 snoise_permute(float3 value) => snoise_mod289(((value * 34.0f) + 1.0f) * value);
private static float4 snoise_permute(float4 value) => snoise_mod289(((value * 34.0f) + 1.0f) * value);
public static float value_noise(float2 position) public static float value_noise(float2 position)
{ {

View file

@ -1,9 +1,28 @@
using System;
namespace CultMath; namespace CultMath;
public record struct quaternion(float x, float y, float z, float w) /// <summary>
/// Not an HLSL type: shaders carry rotations as float4. Equality therefore stays
/// scalar (<c>==</c> returns bool) instead of following the component-wise vector rule.
/// </summary>
public struct quaternion : IEquatable<quaternion>
{ {
public static readonly quaternion identity = new(0.0f, 0.0f, 0.0f, 1.0f); public static readonly quaternion identity = new(0.0f, 0.0f, 0.0f, 1.0f);
public float x;
public float y;
public float z;
public float w;
public quaternion(float x, float y, float z, float w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
public static quaternion LookRotation(float3 forward, float3 up) public static quaternion LookRotation(float3 forward, float3 up)
{ {
forward = math.normalize(forward); forward = math.normalize(forward);
@ -65,4 +84,12 @@ public record struct quaternion(float x, float y, float z, float w)
public static implicit operator quaternion(float4 value) => new(value.x, value.y, value.z, value.w); 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); public static implicit operator float4(quaternion value) => new(value.x, value.y, value.z, value.w);
public static bool operator ==(quaternion left, quaternion right) => left.Equals(right);
public static bool operator !=(quaternion left, quaternion right) => !left.Equals(right);
public readonly bool Equals(quaternion other) => x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z) && w.Equals(other.w);
public override readonly bool Equals(object? obj) => obj is quaternion other && Equals(other);
public override readonly int GetHashCode() => HashCode.Combine(x, y, z, w);
public override readonly string ToString() => FormattableString.Invariant($"quaternion({x:R}, {y:R}, {z:R}, {w:R})");
} }

View file

@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit.v3" Version="3.2.0" /> <PackageReference Include="xunit.v3" Version="3.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />

View file

@ -42,7 +42,8 @@ public sealed class HlslMirrorTests
"cultmath_cubic_bezier", "cultmath_cubic_bezier",
"cultmath_hash", "cultmath_hash",
"cultmath_value_noise", "cultmath_value_noise",
"cultmath_simplex_noise", "cultmath_snoise(float2",
"cultmath_snoise(float3",
"cultmath_value_noise_bicubic", "cultmath_value_noise_bicubic",
"cultmath_value_noise_texture", "cultmath_value_noise_texture",
"cultmath_value_noise_texture_bicubic", "cultmath_value_noise_texture_bicubic",

View file

@ -0,0 +1,441 @@
using System.Text.Json;
using Xunit;
namespace CultMath.Tests;
public sealed class HlslSemanticsTests
{
[Fact]
public void ComponentsAndIndexersAreWritable()
{
var value = new float4(1.0f, 2.0f, 3.0f, 4.0f);
value.w = 9.0f;
value[0] = -1.0f;
Assert.Equal(new float4(-1.0f, 2.0f, 3.0f, 9.0f), value);
var cell = new int3(1, 2, 3);
cell[2] = 7;
Assert.Equal(new int3(1, 2, 7), cell);
ref var component = ref value.y;
component = 20.0f;
Assert.Equal(20.0f, value.y);
}
[Fact]
public void Float2SwizzlesReadAndWrite()
{
var value = new float2(1.0f, 2.0f);
Assert.Equal(new float2(2.0f, 1.0f), value.yx);
value.yx = new float2(5.0f, 6.0f);
Assert.Equal(new float2(6.0f, 5.0f), value);
}
[Fact]
public void Float3SwizzlesReadAndWrite()
{
var source = new float3(1.0f, 2.0f, 3.0f);
Assert.Equal(new float2(1.0f, 2.0f), source.xy);
Assert.Equal(new float2(1.0f, 3.0f), source.xz);
Assert.Equal(new float2(2.0f, 1.0f), source.yx);
Assert.Equal(new float2(2.0f, 3.0f), source.yz);
Assert.Equal(new float2(3.0f, 2.0f), source.zy);
Assert.Equal(new float3(1.0f, 2.0f, 3.0f), source.xyz);
Assert.Equal(new float3(1.0f, 3.0f, 2.0f), source.xzy);
Assert.Equal(new float3(2.0f, 3.0f, 1.0f), source.yzx);
Assert.Equal(new float3(3.0f, 2.0f, 1.0f), source.zyx);
Assert.Equal(new float3(8.0f, 9.0f, 3.0f), Write(source, (ref float3 v) => v.xy = new float2(8.0f, 9.0f)));
Assert.Equal(new float3(8.0f, 2.0f, 9.0f), Write(source, (ref float3 v) => v.xz = new float2(8.0f, 9.0f)));
Assert.Equal(new float3(9.0f, 8.0f, 3.0f), Write(source, (ref float3 v) => v.yx = new float2(8.0f, 9.0f)));
Assert.Equal(new float3(1.0f, 8.0f, 9.0f), Write(source, (ref float3 v) => v.yz = new float2(8.0f, 9.0f)));
Assert.Equal(new float3(1.0f, 9.0f, 8.0f), Write(source, (ref float3 v) => v.zy = new float2(8.0f, 9.0f)));
Assert.Equal(new float3(7.0f, 8.0f, 9.0f), Write(source, (ref float3 v) => v.xyz = new float3(7.0f, 8.0f, 9.0f)));
Assert.Equal(new float3(7.0f, 9.0f, 8.0f), Write(source, (ref float3 v) => v.xzy = new float3(7.0f, 8.0f, 9.0f)));
Assert.Equal(new float3(9.0f, 7.0f, 8.0f), Write(source, (ref float3 v) => v.yzx = new float3(7.0f, 8.0f, 9.0f)));
Assert.Equal(new float3(9.0f, 8.0f, 7.0f), Write(source, (ref float3 v) => v.zyx = new float3(7.0f, 8.0f, 9.0f)));
}
[Fact]
public void Float4SwizzlesReadAndWrite()
{
var source = new float4(1.0f, 2.0f, 3.0f, 4.0f);
Assert.Equal(new float2(1.0f, 2.0f), source.xy);
Assert.Equal(new float3(1.0f, 2.0f, 3.0f), source.xyz);
Assert.Equal(new float3(1.0f, 2.0f, 4.0f), source.xyw);
Assert.Equal(new float3(2.0f, 3.0f, 1.0f), source.yzx);
var a = source;
a.xy = new float2(8.0f, 9.0f);
Assert.Equal(new float4(8.0f, 9.0f, 3.0f, 4.0f), a);
var b = source;
b.xyz = new float3(7.0f, 8.0f, 9.0f);
Assert.Equal(new float4(7.0f, 8.0f, 9.0f, 4.0f), b);
var c = source;
c.xyw = new float3(7.0f, 8.0f, 9.0f);
Assert.Equal(new float4(7.0f, 8.0f, 3.0f, 9.0f), c);
var d = source;
d.yzx = new float3(7.0f, 8.0f, 9.0f);
Assert.Equal(new float4(9.0f, 7.0f, 8.0f, 4.0f), d);
}
[Fact]
public void GeneratedSwizzlesCoverRepeatsColorNamesAndAllVectorFamilies()
{
var v = new float4(1.0f, 2.0f, 3.0f, 4.0f);
Assert.Equal(new float4(4.0f, 4.0f, 1.0f, 2.0f), v.wwxy);
Assert.Equal(new float3(3.0f, 2.0f, 1.0f), v.bgr);
Assert.Equal(4.0f, v.a);
Assert.Equal(new float4(2.0f, 2.0f, 2.0f, 2.0f), new float2(1.0f, 2.0f).yyyy);
v.wx = new float2(9.0f, 8.0f);
Assert.Equal(new float4(8.0f, 2.0f, 3.0f, 9.0f), v);
v.r = 0.5f;
v.gb += new float2(1.0f, 1.0f);
Assert.Equal(new float4(0.5f, 3.0f, 4.0f, 9.0f), v);
Assert.Equal(new int3(3, 1, 1), new int3(1, 2, 3).zxx);
Assert.Equal(new bool2(false, true), new bool4(true, false, false, true).zw);
Assert.Equal(new double2(2.0, 1.0), new double3(1.0, 2.0, 3.0).yx);
var cell = new int4(1, 2, 3, 4);
cell.xyz = new int3(7, 8, 9);
Assert.Equal(new int4(7, 8, 9, 4), cell);
}
[Fact]
public void MixedConstructorsMatchHlsl()
{
var xy = new float2(1.0f, 2.0f);
var zw = new float2(3.0f, 4.0f);
var expected = new float4(1.0f, 2.0f, 3.0f, 4.0f);
Assert.Equal(expected, math.float4(xy, zw));
Assert.Equal(expected, math.float4(1.0f, new float3(2.0f, 3.0f, 4.0f)));
Assert.Equal(expected, math.float4(new float3(1.0f, 2.0f, 3.0f), 4.0f));
Assert.Equal(expected, math.float4(xy, 3.0f, 4.0f));
Assert.Equal(expected, math.float4(1.0f, 2.0f, zw));
Assert.Equal(expected, math.float4(1.0f, new float2(2.0f, 3.0f), 4.0f));
Assert.Equal(expected, new float4(xy, zw));
Assert.Equal(new float3(1.0f, 2.0f, 3.0f), math.float3(1.0f, zw - 1.0f));
Assert.Equal(new int4(1, 2, 3, 4), math.int4(new int2(1, 2), new int2(3, 4)));
Assert.Equal(new bool3(true, false, true), math.bool3(true, new bool2(false, true)));
Assert.Equal(new double3(1.0, 2.0, 3.0), new double3(new double2(1.0, 2.0), 3.0));
}
[Fact]
public void MatrixRowElementsAreWritable()
{
var m = float3x3.identity;
m[1][2] = 5.0f;
m[2].x = 7.0f;
m._m00 = 2.0f;
Assert.Equal(5.0f, m._m12);
Assert.Equal(7.0f, m._m20);
Assert.Equal(math.float3x3(2.0f, 0.0f, 0.0f, 0.0f, 1.0f, 5.0f, 7.0f, 0.0f, 1.0f), m);
Assert.Equal(float3x3.identity, math.float3x3(1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f));
var r = float2x2.identity;
r[0][1] = 3.0f;
Assert.Equal(math.float2x2(1.0f, 3.0f, 0.0f, 1.0f), r);
Assert.Equal(new float2(0.0f, 1.0f), float2x2.identity[1]);
}
[Fact]
public void IntrinsicEdgeCasesFollowHlsl()
{
Assert.True(math.any(new float3(0.0f, 0.0f, -2.0f)));
Assert.False(math.any(float4.zero));
Assert.True(math.all(new int2(1, -1)));
Assert.False(math.all(new int3(1, 0, 1)));
Assert.True(math.any(new float2(float.NaN, 0.0f)));
Assert.Equal(new int3(1, 2, 3), math.min(new int3(1, 5, 3), new int3(4, 2, 6)));
Assert.Equal(new int2(4, 5), math.max(new int2(1, 5), new int2(4, 2)));
Assert.Equal(new int4(1, 2, 0, 3), math.abs(new int4(-1, 2, 0, -3)));
Assert.Equal(7, math.abs(-7));
Assert.Equal(1.0f, math.step(1.0f, float.NaN));
Assert.Equal(1.0f, math.step(float.NaN, 1.0f));
Assert.Equal(1.0f, math.step(1.0f, 1.0f));
// DXIL FMin/FMax/Saturate: a NaN operand returns the other; max(a, b) is a >= b ? a : b.
Assert.Equal(1.0f, math.min(float.NaN, 1.0f));
Assert.Equal(1.0f, math.min(1.0f, float.NaN));
Assert.Equal(1.0f, math.max(float.NaN, 1.0f));
Assert.Equal(1.0f, math.max(1.0f, float.NaN));
Assert.True(float.IsNaN(math.max(float.NaN, float.NaN)));
Assert.Equal(2.0f, math.clamp(float.NaN, 2.0f, 3.0f));
Assert.Equal(new float3(0.0f, 1.0f, 0.0f), math.saturate(new float3(float.NaN, 2.0f, -1.0f)));
Assert.Equal(new float2(-1.0f, 4.0f), math.max(new float2(float.NaN, 4.0f), new float2(-1.0f, float.NaN)));
static int Bits(float value) => BitConverter.SingleToInt32Bits(value);
Assert.Equal(Bits(-0.0f), Bits(math.max(-0.0f, 0.0f)));
Assert.Equal(Bits(0.0f), Bits(math.min(-0.0f, 0.0f)));
Assert.Equal(Bits(0.0f), Bits(math.saturate(-0.0f)));
Assert.Equal(1.0, math.min(double.NaN, 1.0));
Assert.Equal(1.0, math.min(1.0, double.NaN));
Assert.Equal(1.0, math.max(double.NaN, 1.0));
Assert.Equal(1.0, math.max(1.0, double.NaN));
Assert.True(double.IsNaN(math.max(double.NaN, double.NaN)));
static long DoubleBits(double value) => BitConverter.DoubleToInt64Bits(value);
Assert.Equal(DoubleBits(-0.0), DoubleBits(math.max(-0.0, 0.0)));
Assert.Equal(DoubleBits(0.0), DoubleBits(math.min(-0.0, 0.0)));
Assert.Equal(0, math.sign(float.NaN));
Assert.Equal(-1, math.sign(-0.5f));
Assert.Equal(new int3(-1, 0, 1), math.sign(new float3(-2.0f, 0.0f, 4.0f)));
Assert.Equal(new int2(1, -1), math.sign(new int2(9, -9)));
}
[Fact]
public void ComparisonOperatorsReturnComponentWiseBoolVectors()
{
var a = new float3(1.0f, 2.0f, 3.0f);
var b = new float3(3.0f, 2.0f, 1.0f);
Assert.Equal(new bool3(true, false, false), a < b);
Assert.Equal(new bool3(false, false, true), a > b);
Assert.Equal(new bool3(true, true, false), a <= b);
Assert.Equal(new bool3(false, true, true), a >= b);
Assert.Equal(new bool3(false, true, false), a == b);
Assert.Equal(new bool3(true, false, true), a != b);
Assert.Equal(new bool3(true, false, false), a < 2.0f);
Assert.Equal(new bool3(false, false, true), 2.0f < a);
Assert.Equal(new bool2(true, false), new float2(0.0f, 1.0f) == 0.0f);
Assert.Equal(new bool4(false, true, true, true), new float4(0.0f, 1.0f, 2.0f, 3.0f) >= 1.0f);
Assert.Equal(new bool2(false, true), new int2(1, 2) > new int2(1, 1));
Assert.Equal(new bool3(true, false, true), new int3(1, 2, 3) != new int3(0, 2, 4));
Assert.Equal(new bool4(true, true, false, false), new int4(1, 2, 3, 4) <= new int4(1, 2, 2, 2));
Assert.Equal(new bool2(true, false), new double2(0.5, 1.0) < new double2(1.0, 1.0));
Assert.Equal(new bool3(false, false, true), new double3(1.0, 1.0, 2.0) > new double3(1.0, 1.0, 1.0));
}
[Fact]
public void ScalarOperatorsApplyPerComponentOnBothSides()
{
var value = new float3(1.0f, 2.0f, 4.0f);
Assert.Equal(new float3(3.0f, 2.0f, 0.0f), 4.0f - value);
Assert.Equal(new float3(0.0f, 1.0f, 3.0f), value - 1.0f);
Assert.Equal(new float3(2.0f, 3.0f, 5.0f), 1.0f + value);
Assert.Equal(new float3(4.0f, 2.0f, 1.0f), 4.0f / value);
Assert.Equal(new float3(0.5f, 1.0f, 2.0f), value / 2.0f);
Assert.Equal(new float3(2.0f, 4.0f, 8.0f), 2.0f * value);
Assert.Equal(new float3(1.0f, 0.0f, 0.0f), value % 2.0f);
Assert.Equal(new float2(1.5f, 2.5f), new float2(1.0f, 2.0f) + 0.5f);
Assert.Equal(new float4(0.0f, 1.0f, 2.0f, 3.0f), new float4(1.0f, 2.0f, 3.0f, 4.0f) - 1.0f);
}
[Fact]
public void EqualsAndHashCodeStayValueBasedForDictionaryKeys()
{
var map = new Dictionary<float3, string> { [new float3(1.0f, 2.0f, 3.0f)] = "hit" };
Assert.Equal("hit", map[new float3(1.0f, 2.0f, 3.0f)]);
Assert.False(map.ContainsKey(new float3(1.0f, 2.0f, 3.5f)));
var cells = new HashSet<int2> { new int2(3, 4) };
Assert.Contains(new int2(3, 4), cells);
Assert.Contains(new int3(1, 2, 3), new HashSet<int3> { new int3(1, 2, 3) });
var nan = new float2(float.NaN, 0.0f);
var sameNan = nan;
Assert.True(nan.Equals(sameNan));
Assert.False((nan == sameNan).x);
Assert.True(new quaternion(0.0f, 0.0f, 0.0f, 1.0f) == quaternion.identity);
Assert.Equal(float2x2.identity, math.float2x2(1.0f, 0.0f, 0.0f, 1.0f));
Assert.Equal("float3(1, 2.5, -3)", new float3(1.0f, 2.5f, -3.0f).ToString());
}
[Fact]
public void AnyAllAndSelectFollowHlsl()
{
Assert.True(math.any(new bool2(false, true)));
Assert.False(math.any(bool3.@false));
Assert.True(math.all(bool4.@true));
Assert.False(math.all(new bool3(true, false, true)));
var mask = new bool3(true, false, true);
Assert.Equal(new float3(1.0f, 20.0f, 3.0f), math.select(mask, new float3(1.0f, 2.0f, 3.0f), new float3(10.0f, 20.0f, 30.0f)));
Assert.Equal(7.0f, math.select(true, 7.0f, 8.0f));
Assert.Equal(new float2(0.0f, 5.0f), math.select(new bool2(false, true), math.float2(5.0f), math.float2(0.0f)));
Assert.Equal(new float4(1.0f, 0.0f, 1.0f, 0.0f), math.select(new bool4(true, false, true, false), 1.0f, 0.0f));
Assert.Equal(new bool2(false, true), !new bool2(true, false));
Assert.Equal(new bool3(true, false, false), new bool3(true, true, false) & new bool3(true, false, true));
Assert.Equal(new bool3(true, true, true), new bool3(true, true, false) | new bool3(true, false, true));
}
[Fact]
public void AddedIntrinsicsAreComponentWise()
{
Assert.Equal(MathF.Log(3.0f), math.log(3.0f));
Assert.Equal(new float3(0.0f, MathF.Log(2.0f), MathF.Log(4.0f)), math.log(new float3(1.0f, 2.0f, 4.0f)));
Assert.Equal(new float2(8.0f, 9.0f), math.pow(new float2(2.0f, 3.0f), new float2(3.0f, 2.0f)));
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), math.sqrt(new float4(1.0f, 4.0f, 9.0f, 16.0f)));
var sines = math.sin(new float3(0.0f, math.HALF_PI, math.PI));
Assert.Equal(0.0f, sines.x, precision: 5);
Assert.Equal(1.0f, sines.y, precision: 5);
Assert.Equal(0.0f, sines.z, precision: 5);
var cosines = math.cos(new float2(0.0f, math.PI));
Assert.Equal(1.0f, cosines.x, precision: 5);
Assert.Equal(-1.0f, cosines.y, precision: 5);
var arccosines = math.acos(new float3(1.0f, 0.0f, -1.0f));
Assert.Equal(0.0f, arccosines.x, precision: 5);
Assert.Equal(math.HALF_PI, arccosines.y, precision: 5);
Assert.Equal(math.PI, arccosines.z, precision: 5);
Assert.Equal(math.atan2(1.0f, -1.0f), math.atan2(new float2(1.0f, 0.0f), new float2(-1.0f, 1.0f)).x);
Assert.Equal(0.0f, math.atan2(new float4(1.0f, 0.0f, 1.0f, 0.0f), new float4(1.0f, 1.0f, 1.0f, 1.0f)).y);
Assert.Equal(2.5, math.min(2.5, 3.5));
Assert.Equal(3.5, math.max(2.5, 3.5));
}
[Fact]
public void IntVectorsPromoteToFloatVectors()
{
float3 promoted3 = new int3(1, 2, 3);
float4 promoted4 = new int4(1, 2, 3, 4);
Assert.Equal(new float3(1.0f, 2.0f, 3.0f), promoted3);
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), promoted4);
Assert.Equal(new int3(2, 4, 6), new int3(1, 2, 3) * 2);
Assert.Equal(new int4(1, 0, 1, 0), new int4(3, 4, 5, 6) % 2);
Assert.Equal(new int3(1, 2, 3), math.int3(1, 2, 3));
Assert.Equal(new int4(5, 5, 5, 5), math.int4(5));
}
[Fact]
public void MatrixMulFollowsHlslRowMajorConvention()
{
var m = math.float2x2(1.0f, 2.0f, 3.0f, 4.0f);
var v = new float2(5.0f, 6.0f);
Assert.Equal(new float2(1.0f, 2.0f), m[0]);
Assert.Equal(new float2(17.0f, 39.0f), math.mul(m, v));
Assert.Equal(new float2(23.0f, 34.0f), math.mul(v, m));
Assert.Equal(math.float2x2(7.0f, 10.0f, 15.0f, 22.0f), math.mul(m, m));
var m3 = math.float3x3(new float3(1.0f, 2.0f, 3.0f), new float3(4.0f, 5.0f, 6.0f), new float3(7.0f, 8.0f, 9.0f));
var v3 = new float3(1.0f, 0.0f, -1.0f);
Assert.Equal(new float3(-2.0f, -2.0f, -2.0f), math.mul(m3, v3));
Assert.Equal(new float3(-6.0f, -6.0f, -6.0f), math.mul(v3, m3));
Assert.Equal(math.float3x3(30.0f, 36.0f, 42.0f, 66.0f, 81.0f, 96.0f, 102.0f, 126.0f, 150.0f), math.mul(m3, m3));
Assert.Equal(m3, math.mul(m3, float3x3.identity));
m3[1] = new float3(0.0f, 1.0f, 0.0f);
Assert.Equal(1.0f, m3._m11);
Assert.Equal(12.0f, math.mul(3.0f, 4.0f));
}
[Fact]
public void Float2x2RotateMatchesRotateAndUnityMathematics()
{
var rotation = float2x2.Rotate(0.5f);
var column = math.mul(rotation, new float2(1.0f, 0.0f));
var row = math.mul(new float2(1.0f, 0.0f), rotation);
var reference = math.rotate(new float2(1.0f, 0.0f), 0.5f);
Assert.Equal(reference.x, column.x, precision: 6);
Assert.Equal(reference.y, column.y, precision: 6);
// Unity.Mathematics 1.x reference values: mul(float2(1, 0), float2x2.Rotate(0.5f)).
Assert.Equal(0.87758255f, row.x, precision: 6);
Assert.Equal(-0.47942555f, row.y, precision: 6);
}
[Fact]
public void Float3x3EulerMatchesUnityMathematicsReference()
{
// Unity.Mathematics 1.x reference values captured from float3x3.Euler.
AssertRows(
float3x3.Euler(new float3(0.4f, -0.7f, 1.3f), math.RotationOrder.YXZ),
0.44632244f, -0.8874959f, 0.114662126f,
0.6698625f, 0.24638279f, -0.700414f,
0.59336376f, 0.38941833f, 0.70446634f);
AssertRows(
float3x3.Euler(new float3(0.4f, -0.7f, 1.3f)),
-0.037133574f, -0.8040775f, -0.59336376f,
0.8874959f, 0.24638279f, -0.38941833f,
0.45931715f, -0.5410684f, 0.70446634f);
// Aetheria ActionGameManager view direction: mul(float3(0, 0, 1), Euler(float3(yawPitch.yx, 0), YXZ)).
var yawPitch = new float2(0.3f, -1.1f);
var view = math.mul(new float3(0.0f, 0.0f, 1.0f), float3x3.Euler(new float3(yawPitch.yx, 0.0f), math.RotationOrder.YXZ));
Assert.Equal(-0.13404681f, view.x, precision: 5);
Assert.Equal(-0.8912074f, view.y, precision: 5);
Assert.Equal(0.4333369f, view.z, precision: 5);
var composed = math.mul(float3x3.RotateZ(0.2f), math.mul(float3x3.RotateY(0.3f), float3x3.RotateX(0.1f)));
AssertRows(float3x3.Euler(0.1f, 0.3f, 0.2f, math.RotationOrder.XYZ),
composed._m00, composed._m01, composed._m02,
composed._m10, composed._m11, composed._m12,
composed._m20, composed._m21, composed._m22);
}
[Theory]
[InlineData(0.0f, 0.0f, 0.0f, -0.41219875f)]
[InlineData(0.25f, -0.5f, 1.75f, 0.5250269f)]
[InlineData(12.25f, -4.5f, 3.125f, 0.008141451f)]
[InlineData(-7.3f, 2.9f, 101.4f, 0.578015f)]
public void Snoise3MatchesUnityMathematicsReference(float x, float y, float z, float expected)
{
Assert.Equal(expected, math.snoise(new float3(x, y, z)), precision: 5);
}
[Fact]
public void RandomFloat3OverloadsShareOneDrawOrder()
{
var a = new Random(99);
var b = new Random(99);
var c = new Random(99);
var unit = a.NextFloat3();
Assert.Equal(new float3(b.NextFloat(), b.NextFloat(), b.NextFloat()), unit);
Assert.Equal(unit * new float3(2.0f, 4.0f, 8.0f), c.NextFloat3(new float3(2.0f, 4.0f, 8.0f)));
var d = new Random(99);
Assert.Equal(math.float3(-1.0f) + unit * 2.0f, d.NextFloat3(math.float3(-1.0f), math.float3(1.0f)));
}
[Fact]
public void CoreAssemblyStaysEngineFree()
{
var references = typeof(math).Assembly.GetReferencedAssemblies();
Assert.DoesNotContain(references, reference => reference.Name!.StartsWith("UnityEngine", StringComparison.Ordinal));
var root = new DirectoryInfo(AppContext.BaseDirectory);
while (root is not null && !File.Exists(Path.Combine(root.FullName, "packages", "cultmath", "src", "CultMath", "CultMath.asmdef")))
{
root = root.Parent;
}
Assert.NotNull(root);
var asmdefPath = Path.Combine(root!.FullName, "packages", "cultmath", "src", "CultMath", "CultMath.asmdef");
using var asmdef = JsonDocument.Parse(File.ReadAllText(asmdefPath));
Assert.True(asmdef.RootElement.GetProperty("noEngineReferences").GetBoolean());
}
private static float3 Write(float3 value, WriteAction action)
{
action(ref value);
return value;
}
private delegate void WriteAction(ref float3 value);
private static void AssertRows(float3x3 m, params float[] expected)
{
var actual = new[] { m._m00, m._m01, m._m02, m._m10, m._m11, m._m12, m._m20, m._m21, m._m22 };
for (var index = 0; index < expected.Length; index++)
{
Assert.Equal(expected[index], actual[index], precision: 5);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7164d00a023e43578a31897347d8f106

View file

@ -0,0 +1,203 @@
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static CultMath.math;
namespace CultMath.Tests;
/// <summary>
/// The real shader mirror, <c>shaders/CultMath.hlsl</c>, compiles as C# against CultMath under
/// <c>using static CultMath.math;</c> after exactly the source transformations listed in
/// docs/design.md (HLSL Target: Source Transformations). <see cref="TransformHlslToCSharp"/> is
/// that list in code; if a body needs anything else, this test fails.
/// </summary>
public sealed class HlslSourceCompatibilityTests
{
[Fact]
public void ShaderMirrorBodiesCompileAsCSharp()
{
var (assembly, errors) = CompileShaderMirror();
Assert.True(assembly is not null, "HLSL mirror did not compile as C#:" + Environment.NewLine + string.Join(Environment.NewLine, errors));
var shader = assembly!.GetType("CultMathHlsl.HlslShader")!;
Assert.NotNull(ShaderFunction(shader, "cultmath_snoise", typeof(float3)));
Assert.NotNull(ShaderFunction(shader, "cultmath_snoise", typeof(float2)));
Assert.NotNull(ShaderFunction(shader, "cultmath_value_noise_bicubic", typeof(float2)));
}
// HLSL functions carry no access modifier, so they compile as private instance methods.
private static MethodInfo? ShaderFunction(Type shader, string name, params Type[] parameters) =>
shader.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null);
// Mirror internals with no public C# math counterpart. Any other cultmath_* function must match one.
private static readonly string[] MirrorOnly = { "cultmath_snoise_mod289", "cultmath_snoise_permute" };
private static readonly float[] Specials = { 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -2.75f, 3.0f, 1.0e-7f, 1.0e4f, -1.0e4f, 1.0e7f, float.NaN };
/// <summary>
/// Every mirror function with a C# math counterpart returns the same bits (any NaN equals any NaN;
/// -0 differs from 0) over random inputs, every special value in every argument (ties, zeros,
/// negatives, NaN, large values, integer lattice points), special pairs, and vectors whose first two
/// components tie (x0.x == x0.y in snoise). This proves the text of CultMath.hlsl computes the same
/// float32 results as C# math on the CPU. It does not prove GPU agreement: driver sin precision alone
/// makes cultmath_hash and value noise differ bit for bit on hardware. The mirror's intrinsics are C#
/// math itself (it compiles under using static CultMath.math), so this proves the composition in the
/// text, not the intrinsic rules; HlslSemanticsTests alone pins those.
/// </summary>
[Fact]
public void EveryMirrorFunctionMatchesCSharpMath()
{
var (assembly, errors) = CompileShaderMirror();
Assert.True(assembly is not null, string.Join(Environment.NewLine, errors));
var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!;
var shader = Activator.CreateInstance(shaderType);
var random = new System.Random(0x5EED);
float Next() => random.NextSingle() * 200.0f - 100.0f;
var cases = new List<Func<int, float>>();
foreach (var s in Specials) cases.Add(_ => s);
for (var k = 0; k < Specials.Length; k++) { var o = k; cases.Add(i => Specials[(i + o + 1) % Specials.Length]); }
for (var k = 0; k < 64; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i]); }
for (var k = 0; k < 16; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i & ~1]); }
var mismatches = new List<string>();
var compared = new HashSet<string>();
foreach (var mirror in shaderType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Where(m => m.Name.StartsWith("cultmath_")))
{
var types = mirror.GetParameters().Select(p => p.ParameterType).ToArray();
var counterpart = typeof(math).GetMethod(mirror.Name["cultmath_".Length..], BindingFlags.Public | BindingFlags.Static, null, types, null);
if (counterpart is null)
{
Assert.Contains(mirror.Name, MirrorOnly);
continue;
}
compared.Add(mirror.Name);
foreach (var input in cases)
{
var slot = 0;
var args = types.Select(t => t == typeof(float) ? (object)input(slot++)
: Activator.CreateInstance(t, Enumerable.Range(0, Components(t)).Select(_ => (object)input(slot++)).ToArray())!).ToArray();
var expected = Floats(counterpart.Invoke(null, args)!);
var actual = Floats(mirror.Invoke(shader, args)!);
if (expected.Length != actual.Length || expected.Zip(actual).Any(p => !(float.IsNaN(p.First) && float.IsNaN(p.Second))
&& BitConverter.SingleToInt32Bits(p.First) != BitConverter.SingleToInt32Bits(p.Second)))
{
mismatches.Add($"{mirror.Name}({string.Join(", ", args.Select(a => string.Join(" ", Floats(a))))}): C# {string.Join(" ", expected)}, mirror {string.Join(" ", actual)}");
}
}
}
Assert.True(mismatches.Count == 0, $"{mismatches.Count} mismatches:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}");
Assert.Equal(26, compared.Count);
}
private static int Components(Type t) => t.GetFields().Count(f => f.FieldType == typeof(float) && !f.IsStatic);
private static float[] Floats(object value) => value is float f ? new[] { f }
: value.GetType().GetFields().Where(field => field.FieldType == typeof(float) && !field.IsStatic).Select(field => (float)field.GetValue(value)!).ToArray();
/// <summary>The documented HLSL-to-C# transformations, and nothing else.</summary>
internal static string TransformHlslToCSharp(string hlsl)
{
// 1. Include guards (#ifndef/#define/#endif) are dropped; C# has no textual include.
var source = Regex.Replace(hlsl, @"(?m)^\s*#(ifndef|define|endif)\b.*$", string.Empty);
// 2. Functions taking Texture2D/SamplerState resources are dropped; GPU resources have
// no CultMath analog.
source = Regex.Replace(source, @"(?ms)^\w+ \w+\([^)]*\b(Texture2D|SamplerState)\b[^)]*\)\s*\{.*?^\}[ \t]*\r?$", string.Empty);
// 3. File-scope `static const` becomes `const` (C# consts are implicitly static).
source = Regex.Replace(source, @"(?m)^static const ", "const ");
// 4. Floating literals gain the `f` suffix; an unsuffixed C# literal is a double.
source = Regex.Replace(source, @"(?<![\w.])(\d+\.\d*(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+)(?![\w.])", "$1f");
// 5. The file body is wrapped in a class; C# has no free functions.
return "using CultMath;\nusing static CultMath.math;\nnamespace CultMathHlsl;\npublic class HlslShader\n{\n" + source + "\n}\n";
}
private static (Assembly? Assembly, IReadOnlyList<string> Errors) CompileShaderMirror()
{
var hlsl = File.ReadAllText(Path.Combine(FindCultMathRoot(), "shaders", "CultMath.hlsl"));
var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl));
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
.Split(Path.PathSeparator)
.Append(typeof(math).Assembly.Location)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(path => MetadataReference.CreateFromFile(path));
var compilation = CSharpCompilation.Create(
"CultMathHlslMirror",
new[] { tree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using var stream = new MemoryStream();
var result = compilation.Emit(stream);
var errors = result.Diagnostics
.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.Select(diagnostic => diagnostic.ToString())
.ToList();
return (result.Success ? Assembly.Load(stream.ToArray()) : null, errors);
}
private static string FindCultMathRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "packages", "cultmath", "shaders", "CultMath.hlsl")))
{
directory = directory.Parent;
}
return Path.Combine(directory?.FullName ?? throw new InvalidOperationException("CultMath package root not found."), "packages", "cultmath");
}
// ---- Hand-written HLSL-shaped host code: swizzles, mixed constructors, matrix rows. ----
static float3 shade(float3 position, float3 normal, float2 wind, float time)
{
float3 p = position;
p.xz = p.xz + wind * time;
p.y = saturate(p.y);
float3x3 basis = float3x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1));
basis[1][1] = 1.0f;
float3 n = normalize(mul(basis, normal));
float ndotl = saturate(dot(n, normalize(float3(0.3f, 1, 0.2f))));
float4 color = float4(lerp(float3(0.1f, 0.1f, 0.2f), float3(1, 0.9f, 0.7f), ndotl), 1);
if (any(p < 0.0f))
color.rgb *= 0.5f;
if (all(abs(n) <= 1.0f))
color.bgr = color.bgr * float3(1, 1, 1);
color.rgb = select(p > 10.0f, float3(0), color.rgb);
float2 uv = frac(p.xz * 0.25f);
color.rg = lerp(color.rg, uv, 0.1f);
return float4(color.xy, color.zw).xyz;
}
static float2 steer(float2 direction, float torque)
{
float2x2 turn = float2x2(cos(torque), -sin(torque), sin(torque), cos(torque));
float2 turned = mul(direction, turn);
return normalize(turned) * pow(length(turned), 1.0f);
}
[Fact]
public void HandWrittenShaderStyleMathEvaluates()
{
var color = shade(float3(1.0f, 0.5f, 2.0f), float3(0.0f, 1.0f, 0.0f), float2(1.0f, 0.0f), 0.0f);
Assert.True(color.x > 0.1f && color.x <= 1.0f);
Assert.True(float.IsFinite(color.y) && float.IsFinite(color.z));
var far = shade(float3(20.0f, 20.0f, 20.0f), float3(0.0f, 1.0f, 0.0f), float2(0.0f), 0.0f);
Assert.Equal(0.0f, far.z);
var steered = steer(float2(1.0f, 0.0f), 0.5f);
var viaRotate = mul(float2(1.0f, 0.0f), CultMath.float2x2.Rotate(0.5f));
Assert.Equal(viaRotate.x, steered.x, precision: 5);
Assert.Equal(viaRotate.y, steered.y, precision: 5);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 942fbcb9c28a4e559e5baf435dc24cf1

View file

@ -33,7 +33,7 @@ public sealed class MathTests
} }
[Fact] [Fact]
public void VectorsExposeCommonReadOnlySwizzles() public void VectorsExposeCommonSwizzles()
{ {
Assert.Equal(new float3(2.0f, 3.0f, 1.0f), new float3(1.0f, 2.0f, 3.0f).yzx); 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 float3(3.0f, 2.0f, 1.0f), new float3(1.0f, 2.0f, 3.0f).zyx);
@ -170,10 +170,10 @@ public sealed class MathTests
[Fact] [Fact]
public void SignIsComponentWiseForSimulationVectors() public void SignIsComponentWiseForSimulationVectors()
{ {
Assert.Equal(-1.0f, math.sign(-12.0f)); Assert.Equal(-1, math.sign(-12.0f));
Assert.Equal(0.0f, math.sign(0.0f)); Assert.Equal(0, math.sign(0.0f));
Assert.Equal(1.0f, math.sign(12.0f)); Assert.Equal(1, math.sign(12.0f));
Assert.Equal(new float3(-1.0f, 0.0f, 1.0f), math.sign(new float3(-2.0f, 0.0f, 4.0f))); Assert.Equal(new int3(-1, 0, 1), math.sign(new float3(-2.0f, 0.0f, 4.0f)));
} }
[Fact] [Fact]
@ -277,7 +277,7 @@ public sealed class MathTests
[InlineData(536.5106f, 536.5106f, -0.6901103f)] [InlineData(536.5106f, 536.5106f, -0.6901103f)]
public void SimplexNoiseMatchesUnityMathematics(float x, float y, float expected) public void SimplexNoiseMatchesUnityMathematics(float x, float y, float expected)
{ {
Assert.Equal(expected, math.simplex_noise(new float2(x, y)), precision: 6); Assert.Equal(expected, math.snoise(new float2(x, y)), precision: 6);
} }
[Fact] [Fact]

View file

@ -0,0 +1,132 @@
param(
[string]$OutputPath = ""
)
# Writes src\CultMath\Swizzles.g.cs: HLSL swizzles and mixed constructors for
# CultMath's vector types. The output is checked in; rerun after changing the
# family table below and commit both files together.
$ErrorActionPreference = "Stop"
if (-not $OutputPath) {
$OutputPath = Join-Path $PSScriptRoot "..\src\CultMath\Swizzles.g.cs"
}
# Functions: emit math.<type>(...) constructor functions. double has none because
# a math.double3 method group would hide the double3 type name under
# `using static CultMath.math;` and HLSL shader code does not construct doubles.
$families = @(
@{ Scalar = "float"; Sizes = @(2, 3, 4); Functions = $true },
@{ Scalar = "int"; Sizes = @(2, 3, 4); Functions = $true },
@{ Scalar = "bool"; Sizes = @(2, 3, 4); Functions = $true },
@{ Scalar = "double"; Sizes = @(2, 3); Functions = $false }
)
$componentFields = @("x", "y", "z", "w")
$namings = @(@("x", "y", "z", "w"), @("r", "g", "b", "a"))
function Get-Compositions([int]$total) {
# Ordered part sizes summing to $total, excluding the all-scalar form that the
# hand-written base constructor already owns.
$results = [System.Collections.Generic.List[object]]::new()
function Expand([int]$remaining, [int[]]$prefix) {
if ($remaining -eq 0) {
if (@($prefix | Where-Object { $_ -ne 1 }).Count -gt 0) {
$results.Add($prefix)
}
return
}
for ($part = 1; $part -le $remaining; $part++) {
Expand ($remaining - $part) ($prefix + $part)
}
}
Expand $total @()
return , $results
}
$builder = [System.Text.StringBuilder]::new()
function Line([string]$text = "") { [void]$builder.Append($text).Append("`n") }
Line "// <auto-generated>"
Line "// Generated by tools/generate-swizzles.ps1. Do not edit by hand."
Line "// </auto-generated>"
Line ""
Line "namespace CultMath;"
$functionLines = [System.Collections.Generic.List[string]]::new()
foreach ($family in $families) {
$scalar = $family.Scalar
$maxSize = ($family.Sizes | Measure-Object -Maximum).Maximum
foreach ($size in $family.Sizes) {
$type = "$scalar$size"
Line ""
Line "public partial struct $type"
Line "{"
foreach ($composition in (Get-Compositions $size)) {
$parameters = @()
$arguments = @()
$offset = 0
foreach ($part in $composition) {
$name = -join $componentFields[$offset..($offset + $part - 1)]
if ($part -eq 1) {
$parameters += "$scalar $name"
$arguments += $name
} else {
$parameters += "$scalar$part $name"
for ($i = 0; $i -lt $part; $i++) { $arguments += "$name.$($componentFields[$i])" }
}
$offset += $part
}
$parameterList = $parameters -join ", "
Line " public $type($parameterList) : this($($arguments -join ', ')) { }"
if ($family.Functions) {
$names = @($parameters | ForEach-Object { ($_ -split " ")[1] })
$functionLines.Add(" public static $type $type($parameterList) => new($($names -join ', '));")
}
}
# Single-component color aliases; x/y/z/w are the fields themselves.
$letters = $namings[1]
for ($i = 0; $i -lt $size; $i++) {
Line " public $scalar $($letters[$i]) { readonly get => $($componentFields[$i]); set => $($componentFields[$i]) = value; }"
}
foreach ($letters in $namings) {
for ($length = 2; $length -le $maxSize; $length++) {
$count = [math]::Pow($size, $length)
for ($index = 0; $index -lt $count; $index++) {
[int[]]$digits = @()
[int]$remainder = $index
for ($d = 0; $d -lt $length; $d++) {
$digits = @([int]($remainder % $size)) + $digits
$remainder = [int][math]::Floor($remainder / $size)
}
$name = -join ($digits | ForEach-Object { $letters[$_] })
$getter = ($digits | ForEach-Object { $componentFields[$_] }) -join ", "
$resultType = "$scalar$length"
$unique = @($digits | Select-Object -Unique).Count -eq $length
if ($unique) {
$assignments = for ($d = 0; $d -lt $length; $d++) { "$($componentFields[$digits[$d]]) = value.$($componentFields[$d]);" }
Line " public $resultType $name { readonly get => new($getter); set { $($assignments -join ' ') } }"
} else {
Line " public readonly $resultType $name => new($getter);"
}
}
}
}
Line "}"
}
}
Line ""
Line "public static partial class math"
Line "{"
foreach ($functionLine in $functionLines) { Line $functionLine }
Line "}"
$fullPath = [System.IO.Path]::GetFullPath($OutputPath)
$content = $builder.ToString() -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($fullPath, $content, [System.Text.UTF8Encoding]::new($false))
Write-Host "Wrote $fullPath ($((Get-Item -LiteralPath $fullPath).Length) bytes)"

View file

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

View file

@ -8,26 +8,27 @@ $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$installPath = Join-Path $repoRoot $InstallRoot $installPath = Join-Path $repoRoot $InstallRoot
New-Item -ItemType Directory -Force -Path $installPath | Out-Null New-Item -ItemType Directory -Force -Path $installPath | Out-Null
$release = Invoke-RestMethod ` # Pinned: parity means this compiler's lowering (dxcompiler 1.9.0.5402). The hash is the SHA256
-Uri "https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/latest" ` # of dxc_2026_07_29.zip downloaded from Microsoft's GitHub release v1.9.2607, and matches the
-Headers @{ "User-Agent" = "CultMath-DXC-Bootstrap" } # digest GitHub publishes for that asset.
$zipName = "dxc_2026_07_29.zip"
$zipUrl = "https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.9.2607/$zipName"
$zipSha256 = "A1DFB116BA3EEAE6A1582291B53A8E7BF65AD760676BD3194685C8F7367CD241"
$asset = $release.assets | $zipPath = Join-Path $installPath $zipName
Where-Object { $_.name -like "dxc_*.zip" } |
Select-Object -First 1
if (-not $asset) {
throw "No Windows dxc_*.zip asset found on the latest DirectXShaderCompiler release."
}
$zipPath = Join-Path $installPath $asset.name
Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue
& curl.exe -L --retry 5 --retry-delay 2 --fail -o $zipPath $asset.browser_download_url & curl.exe -L --retry 5 --retry-delay 2 --fail -o $zipPath $zipUrl
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "curl.exe failed with exit code $LASTEXITCODE." throw "curl.exe failed with exit code $LASTEXITCODE."
} }
$actualSha256 = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash
if ($actualSha256 -ne $zipSha256) {
Remove-Item -LiteralPath $zipPath -Force
throw "$zipName SHA256 is $actualSha256, expected $zipSha256."
}
$currentPath = Join-Path $installPath "current" $currentPath = Join-Path $installPath "current"
Remove-Item -LiteralPath $currentPath -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $currentPath -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $currentPath | Out-Null New-Item -ItemType Directory -Force -Path $currentPath | Out-Null

View file

@ -1,8 +1,16 @@
# CultMath for Unity # CultMath for Unity
This is CultMath's Unity 2021.3-compatible Git-UPM surface. It contains the This is CultMath's Unity 2021.3-compatible Git-UPM surface. It contains the
precompiled `CultMath.dll`, portable symbols, and the numeric precompiled `CultMath.dll`, portable symbols, the numeric
`Shaders/CultMath.hlsl` mirror. `Shaders/CultMath.hlsl` mirror, and the `CultMath.UnityBridge` assembly.
`CultMath.UnityBridge` is the only C# source Unity compiles here. It holds the
UnityEngine conversions the engine-free core cannot declare: `ToCultMath()` on
`Vector2/3/4`, `Vector2Int/3Int`, `Quaternion`, and `Color`; `ToUnity()` on
`float2/3/4`, `int2/3`, and `quaternion`; `ToColor()` on `float4`. C# only
admits conversion operators on the source or target type, so these are
extension methods rather than implicit casts. Import `CultMath.UnityBridge` to
use them.
Unity does not compile CultMath's repository source tree. That source uses the Unity does not compile CultMath's repository source tree. That source uses the
current C# language and remains owned by the normal .NET project. The package current C# language and remains owned by the normal .NET project. The package

View file

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

View file

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

View file

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

View file

@ -0,0 +1,28 @@
using UnityEngine;
namespace CultMath.UnityBridge
{
/// <summary>
/// Explicit conversions between UnityEngine and CultMath value types.
/// C# only admits user-defined conversion operators declared on the source or target
/// type, and CultMath's core types stay engine-free, so the bridge is extension methods.
/// </summary>
public static class UnityConversions
{
public static float2 ToCultMath(this Vector2 value) => new float2(value.x, value.y);
public static float3 ToCultMath(this Vector3 value) => new float3(value.x, value.y, value.z);
public static float4 ToCultMath(this Vector4 value) => new float4(value.x, value.y, value.z, value.w);
public static float4 ToCultMath(this Color value) => new float4(value.r, value.g, value.b, value.a);
public static quaternion ToCultMath(this Quaternion value) => new quaternion(value.x, value.y, value.z, value.w);
public static int2 ToCultMath(this Vector2Int value) => new int2(value.x, value.y);
public static int3 ToCultMath(this Vector3Int value) => new int3(value.x, value.y, value.z);
public static Vector2 ToUnity(this float2 value) => new Vector2(value.x, value.y);
public static Vector3 ToUnity(this float3 value) => new Vector3(value.x, value.y, value.z);
public static Vector4 ToUnity(this float4 value) => new Vector4(value.x, value.y, value.z, value.w);
public static Quaternion ToUnity(this quaternion value) => new Quaternion(value.x, value.y, value.z, value.w);
public static Vector2Int ToUnity(this int2 value) => new Vector2Int(value.x, value.y);
public static Vector3Int ToUnity(this int3 value) => new Vector3Int(value.x, value.y, value.z);
public static Color ToColor(this float4 value) => new Color(value.x, value.y, value.z, value.w);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8fc742177b3f4715b204afe0e69a400c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -26,10 +26,11 @@ float2 cultmath_clamp(float2 value, float2 minimum, float2 maximum) { return min
float3 cultmath_clamp(float3 value, float3 minimum, float3 maximum) { return min(max(value, minimum), maximum); } float3 cultmath_clamp(float3 value, float3 minimum, float3 maximum) { return min(max(value, minimum), maximum); }
float4 cultmath_clamp(float4 value, float4 minimum, float4 maximum) { return min(max(value, minimum), maximum); } float4 cultmath_clamp(float4 value, float4 minimum, float4 maximum) { return min(max(value, minimum), maximum); }
float cultmath_saturate(float value) { return cultmath_clamp(value, 0.0, 1.0); } // DXIL Saturate is FMin(1, FMax(0, x)); dxc lowers min/max to the same FMin/FMax, NaN rules included.
float2 cultmath_saturate(float2 value) { return cultmath_clamp(value, float2(0.0, 0.0), float2(1.0, 1.0)); } float cultmath_saturate(float value) { return min(1.0, max(0.0, value)); }
float3 cultmath_saturate(float3 value) { return cultmath_clamp(value, float3(0.0, 0.0, 0.0), float3(1.0, 1.0, 1.0)); } float2 cultmath_saturate(float2 value) { return min(float2(1.0, 1.0), max(float2(0.0, 0.0), value)); }
float4 cultmath_saturate(float4 value) { return cultmath_clamp(value, float4(0.0, 0.0, 0.0, 0.0), float4(1.0, 1.0, 1.0, 1.0)); } float3 cultmath_saturate(float3 value) { return min(float3(1.0, 1.0, 1.0), max(float3(0.0, 0.0, 0.0), value)); }
float4 cultmath_saturate(float4 value) { return min(float4(1.0, 1.0, 1.0, 1.0), max(float4(0.0, 0.0, 0.0, 0.0), value)); }
float cultmath_lerp(float start, float end, float amount) { return start + (end - start) * amount; } float cultmath_lerp(float start, float end, float amount) { return start + (end - start) * amount; }
float2 cultmath_lerp(float2 start, float2 end, float2 amount) { return start + (end - start) * amount; } float2 cultmath_lerp(float2 start, float2 end, float2 amount) { return start + (end - start) * amount; }
@ -73,13 +74,66 @@ float cultmath_smootherstep(float value)
return value * value * value * (value * (value * 6.0 - 15.0) + 10.0); return value * value * value * (value * (value * 6.0 - 15.0) + 10.0);
} }
float2 cultmath_simplex_mod289(float2 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } float2 cultmath_snoise_mod289(float2 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
float3 cultmath_simplex_mod289(float3 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; } float3 cultmath_snoise_mod289(float3 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
float3 cultmath_simplex_permute(float3 value) { return cultmath_simplex_mod289(((value * 34.0) + 1.0) * value); } float4 cultmath_snoise_mod289(float4 value) { return value - floor(value * (1.0 / 289.0)) * 289.0; }
float3 cultmath_snoise_permute(float3 value) { return cultmath_snoise_mod289(((value * 34.0) + 1.0) * value); }
float4 cultmath_snoise_permute(float4 value) { return cultmath_snoise_mod289(((value * 34.0) + 1.0) * value); }
float cultmath_simplex_noise(float2 value) // Ashima Arts / Ian McEwan 3D simplex noise (MIT), same float32 evaluation
// order as the C# math.snoise(float3) mirror.
float cultmath_snoise(float3 value)
{ {
const float4 c = float4( float2 c = float2(1.0 / 6.0, 1.0 / 3.0);
float3 i = floor(value + dot(value, c.yyy));
float3 x0 = value - i + dot(i, c.xxx);
float3 g = step(x0.yzx, x0.xyz);
float3 l = 1.0 - g;
float3 i1 = min(g.xyz, l.zxy);
float3 i2 = max(g.xyz, l.zxy);
float3 x1 = x0 - i1 + c.xxx;
float3 x2 = x0 - i2 + c.yyy;
float3 x3 = x0 - 0.5;
i = cultmath_snoise_mod289(i);
float4 p = cultmath_snoise_permute(cultmath_snoise_permute(cultmath_snoise_permute(
i.z + float4(0.0, i1.z, i2.z, 1.0)) + i.y + float4(0.0, i1.y, i2.y, 1.0)) + i.x + float4(0.0, i1.x, i2.x, 1.0));
const float n_ = 0.142857142857;
float3 ns = float3(2.0 * n_, 0.5 * n_ - 1.0, n_);
float4 j = p - 49.0 * floor(p * ns.z * ns.z);
float4 x_ = floor(j * ns.z);
float4 y_ = floor(j - 7.0 * x_);
float4 x = x_ * ns.x + ns.yyyy;
float4 y = y_ * ns.x + ns.yyyy;
float4 h = 1.0 - abs(x) - abs(y);
float4 b0 = float4(x.xy, y.xy);
float4 b1 = float4(x.zw, y.zw);
float4 s0 = floor(b0) * 2.0 + 1.0;
float4 s1 = floor(b1) * 2.0 + 1.0;
float4 sh = -step(h, 0.0);
float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
float4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
float3 p0 = float3(a0.xy, h.x);
float3 p1 = float3(a0.zw, h.y);
float3 p2 = float3(a1.xy, h.z);
float3 p3 = float3(a1.zw, h.w);
float4 norm = 1.79284291400159 - 0.85373472095314 * float4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
float4 m = max(0.6 - float4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
m = m * m;
return 42.0 * dot(m * m, float4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}
float cultmath_snoise(float2 value)
{
float4 c = float4(
0.211324865405187, 0.211324865405187,
0.366025403784439, 0.366025403784439,
-0.577350269189626, -0.577350269189626,
@ -89,9 +143,9 @@ float cultmath_simplex_noise(float2 value)
float2 i1 = x0.x > x0.y ? float2(1.0, 0.0) : float2(0.0, 1.0); float2 i1 = x0.x > x0.y ? float2(1.0, 0.0) : float2(0.0, 1.0);
float4 x12 = float4(x0.x + c.x - i1.x, x0.y + c.x - i1.y, x0.x + c.z, x0.y + c.z); float4 x12 = float4(x0.x + c.x - i1.x, x0.y + c.x - i1.y, x0.x + c.z, x0.y + c.z);
i = cultmath_simplex_mod289(i); i = cultmath_snoise_mod289(i);
float3 p = cultmath_simplex_permute( float3 p = cultmath_snoise_permute(
cultmath_simplex_permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0)); cultmath_snoise_permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0));
float3 m = max(0.5 - float3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); float3 m = max(0.5 - float3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
m *= m; m *= m;
m *= m; m *= m;

View file

@ -1,9 +1,9 @@
# Third-Party Notices # Third-Party Notices
## 2D Simplex Noise ## 2D and 3D Simplex Noise
`math.simplex_noise` and `cultmath_simplex_noise` port the textureless 2D `math.snoise` and `cultmath_snoise` port the textureless 2D and 3D
simplex-noise function by Ian McEwan and Ashima Arts, as distributed by Stefan simplex-noise functions by Ian McEwan and Ashima Arts, as distributed by Stefan
Gustavson's `webgl-noise` project and Unity Mathematics. Gustavson's `webgl-noise` project and Unity Mathematics.
- Copyright 2011 Ashima Arts. - Copyright 2011 Ashima Arts.

View file

@ -1,7 +1,7 @@
{ {
"name": "org.gamecult.cultmath", "name": "org.gamecult.cultmath",
"displayName": "CultMath", "displayName": "CultMath",
"version": "0.1.2", "version": "0.2.0",
"unity": "2021.3", "unity": "2021.3",
"description": "Precompiled CultMath numeric primitives and matching HLSL semantics for GameCult Unity runtimes.", "description": "Precompiled CultMath numeric primitives and matching HLSL semantics for GameCult Unity runtimes.",
"license": "MPL-2.0", "license": "MPL-2.0",