Merge hands/cultmath-release-0.4.0: org.gamecult.cultmath 0.4.0

Cut cultmath-release (campaign cultlib-gaps). Soul passes 1-4; the last docs edit is the one pass 4 prescribed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 00:13:10 +02:00
commit 252e226c51
16 changed files with 448 additions and 12 deletions

View file

@ -111,7 +111,7 @@ swap under "Changed" rather than "Breaking".
## Pre-1.0 packages
A package at `0.y.z` (currently `org.gamecult.cultmath`, at `0.3.x`) uses the
A package at `0.y.z` (currently `org.gamecult.cultmath`, at `0.4.x`) uses the
standard pre-1.0 convention: `MAJOR` stays `0` during initial development, so
`MINOR` carries what `MAJOR` means once the package reaches `1.0.0`. Concretely:

View file

@ -36,14 +36,21 @@ shader semantics.
It includes `shaders/CultMath.Phacelle.hlsl` (MPL-2.0) and
`shaders/CultMath.Interval.hlsl` and `shaders/CultMath.Affine.hlsl` from the same
directory, so ship all four.
- interval arithmetic for culling empty space with a proof: `iv_*` functions
- interval arithmetic for culling empty space: `iv_*` functions
over `float2(lo, hi)` (`iv_add`, `iv_mul`, `iv_exp`, `iv_smoothstep`, ...)
and `iv_snoise_ball`/`iv_fbm_ball`, which bound `snoise` and `fbm_grad` over a
ball, and `iv_frustum_ball`, the ball around a screen tile's rays over a depth
segment, so one probe serves the whole tile (its radius carries its own
float32 rounding bound). Every one returns an interval
containing the function's value at every point of its input, so a raymarch
float32 rounding bound). The arithmetic functions return an interval built to
contain the function's value over their operand intervals, so a raymarch
can skip a segment whose `hi` is below its cutoff; `cultmath_iv_*` in HLSL.
The noise functions rest on empirical constants with a margin, not a proof:
`iv_snoise_ball(float3)` on `SNOISE_LIPSCHITZ` (`iv_fbm_ball` through it),
`iv_snoise_ball(float2)` on `SNOISE2_LIPSCHITZ`, and `af_snoise` on
`SNOISE_HESSIAN` and `SNOISE_LIPSCHITZ` (`af_fbm` through it); the enclosure
tests sample them. `iv_frustum_ball` and `af_frustum_ball` read no noise
constant: the ball is geometry plus a rounding widening, and the noise call
it feeds carries the constant.
The bounds compose with a consumer's own analytic envelope (a height fog, a
carved sphere): its interval over the segment proves empty space in one probe,
and the noise ball is consulted only near the surface. See `docs/design.md`,
@ -122,6 +129,12 @@ The repository root is not a Unity package. Build the tracked package with
assembly and rejects stale binaries, missing metadata, unexpected assemblies,
or leaked C# source.
Before tagging `cultmath-unity-v*`, run the import and dispatch smoke on a Windows machine with a GPU,
alone: `packages/cultmath/scripts/unity-smoke/run-unity-smoke.ps1 -UnityExe <path to Unity.exe>`. It
builds a scratch project outside the repository that references the package by a `file:` path,
dispatches a compute shader that includes `CultMath.Interval.hlsl` and `CultMath.Affine.hlsl`, requires
a deliberately broken control shader to be rejected, and deletes the project afterwards.
## Shader Tooling
CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`:

View file

@ -371,9 +371,11 @@ together, laid out as `float4(gradient.xyz, value.w)`, with no value-only twin
pins the continuity. Ties in the offset `x0` resolve in Gustavson's
`simplexnoise1234` order (x over y, y over z, x over z), a total order, so the
four corners are always a simplex. Upstream's `step` compares are cyclic: where
the three components are equal, which includes the origin and every
`(t, t, t)`, the corners repeat the origin vertex and the value jumps by about
0.77 near the origin and up to 1.56 at magnitudes of 64 and above. `NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal` pins the order.
the three components are equal, which is every input whose differences `y - x`
and `z - x` are whole numbers (every integer point, the origin and every
`(t, t, t)` among them), the corners repeat the origin vertex and the value
jumps by about 0.77 near the origin and up to 1.56 at magnitudes of 64 and
above. `SnoiseTieScopeTests` pins the set against the 0.3.0 values. `NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal` pins the order.
- `snoise_grad(float3 p)` carries `snoise(float3)` to value-and-gradient form,
following the differentiation in webgl-noise `src/noise3Dgrad.glsl` (Ashima
Arts / Ian McEwan, MIT), which uses the same radius, permutation and scale as

View file

@ -0,0 +1,11 @@
#pragma kernel CSMain
#include "Packages/org.gamecult.cultmath/Shaders/CultMath.hlsl"
#include "Packages/org.gamecult.cultmath/Shaders/CultMath.Interval.hlsl"
#include "Packages/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl"
RWStructuredBuffer<float4> Out;
[numthreads(1,1,1)] void CSMain(uint3 id : SV_DispatchThreadID)
{
float2 r = cultmath_iv_add_missing(float2(1, 2), float2(3, 5));
float2 a = cultmath_af_range(cultmath_af_add(cultmath_af_point(3), cultmath_af_point(5)));
Out[0] = float4(r, a);
}

View file

@ -0,0 +1,14 @@
{
"name": "CultMath.Smoke.Tests",
"rootNamespace": "",
"references": [ "UnityEngine.TestRunner", "UnityEditor.TestRunner" ],
"includePlatforms": [ "Editor" ],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [ "nunit.framework.dll", "CultMath.dll" ],
"autoReferenced": false,
"defineConstraints": [ "UNITY_INCLUDE_TESTS" ],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,11 @@
#pragma kernel CSMain
#include "Packages/org.gamecult.cultmath/Shaders/CultMath.hlsl"
#include "Packages/org.gamecult.cultmath/Shaders/CultMath.Interval.hlsl"
#include "Packages/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl"
RWStructuredBuffer<float4> Out;
[numthreads(1,1,1)] void CSMain(uint3 id : SV_DispatchThreadID)
{
float2 r = cultmath_iv_add(float2(1, 2), float2(3, 5));
float2 a = cultmath_af_range(cultmath_af_add(cultmath_af_point(3), cultmath_af_point(5)));
Out[0] = float4(r, a);
}

View file

@ -0,0 +1,44 @@
using System;
using CultMath;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
public class SmokeTests
{
// Compiles by FindKernel and dispatches on the device; false when the shader does not compile or the output is wrong.
static bool CompilesAndRuns(string path)
{
var cs = AssetDatabase.LoadAssetAtPath<ComputeShader>(path);
Assert.IsNotNull(cs, path);
int k;
try { k = cs.FindKernel("CSMain"); } catch (Exception e) { Debug.Log("SMOKE " + path + " FindKernel threw: " + e.Message); return false; }
if (!cs.HasKernel("CSMain") || !cs.IsSupported(k)) { Debug.Log("SMOKE " + path + " kernel unusable"); return false; }
var buf = new ComputeBuffer(1, 16);
try
{
cs.SetBuffer(k, "Out", buf);
cs.Dispatch(k, 1, 1, 1);
var o = new Vector4[1]; buf.GetData(o);
Debug.Log($"SMOKE {path} out=({o[0].x},{o[0].y},{o[0].z},{o[0].w}) gpu={SystemInfo.graphicsDeviceType}");
return o[0].x == 4f && o[0].y == 7f && o[0].z <= 8f && o[0].w >= 8f && o[0].w - o[0].z < 1e-3f;
}
finally { buf.Release(); }
}
[Test] public void CSharpIntervalAndAffineWork()
{
var r = math.iv_add(new float2(1, 2), new float2(3, 5));
Assert.AreEqual(4f, r.x); Assert.AreEqual(7f, r.y);
Assert.AreEqual(0f, math.snoise(new float3(0, 0, 0)));
}
[Test] public void RealShaderCompilesAndDispatches() { Assert.IsTrue(CompilesAndRuns("Assets/Shaders/Good.compute")); }
[Test] public void BrokenShaderIsDetected()
{
LogAssert.ignoreFailingMessages = true;
Assert.IsFalse(CompilesAndRuns("Assets/Shaders/Broken.compute"));
}
}

View file

@ -0,0 +1,94 @@
<#
.SYNOPSIS
The release smoke for the tracked org.gamecult.cultmath Unity package: imports it into a
scratch project, compiles and dispatches a compute shader that includes the package's
CultMath.Interval.hlsl and CultMath.Affine.hlsl on a real graphics device, and proves a
deliberately broken shader is rejected.
.DESCRIPTION
Run it on a Windows machine with a GPU, alone, before tagging cultmath-unity-v*. The scratch
project is built outside the repository under -WorkRoot, references the package by a file: path,
and is deleted afterwards (-Keep leaves it). Exit 0 only when Unity exited 0, this run wrote the
results file (earlier ones are deleted before launch), and all three tests of SmokeTests.cs
passed: the C# calls, the real shader dispatched with the right output, and the broken control
shader detected.
Never add -nographics. Without a device nothing compiles, and
ShaderUtil.GetComputeShaderMessageCount reports 0 messages for a compute shader that calls an
undeclared function until FindKernel or Dispatch compiles it, so a message-count check passes a
broken shader. SmokeTests.cs compiles by FindKernel, dispatches, and reads the output back.
For a long run start it detached and poll the log, for example:
Start-Process powershell -WindowStyle Hidden -ArgumentList '-File', '<this script>', '-UnityExe', '<Unity.exe>', '-WorkRoot', '<dir>'
The script writes unity.pid and unity.log under the scratch root's logs folder (kept after the run).
.PARAMETER UnityExe
Path to Unity.exe of the editor to test with (the cut that released 0.4.0 used 6000.3.24f1).
.PARAMETER WorkRoot
Directory outside the repository for the scratch project and logs. Default: a new folder under TEMP.
.PARAMETER Keep
Keep the scratch project instead of deleting it.
#>
param(
[Parameter(Mandatory)] [string] $UnityExe,
[string] $WorkRoot = (Join-Path $env:TEMP ("cultmath-unity-smoke-" + [guid]::NewGuid().ToString('N').Substring(0, 8))),
[switch] $Keep
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path -LiteralPath $UnityExe -PathType Leaf)) { throw "UnityExe not found: $UnityExe" }
$smoke = $PSScriptRoot
$package = (Resolve-Path (Join-Path $smoke '..\..\unity\org.gamecult.cultmath')).Path
$repoRoot = (git -C $smoke rev-parse --show-toplevel).Trim()
$work = [IO.Path]::GetFullPath($WorkRoot)
if ($work.StartsWith([IO.Path]::GetFullPath($repoRoot), [StringComparison]::OrdinalIgnoreCase)) {
throw "WorkRoot must be outside the repository: $work"
}
$project = Join-Path $work 'project'
$logs = Join-Path $work 'logs'
New-Item -ItemType Directory -Force -Path $logs | Out-Null
foreach ($d in 'Packages', 'Assets\Shaders', 'Assets\Tests') { New-Item -ItemType Directory -Force -Path (Join-Path $project $d) | Out-Null }
Copy-Item -LiteralPath (Join-Path $smoke 'Good.compute'), (Join-Path $smoke 'Broken.compute') -Destination (Join-Path $project 'Assets\Shaders')
Copy-Item -LiteralPath (Join-Path $smoke 'SmokeTests.cs'), (Join-Path $smoke 'CultMath.Smoke.Tests.asmdef') -Destination (Join-Path $project 'Assets\Tests')
$packageUri = 'file:' + $package.Replace([string][char]92, '/')
$manifest = [ordered]@{
dependencies = [ordered]@{
'com.unity.test-framework' = '1.6.0'
'org.gamecult.cultmath' = $packageUri
}
}
[IO.File]::WriteAllText((Join-Path $project 'Packages\manifest.json'), ($manifest | ConvertTo-Json -Depth 4), (New-Object Text.UTF8Encoding($false)))
$log = Join-Path $logs 'unity.log'
$results = Join-Path $logs 'results.xml'
# A kept logs folder may hold an earlier run's evidence: nothing from a previous run may decide this one.
foreach ($stale in $log, $results, (Join-Path $logs 'unity.pid')) { Remove-Item -LiteralPath $stale -Force -ErrorAction SilentlyContinue }
if (Test-Path -LiteralPath $results) { throw "could not clear the previous results file: $results" }
$unityArgs = @('-batchmode', '-projectPath', $project, '-runTests', '-testPlatform', 'EditMode',
'-testResults', $results, '-logFile', $log)
$launched = Get-Date
$p = Start-Process -PassThru -FilePath $UnityExe -ArgumentList $unityArgs -WindowStyle Hidden
$p.Id | Out-File (Join-Path $logs 'unity.pid') -Encoding ascii
"unity pid=$($p.Id) log=$log"
$p.WaitForExit()
"unity exit=$($p.ExitCode)"
# Pass needs all of: Unity exited 0; a results file written by this run; the real shader test passed and
# the broken control test passed (it asserts that the broken shader is rejected).
$ok = $false
if ($p.ExitCode -ne 0) { "unity exited non-zero: $($p.ExitCode)" }
elseif (-not (Test-Path -LiteralPath $results)) { 'no results file from this run' }
elseif ((Get-Item -LiteralPath $results).LastWriteTime -lt $launched) { 'results file predates this run' }
else {
$cases = @(([xml](Get-Content -LiteralPath $results -Raw)).SelectNodes('//test-case'))
$cases | ForEach-Object { "{0} {1}" -f $_.result, $_.name }
$want = 'CSharpIntervalAndAffineWork', 'RealShaderCompilesAndDispatches', 'BrokenShaderIsDetected'
$passed = @($cases | Where-Object { $_.result -eq 'Passed' } | ForEach-Object { ($_.name -split '\.')[-1] })
$ok = (@($want | Where-Object { $passed -contains $_ }).Count -eq $want.Count)
}
Get-Content -LiteralPath $log -ErrorAction SilentlyContinue | Select-String -CaseSensitive -Pattern '^SMOKE ' | ForEach-Object { $_.Line }
if (-not $Keep) { Remove-Item -LiteralPath $project -Recurse -Force }
if ($ok) { 'SMOKE PASSED'; exit 0 } else { 'SMOKE FAILED'; exit 1 }

View file

@ -10,7 +10,7 @@
<PackageId>GameCult.Math</PackageId>
<!-- MIT overall; math.Phacelle.cs and CultPhasor.cs (and the two Phacelle HLSL files) are MPL-2.0. -->
<PackageLicenseExpression>MIT AND MPL-2.0</PackageLicenseExpression>
<Version>0.3.0</Version>
<Version>0.4.0</Version>
<Description>Clean-room C# math shaped for HLSL-style renderer code.</Description>
<Authors>GameCult</Authors>
<PackageReadmeFile>README.md</PackageReadmeFile>
@ -25,6 +25,7 @@
<None Include="..\..\shaders\CultMath.hlsl" Pack="true" PackagePath="contentFiles\any\any\shaders\CultMath.hlsl" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\shaders\CultMath.Phacelle.hlsl" Pack="true" PackagePath="contentFiles\any\any\shaders\CultMath.Phacelle.hlsl" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\shaders\CultMath.Interval.hlsl" Pack="true" PackagePath="contentFiles\any\any\shaders\CultMath.Interval.hlsl" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\shaders\CultMath.Affine.hlsl" Pack="true" PackagePath="contentFiles\any\any\shaders\CultMath.Affine.hlsl" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\THIRD-PARTY-NOTICES.md" Pack="true" PackagePath="\" />
</ItemGroup>

View file

@ -192,8 +192,9 @@ public sealed class NoiseGradTests
// The simplex diagonals are where x0's three components compare equal. The tie rule must be a total
// order there (math.snoise): with a cyclic or all-strict compare the four corners stop being a
// simplex and the value jumps on the line (t, t, t), lattice diagonals included: about 0.77 near the
// origin, up to 1.56 at magnitudes of 64 and above.
// simplex and the value jumps wherever x0 has three equal components (inputs whose differences y - x and
// z - x are whole numbers, the line (t, t, t) among them): about 0.77 near the origin, up to 1.56 at
// magnitudes of 64 and above. SnoiseTieScopeTests pins the set against the 0.3.0 values.
[Fact]
public void SnoiseIsContinuousOnTheSimplexDiagonal()
{

View file

@ -0,0 +1,177 @@
using System;
using CultMath;
using Xunit;
namespace CultMath.Tests;
// The 0.4.0 tie rule (math.snoise(float3), math.snoise_grad) changes values exactly where the cell
// offset x0 has three equal components, which is every input whose coordinate differences y - x and
// z - x are whole numbers (every integer point, and the whole (1, 1, 1) line through each). Off that
// set and its float32 band (below) the results are bit-identical to 0.3.0. The goldens below were computed by the
// cultmath-unity-v0.3.0 tag's CultMath.dll (Runtime/Plugins), not by this code. Float32 rounding
// decides whether a given integer-difference input ties, because x0 is computed in float32: an
// input whose differences round onto whole numbers can tie too and move, within a few float32
// ulps of the largest coordinate (a probe found up to about two; the band widens with the magnitude), and some inputs on
// the set keep their value (about 3% of random integer points). Every input below ties and moves
// all five outputs (value and the four gradient words).
// TieSetNow pins what this code returns on the same inputs, so that each of the two tie-rule sites
// (the step in snoise, the step in snoise_grad) is pinned on its own.
public sealed class SnoiseTieScopeTests
{
// (input, snoise bits, snoise_grad x y z w bits) from 0.3.0, each on the tie set and moved by the new rule.
private static readonly (float3 P, uint Value, uint[] Grad)[] TieSet =
{
(new float3(8f, -9f, 1f), 0xBDD82D0D, new uint[] { 0x3FA7D14B, 0x40A68968, 0xC0EE5B16, 0xBDD82D0D }),
(new float3(-15f, 1f, -3f), 0xBF29AEDB, new uint[] { 0x3ECABF0C, 0x40482713, 0x3F444FED, 0xBF29AEDB }),
(new float3(9f, -14f, 7f), 0x3F3F28F4, new uint[] { 0x3F5FB228, 0x3F471A21, 0x40454A1F, 0x3F3F28F4 }),
(new float3(15f, 8f, 17f), 0xBDEE4FE2, new uint[] { 0x3F1F3C06, 0x3FFA0CCF, 0xC00C8395, 0xBDEE4FE2 }),
(new float3(3f, -12f, 10f), 0xBF3A1373, new uint[] { 0x3FC00B62, 0x3E28F20F, 0x403F02DA, 0xBF3A1373 }),
(new float3(2f, -8f, 15f), 0x3F180CA2, new uint[] { 0x3F567351, 0xC0340E87, 0x40E74458, 0x3F180CA2 }),
(new float3(-6.1645937f, -6.1645937f, -6.1645937f), 0xBF1C7CA6, new uint[] { 0xC04341A7, 0xBFE7241A, 0x3EE7511A, 0xBF1C7CA6 }),
(new float3(22.580343f, 22.580343f, 22.580343f), 0x3F5D9075, new uint[] { 0x400096F0, 0xC0590215, 0x3FAAC79D, 0x3F5D9075 }),
(new float3(-25.630196f, -25.630196f, -25.630196f), 0x3F4C7BDB, new uint[] { 0xBFCE54B7, 0x3FCD63EC, 0x403492AC, 0x3F4C7BDB }),
(new float3(-26.818033f, -26.818033f, -26.818033f), 0x3DE34495, new uint[] { 0xBEF368D6, 0xBFD14FED, 0x3FE503E7, 0x3DE34495 }),
(new float3(8.4097805f, -2.5902195f, -0.5902195f), 0xBF85F631, new uint[] { 0xBFE0D093, 0x3F8E6022, 0x3ECA4F57, 0xBF85F631 }),
(new float3(8.618587f, 1.6185865f, 0.61858654f), 0xBE8FB513, new uint[] { 0xC02F9DDD, 0x4063CEA5, 0xBDF0BFBB, 0xBE8FB513 }),
(new float3(-13.692368f, -9.692368f, -20.692368f), 0xBF28A1E3, new uint[] { 0xC012A224, 0xC0187F57, 0xBF57F2CB, 0xBF28A1E3 }),
(new float3(9.495648f, 11.495648f, 15.495648f), 0x3E4FC9B5, new uint[] { 0x3EA26E55, 0x403CE620, 0xBF46F425, 0x3E4FC9B5 }),
(new float3(-12.66157f, -19.66157f, -9.66157f), 0xBF179BF9, new uint[] { 0x3FD03786, 0x403A1C1E, 0xBEAD7581, 0xBF179BF9 }),
(new float3(-8.801376f, -1.8013763f, -19.801376f), 0xBE82F2EE, new uint[] { 0xC0DCFC55, 0x405501DB, 0x401E6CF8, 0xBE82F2EE }),
};
// The values this code returns on TieSet's inputs, in the same order (the value, then the four
// snoise_grad components).
private static readonly (uint Value, uint[] Grad)[] TieSetNow =
{
(0xB37C25C6, new uint[] { 0x3F281943, 0x40521F92, 0xC0A8193D, 0xB37C25C6 }),
(0xBF214750, new uint[] { 0xBD931F34, 0x40260272, 0x3E94E53D, 0xBF214750 }),
(0x3F35B75A, new uint[] { 0x3EAD85CB, 0x3E78AB7B, 0x401F83B4, 0x3F35B75A }),
(0xBDE33E78, new uint[] { 0x3F0705E4, 0x3FE9CB38, 0xC00DE5C4, 0xBDE33E78 }),
(0xBF30DF87, new uint[] { 0x3F76B426, 0xBEAE857A, 0x401A3325, 0xBF30DF87 }),
(0x00000000, new uint[] { 0x3FEC7798, 0xBF1DA4FF, 0x40C50E50, 0x00000000 }),
(0xBF13F436, new uint[] { 0xC0219F00, 0xBFA829D9, 0x3F629666, 0xBF13F436 }),
(0x3F0C1A1C, new uint[] { 0x404F76CC, 0xBFA6560B, 0x402B2291, 0x3F0C1A1C }),
(0x3F2E5150, new uint[] { 0xC01F3830, 0x3F007F73, 0x3FD11DF1, 0x3F2E5150 }),
(0x3DDE3B0E, new uint[] { 0xBED7A81C, 0xBFC89166, 0x3FE7E3CC, 0x3DDE3B0E }),
(0xBF23BB4A, new uint[] { 0xC057D547, 0xBF7B66AE, 0xBFCA313F, 0xBF23BB4A }),
(0xBE68D15B, new uint[] { 0xC0391D00, 0x40366C69, 0xBEFFC8AD, 0xBE68D15B }),
(0xBF2659BE, new uint[] { 0xC0048291, 0xC00A5FC3, 0xBF21B992, 0xBF2659BE }),
(0x3E4630E4, new uint[] { 0x3E35FE51, 0x40306ACC, 0xBF655794, 0x3E4630E4 }),
(0xBF117B1D, new uint[] { 0x3F9D6F53, 0x401F35B7, 0xBF31BAFE, 0xBF117B1D }),
(0xBDA5A39E, new uint[] { 0xC0B4E453, 0x3FF84A5D, 0x3FA74D5F, 0xBDA5A39E }),
};
// The same, off the tie set: coordinate differences that are not whole numbers.
private static readonly (float3 P, uint Value, uint[] Grad)[] OffSet =
{
(new float3(-3.7841978f, -12.899288f, 1.5972604f), 0x3EA04649, new uint[] { 0xBF74444B, 0x3F87A8A7, 0xC021229C, 0x3EA04649 }),
(new float3(8.898657f, 12.611845f, 4.790286f), 0xBEC97F79, new uint[] { 0xBD25992C, 0xC02EF328, 0xBF17ECAB, 0xBEC97F79 }),
(new float3(0.7057237f, 11.097378f, 13.493794f), 0xBE00B70D, new uint[] { 0xBEDBFFF7, 0xBFDA79F7, 0xBF07EA57, 0xBE00B70D }),
(new float3(-15.702414f, 3.5919724f, -7.0502086f), 0xBDC6607C, new uint[] { 0xBF113459, 0x3EB74352, 0xBF4D2C56, 0xBDC6607C }),
(new float3(-13.623555f, -10.574037f, -15.697628f), 0x3E823717, new uint[] { 0xBF4926AE, 0x4076E757, 0x40710AFC, 0x3E823717 }),
(new float3(-10.028356f, 5.4002185f, -13.579105f), 0x3EF1651D, new uint[] { 0x3FF0CA03, 0xC004B90C, 0xBFAF91DA, 0x3EF1651D }),
(new float3(12.788626f, 14.914132f, -0.39256755f), 0x3DB39ABE, new uint[] { 0xC06D6427, 0xBF941974, 0x3E8B556D, 0x3DB39ABE }),
(new float3(-2.4998372f, 10.696207f, 15.391607f), 0xBF0D1821, new uint[] { 0x3FAB00CF, 0x4044DAE4, 0x3F1E2E21, 0xBF0D1821 }),
(new float3(10.437524f, -3.4692447f, 15.652484f), 0x3EFCC588, new uint[] { 0xC083735E, 0x3FA4A10F, 0xBE7D19D2, 0x3EFCC588 }),
(new float3(-4.4040494f, 14.969201f, -2.2152362f), 0xBD4192BF, new uint[] { 0xBF3F69AE, 0xBF843396, 0xBFF88079, 0xBD4192BF }),
(new float3(2.7095387f, 13.3163395f, -0.28273603f), 0x3EF0704A, new uint[] { 0xBF75C892, 0x3F0C95B0, 0x3F7401B5, 0x3EF0704A }),
(new float3(-10.564334f, -5.6527104f, -6.6908674f), 0x3EA35AEA, new uint[] { 0xC05CB149, 0x3F928B08, 0x3EB68FCB, 0x3EA35AEA }),
(new float3(-13.572381f, -2.2560256f, -12.702176f), 0x3D2013BE, new uint[] { 0xC0236256, 0x3D02D65C, 0x3DB57CBC, 0x3D2013BE }),
(new float3(0.11813756f, -7.2100754f, 7.144552f), 0xBF244AB4, new uint[] { 0xBF6B4A5E, 0xBFFC1BFA, 0xBE868270, 0xBF244AB4 }),
(new float3(9.668022f, -0.32686335f, 2.333893f), 0xBDE9567A, new uint[] { 0x3F359CBB, 0x3F2B104D, 0x3FEFAB81, 0xBDE9567A }),
(new float3(-6.9417686f, -6.2886133f, -13.874224f), 0x3CC28786, new uint[] { 0xC0291AB6, 0x3E57B89E, 0xBE884AF7, 0x3CC28786 }),
(new float3(9.9228945f, 13.366786f, 1.9626095f), 0x3DDCC7CA, new uint[] { 0xBFA57791, 0x40277CF0, 0xBF816FCF, 0x3DDCC7CA }),
(new float3(1.3210233f, -5.5900016f, 9.691286f), 0x3D140957, new uint[] { 0x4003DBED, 0x3E35DCDD, 0xBF9CFAAE, 0x3D140957 }),
(new float3(2.5023124f, 14.715639f, -7.5954947f), 0x3EB522F9, new uint[] { 0x3FB54F05, 0x4003D90A, 0x3F4CCE6D, 0x3EB522F9 }),
(new float3(2.2056534f, 14.461132f, 0.081089176f), 0xBE0FBF83, new uint[] { 0xBF2E2FDB, 0x4014B7C5, 0x3F8C8EC2, 0xBE0FBF83 }),
(new float3(5.137802f, -10.890227f, 12.128177f), 0xBEAEF52F, new uint[] { 0xBFBC5CA4, 0xBFD159C2, 0x402EBCD4, 0xBEAEF52F }),
(new float3(-0.290969f, -2.4119518f, 6.2756324f), 0x3EE30F7E, new uint[] { 0x3E94AD2B, 0x3F8DCBA0, 0x4055B8BB, 0x3EE30F7E }),
(new float3(9.232528f, 12.940779f, 11.078674f), 0x3E380DF3, new uint[] { 0x3F8B7DBE, 0x3F1596ED, 0xC0527328, 0x3E380DF3 }),
(new float3(6.9932394f, -12.455975f, -8.343756f), 0x3E929F5A, new uint[] { 0x3E5564B6, 0x3FF1BE81, 0xBF77638A, 0x3E929F5A }),
(new float3(15.123071f, 12.623071f, 20.12307f), 0x3E9527A3, new uint[] { 0x3D084582, 0xC07DE925, 0xBECA2112, 0x3E9527A3 }),
(new float3(8.46288f, 11.96288f, 4.46288f), 0x3E8C0F92, new uint[] { 0x3FD2DFD3, 0x3FB49817, 0x3F023648, 0x3E8C0F92 }),
(new float3(-2.9422863f, -1.4422863f, -12.9422865f), 0xBEBAEE39, new uint[] { 0x3FECB31B, 0xBFD33138, 0x3F9A17A8, 0xBEBAEE39 }),
(new float3(13.526707f, 15.026707f, 2.5267067f), 0x3EF22701, new uint[] { 0x3F1D389B, 0x3FCABBBE, 0xBF66EE93, 0x3EF22701 }),
(new float3(11.822154f, 1.322154f, 13.822154f), 0x3E85ACC7, new uint[] { 0x3FA7C0C7, 0xC067C262, 0x3FF35925, 0x3E85ACC7 }),
(new float3(6.025978f, 8.525978f, 3.025978f), 0x3E9964B0, new uint[] { 0xBEE7F346, 0x3F1918E9, 0x3EF3DA8A, 0x3E9964B0 }),
(new float3(-15.095373f, -14.595373f, -16.095373f), 0xBE6EF82D, new uint[] { 0xC01F9511, 0x3FEDB679, 0xBFDA5058, 0xBE6EF82D }),
(new float3(3.2167344f, 12.716734f, -6.7832656f), 0x3EA3C041, new uint[] { 0xBFFA5E11, 0xBF41778D, 0xBFCFD96A, 0x3EA3C041 }),
(new float3(12.25f, -0.25f, -3.25f), 0x3EB8C88C, new uint[] { 0xBFA0E2A2, 0x40101407, 0x402D2F5B, 0x3EB8C88C }),
(new float3(-9f, -13.25f, 10f), 0x3F032C5D, new uint[] { 0x3F9FFBCB, 0x3E6ED505, 0x401D75F4, 0x3F032C5D }),
(new float3(-5.75f, 2.25f, 13.75f), 0x3E62496B, new uint[] { 0x3FC789CA, 0x3EE63ACD, 0xBEE57812, 0x3E62496B }),
(new float3(-12.5f, 10f, -5.25f), 0x3EEA194F, new uint[] { 0x3F182206, 0xC0282BDE, 0xBFA51540, 0x3EEA194F }),
(new float3(13.5f, -11f, -3f), 0x3E6B5E9A, new uint[] { 0xBF2F5213, 0x3FE97BC2, 0x3F072FB4, 0x3E6B5E9A }),
(new float3(-2.75f, 7.75f, 3.5f), 0x3E91F33E, new uint[] { 0x3E2546E0, 0xBF63D038, 0xBFC3C500, 0x3E91F33E }),
(new float3(10.5f, 4f, -3.75f), 0x3EBCDE1F, new uint[] { 0xBFDC4426, 0x400DCD14, 0x3F7CACF6, 0x3EBCDE1F }),
(new float3(-12.75f, -13f, -6f), 0x3D5F6FB0, new uint[] { 0xBFBF0ED8, 0xC0109EED, 0xC007C568, 0x3D5F6FB0 }),
(new float3(-0.069121994f, 0.76107657f, 0.41525742f), 0x3DA72363, new uint[] { 0x3E5DF1FF, 0x3F7E17BE, 0x3FA89CB3, 0x3DA72363 }),
(new float3(1.0466753f, -0.51170796f, 0.21126905f), 0xBDD1FE81, new uint[] { 0x3DFD066D, 0x3E5E0061, 0x3FDBADAF, 0xBDD1FE81 }),
(new float3(0.30550694f, 1.6808131f, -0.76585627f), 0x3F0F3AB5, new uint[] { 0x3FF9EAC8, 0xC001ADBF, 0x3FD97B97, 0x3F0F3AB5 }),
(new float3(-0.8827523f, 0.12493165f, -1.8058014f), 0x3EC3BE7E, new uint[] { 0xC02395D6, 0x3F00EEB9, 0x3F976926, 0x3EC3BE7E }),
};
private static bool OnTieSet(float3 p)
{
var a = (double)p.y - p.x;
var b = (double)p.z - p.x;
return a == Math.Floor(a) && b == Math.Floor(b);
}
// Each output is asserted on its own: a tie rule reverted at one site must fail here by itself,
// whatever the other site returns. A given input moves some outputs and not others, so each
// output must differ from 0.3.0 on at least one tie-set input.
[Fact]
public void EachOutputDiffersFromZeroPointThreeSomewhereOnTheTieSet()
{
Assert.NotEmpty(TieSet);
var moved = new bool[5];
foreach (var (p, value, grad) in TieSet)
{
Assert.True(OnTieSet(p));
var g = math.snoise_grad(p);
moved[0] |= BitConverter.SingleToUInt32Bits(math.snoise(p)) != value;
moved[1] |= BitConverter.SingleToUInt32Bits(g.x) != grad[0];
moved[2] |= BitConverter.SingleToUInt32Bits(g.y) != grad[1];
moved[3] |= BitConverter.SingleToUInt32Bits(g.z) != grad[2];
moved[4] |= BitConverter.SingleToUInt32Bits(g.w) != grad[3];
}
Assert.True(moved[0], "snoise(float3) still reads its 0.3.0 value on the tie set");
Assert.True(moved[1], "snoise_grad.x still reads its 0.3.0 value on the tie set");
Assert.True(moved[2], "snoise_grad.y still reads its 0.3.0 value on the tie set");
Assert.True(moved[3], "snoise_grad.z still reads its 0.3.0 value on the tie set");
Assert.True(moved[4], "snoise_grad.w still reads its 0.3.0 value on the tie set");
}
[Fact]
public void TieSetInputsReturnTheirZeroPointFourGoldens()
{
Assert.Equal(TieSet.Length, TieSetNow.Length);
for (var k = 0; k < TieSet.Length; k++)
{
var p = TieSet[k].P;
var (value, grad) = TieSetNow[k];
var g = math.snoise_grad(p);
Assert.Equal(value, BitConverter.SingleToUInt32Bits(math.snoise(p)));
Assert.Equal(grad[0], BitConverter.SingleToUInt32Bits(g.x));
Assert.Equal(grad[1], BitConverter.SingleToUInt32Bits(g.y));
Assert.Equal(grad[2], BitConverter.SingleToUInt32Bits(g.z));
Assert.Equal(grad[3], BitConverter.SingleToUInt32Bits(g.w));
}
}
[Fact]
public void InputsOffTheTieSetKeepTheirZeroPointThreeBits()
{
Assert.NotEmpty(OffSet);
foreach (var (p, value, grad) in OffSet)
{
Assert.False(OnTieSet(p));
var g = math.snoise_grad(p);
Assert.Equal(value, BitConverter.SingleToUInt32Bits(math.snoise(p)));
Assert.Equal(grad[0], BitConverter.SingleToUInt32Bits(g.x));
Assert.Equal(grad[1], BitConverter.SingleToUInt32Bits(g.y));
Assert.Equal(grad[2], BitConverter.SingleToUInt32Bits(g.z));
Assert.Equal(grad[3], BitConverter.SingleToUInt32Bits(g.w));
}
}
}

View file

@ -5,6 +5,74 @@ All notable changes to this package are documented in this file.
Earlier releases (0.2.0-0.2.3) predate this package's changelog and are not
backfilled here; see `docs/semver-policy.md` for why.
## [0.4.0]
### Breaking
- `math.snoise(float3)` and `math.snoise_grad`, and their HLSL mirrors in
`Shaders/CultMath.hlsl`, now resolve ties among the components of the cell
offset in a fixed order: x over y, y over z, x over z. The cyclic rule they
used before read a different function wherever the three components of the
cell offset are equal. That is every input whose coordinate differences
`y - x` and `z - x` are whole numbers: every integer point (the origin
included), and the whole `(1, 1, 1)` line through each of them (`(t, t, t)`
among them). On that set values change by up to 0.776; for example
`snoise(0, 0, 0)` was -0.4358730 and is 0, `snoise(5, 0, 0)` was 0.7076616 and
is 0.671713, and `snoise(1, 2, 3)` was 0.22423841 and is 0. Float32 rounding
decides whether a given input ties, and an input that ties may still keep its
value; measured over random integer points, about 97% change. The cell offset
is computed in float32, so an input whose differences `y - x` and `z - x`
round, in float32, onto whole numbers can tie and change too. How far from the set that
reaches scales with the coordinates' magnitude: a few float32 ulps of the largest
coordinate (a probe over random inputs found changed inputs up to about two ulps
away; measured, not proved). Computed coordinates such as `i * 0.1f`
can land there, so near large coordinates the band is wide. Elsewhere the results are
bit-identical to 0.3.0 (`SnoiseTieScopeTests`, goldens computed by the 0.3.0
DLL). `snoise` is now
continuous across the diagonals to within 1e-4 over two ulps either side
(`NoiseGradTests`, `SnoiseIsContinuousOnTheSimplexDiagonal`). Anything keyed
on integer lattice coordinates (voxel or tile seeds, cell centres, generated
content sampled on a whole-number grid) changes; regenerate it when you take
this version.
### Added
- Interval arithmetic on `float2(lo, hi)`: `math.iv_point`, `iv_add`, `iv_sub`,
`iv_mul`, `iv_neg`, `iv_scale`, `iv_abs`, `iv_min`, `iv_max`, `iv_clamp`,
`iv_saturate`, `iv_lerp`, `iv_sqr`, `iv_sqrt`, `iv_exp`, `iv_smoothstep`,
`iv_snoise_ball` (`float3` and `float2` centres), `iv_fbm_ball` and
`iv_frustum_ball`, with the HLSL mirrors in the new
`Shaders/CultMath.Interval.hlsl`. Each function is built to return an interval that
contains the pointwise float32 result for the operand intervals, but the noise
functions rest on empirical constants with a margin, not on a proof:
`iv_snoise_ball(float3)` on `SNOISE_LIPSCHITZ`, `iv_fbm_ball` on the same constant through
`iv_snoise_ball(float3)`, `iv_snoise_ball(float2)` on `SNOISE2_LIPSCHITZ`, and
`af_snoise` on `SNOISE_HESSIAN` and `SNOISE_LIPSCHITZ`, which `af_fbm` reaches through
`af_snoise`. `iv_frustum_ball` and `af_frustum_ball` read no noise constant: they are
geometry plus a 2^-20 rounding widening, and they return a ball (`float4`) to hand to a noise
call, which carries the constant. The enclosure tests sample
points and check containment without a tolerance; they do not prove it for
every input.
- Reduced affine forms on `float3(x0, a, e)` that share one symbol over a
region: `math.af_point`, `af_symbol`, `af_range`, `af_from_iv`, `af_add`,
`af_add_iv`, `af_sub`, `af_neg`, `af_scale`, `af_mul`, `af_snoise`, `af_fbm`,
`af_frustum_axis` and `af_frustum_ball`, with the HLSL mirrors in the new
`Shaders/CultMath.Affine.hlsl`.
- Noise bound constants `math.SNOISE_LIPSCHITZ`, `SNOISE2_LIPSCHITZ` and
`SNOISE_HESSIAN`: empirical bounds with a margin, re-measured and pinned by
`NoiseBoundTests`.
- `math.asfloat(uint)`, the inverse of `math.asuint`.
- `BoundedLeastSquares.Solve` overload taking `kktRelativeTolerance`, and the
constant `BoundedLeastSquares.DefaultKktRelativeTolerance` (1e-6). The
tolerance must be finite and greater than zero, else the result is
`InvalidInput`. The overload without it passes the default and returns the same
result as before.
### Fixed
- The NuGet package `GameCult.Math` now carries `CultMath.Affine.hlsl` under
`contentFiles/any/any/shaders`, as the README says it does.
## [0.3.0]
### Breaking

View file

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

View file

@ -4,7 +4,7 @@
"version": "1.0.60",
"unity": "2021.3",
"dependencies": {
"org.gamecult.cultmath": "0.3.0"
"org.gamecult.cultmath": "0.4.0"
},
"description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.",
"license": "MIT",