Move CultMath into the CultLib monorepo

This commit is contained in:
Meta Vens 2026-08-26 17:35:51 +02:00
commit ea88cfcd05
98 changed files with 3574 additions and 4 deletions

View file

@ -43,6 +43,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EveBrowserNetworkSample", "samples\eve-browser-network\EveBrowserNetworkSample.csproj", "{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "packages", "packages", "{809F86A1-1C4C-B159-0CD4-DF9D33D876CE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cultmath", "cultmath", "{B48EAE30-152F-01E3-6E31-82B350494D3C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath", "packages\cultmath\src\CultMath\CultMath.csproj", "{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CultMath.Tests", "packages\cultmath\tests\CultMath.Tests\CultMath.Tests.csproj", "{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -257,6 +265,30 @@ Global
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x64.Build.0 = Release|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x86.ActiveCfg = Release|Any CPU
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB}.Release|x86.Build.0 = Release|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|x64.ActiveCfg = Debug|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|x64.Build.0 = Debug|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|x86.ActiveCfg = Debug|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Debug|x86.Build.0 = Debug|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Release|Any CPU.Build.0 = Release|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Release|x64.ActiveCfg = Release|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Release|x64.Build.0 = Release|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Release|x86.ActiveCfg = Release|Any CPU
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F}.Release|x86.Build.0 = Release|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Debug|x64.ActiveCfg = Debug|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Debug|x64.Build.0 = Debug|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Debug|x86.ActiveCfg = Debug|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Debug|x86.Build.0 = Debug|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Release|Any CPU.Build.0 = Release|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Release|x64.ActiveCfg = Release|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Release|x64.Build.0 = Release|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Release|x86.ActiveCfg = Release|Any CPU
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -279,5 +311,8 @@ Global
{7F32D96B-AB5D-4A72-9217-B5DCCFEFC7E5} = {E733CBFF-42AE-4B6A-B5B9-0071ECFB45F1}
{B9A441EC-DADE-4500-B092-C635F3EB296A} = {39D316AB-ABB5-49A2-8E83-CAF4C75B48F4}
{5FA81CC7-87B2-4585-9D8E-9BBEC0AD85FB} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
{B48EAE30-152F-01E3-6E31-82B350494D3C} = {809F86A1-1C4C-B159-0CD4-DF9D33D876CE}
{54D543F7-8EB7-4A11-977E-22D3EE2D8C3F} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
{FEF9DA88-2177-4FD0-885D-7096ACFA8A9D} = {B48EAE30-152F-01E3-6E31-82B350494D3C}
EndGlobalSection
EndGlobal

View file

@ -38,7 +38,8 @@ and pretending the label makes it furniture.
| Job | Start With | Owns | Use When | Do Not Use It For |
| --- | --- | --- | --- | --- |
| Local typed state | `GameCult.Caching` / CultCache | Document identity, schema compatibility, record keys, indexes, globals, and local persistence | You need a typed cache, file-compatible save data, local reactive reads, or a stable domain document model | Peer discovery, transport security, shard routing, or mesh consensus |
| Procedural geometry state | `GameCult.Geometry` | CultCache-native domain trees, LOD build requests, selected-cut diagnostics, and chunk artifact payloads | Rust, Unity, or remote workers need to share CSG/LOD geometry and graph metadata as typed state | Transport policy, peer discovery, or gameplay authority |
| Portable math | `GameCult.Math` / CultMath | Shader-shaped numeric primitives, deterministic random, noise, and CPU/GPU parity helpers | Shared simulation and geometry code must not depend on an engine's math or RNG types | Domain-specific geometry, rendering, or world authority |
| Procedural geometry | `GameCult.Geometry` / CultGeometry | Isosurface extraction plus CultCache-native domains, LOD requests, selected cuts, and mesh artifacts | C#, Rust, Godot, or remote workers need shared geometry algorithms and typed results | Transport policy, peer discovery, or gameplay authority |
| Network transport and database plumbing | `GameCult.Networking` / CultNet | Native RUDP sessions, LiteNetLib/TCP adapters, an authenticated WebSocket host adapter, schema-v0 wire contracts, shard authority, raw document replication, snapshots, and subscriptions | You need a client/server pipe, login/session flow, schema discovery, reliable UDP, or a low-level distributed CultCache lane | Gameplay-facing mesh ergonomics, Verse policy, mod branches, or simulation consensus composition |
| Distributed realtime gameplay state | `GameCult.Mesh` / CultMesh | Public mesh entrypoints, Verse discovery, peer exchange, shard replication defaults, authority leases, client prediction, and witness consensus | You want the game to treat clients and servers as one reactive database for persistent state, input state, and simulation facts | A tiny local-only tool, a bare transport client, or a storage format contract |
| Realtime media/frame streams | `GameCult.Mesh` / CultMesh streaming mode | Stream identity, authority, clock metadata, body transport negotiation, frame cursors, and backpressure state | Audio/video/tensor frames need to move between runtimes through shared memory, GPU handles, platform buffers, or CultCache page refs | Durable document mutation, mesh consensus facts, or pretending inline bytes are zero-copy |
@ -47,8 +48,10 @@ and pretending the label makes it furniture.
Quick rule:
- Choose CultCache when the problem is "how do I model and persist typed state?"
- Choose GameCult.Geometry when the problem is "how do geometry workers share
domain trees, LOD build requests, and mesh chunks as typed state?"
- Choose GameCult.Math when the problem is "which portable numeric and random
semantics do these runtimes share?"
- Choose GameCult.Geometry when the problem is "how do geometry workers build
and share engine-neutral geometry?"
- Choose CultNet when the problem is "how do peers exchange authenticated,
schema-aware database messages?"
- Choose CultMesh when the problem is "how does a game join a Verse and share
@ -97,7 +100,8 @@ The solution includes:
- `GameCult.Caching.NewtonsoftJson`: Newtonsoft.Json-backed persistence for the cache
- `GameCult.Caching.MessagePack.Generator`: source generator for MessagePack formatters for cache models
- `GameCult.Caching.MessagePack.Analyzers`: packaging project that delivers the generator to consuming projects
- `GameCult.Geometry`: CultCache-native geometry domain, selected-cut, and chunk artifact documents for VibeGeometry/Fensalir-style pipelines
- `GameCult.Math` / CultMath: portable numeric primitives, deterministic random, noise, and shader-parity helpers
- `GameCult.Geometry` / CultGeometry: engine-neutral geometry algorithms plus CultCache-native domain, selected-cut, and mesh artifact documents
- `GameCult.Networking`: encrypted login/register/verify flows, schema-v0 contracts, transport adapters, and native RUDP sessions
- `GameCult.Networking.WebSockets`: bounded ASP.NET Core binary WebSocket host adapter for browser CultMesh clients
- `GameCult.Mesh`: CultMesh package home for distributed realtime database, shard replication, client prediction, Verse discovery, and mesh witness consensus
@ -134,6 +138,7 @@ tests/
GameCult.Geometry.Tests/
GameCult.Networking.Tests/
packages/
cultmath/
cultcache-ts/
cultnet-ts/
cultmesh-ts/

9
packages/cultmath/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
bin/
obj/
**/bin/
**/obj/
target/
**/target/
/native/cultmath-core/target.meta
.tools/
/artifacts/

134
packages/cultmath/README.md Normal file
View file

@ -0,0 +1,134 @@
# CultMath
CultMath is the project name. Its clean .NET package identity is
`GameCult.Math`, owned and released from the CultLib monorepo.
Clean-room math shaped for people who think in HLSL.
CultMath provides lowercase vector types, shader-style intrinsics, and native
hot-path kernels so math-heavy renderer code can move between C#, Rust, and HLSL
without turning every line into a translation exercise. It is not a
Unity.Mathematics clone. It is a small, Aquarium-born library built from public
shader semantics.
## Current Surface
- `float2`, `float3`, `float4`
- component-wise arithmetic
- scalar-to-vector splats
- swizzles for common lanes
- conversions to and from `System.Numerics`
- `Random`, a compact deterministic XORShift stream with scalar and vector draws
- `BatchMath` SoA kernels over `Span<float>` / `ReadOnlySpan<float>` using
`System.Numerics.Vector<float>` when available, including radial falloff
acceleration and semi-implicit Euler integration
- `math` intrinsics: `radians`, `degrees`, `abs`, `floor`, `ceil`, `frac`,
`min`, `max`, `clamp`, `saturate`, `lerp`, `step`, `smoothstep`, `dot`,
`cross`, `length`, `distance`, `normalize`, `reflect`, `csum`, exponential
`decay`/`damp`, intercept/segment helpers, Catmull-Rom, Bezier curves, and
deterministic value-noise primitives
- `shaders/CultMath.hlsl`, a canonical HLSL mirror include for shader-side
parity. HLSL already owns `float2`, `float3`, and `float4`; the include
exposes `cultmath_*` functions for shared semantics, including CultMath's
safe `normalize` contract.
- `Voronoi.SampleTones`, a C# batch surface that calls the Rust
`cultmath-core` native kernel when `cultmath_core` is available and falls back
to the managed parity path otherwise.
## Native Core
The canonical hot-path body lives in `native/cultmath-core`. It exports a C ABI
for batch kernels such as `cultmath_apollonian_voronoi_tones`.
Runtime wrappers own syntax and integration. Rust owns optimized primitive
kernel behavior and parity fixtures.
## Example
```csharp
using CultMath;
float3 normal = math.normalize(new float3(0.25f, 1.0f, -0.1f));
float rim = math.saturate(1.0f - math.dot(normal, new float3(0.0f, 0.0f, -1.0f)));
float glow = math.smoothstep(0.2f, 1.0f, rim);
float grain = math.value_noise_bicubic(math.float2(12.0f, 3.5f));
```
```hlsl
#include "CultMath.hlsl"
float3 normal = cultmath_normalize(float3(0.25, 1.0, -0.1));
float rim = cultmath_saturate(1.0 - dot(normal, float3(0.0, 0.0, -1.0)));
float glow = cultmath_smoothstep(0.2, 1.0, rim);
float grain = cultmath_value_noise_bicubic(float2(12.0, 3.5));
```
## Provenance
The small spline, damping, and pursuit helpers were promoted from Aetheria's
local `AetheriaMath` surface. The value-noise family follows public shader
literature from Inigo Quilez's noise and mini-spline articles, expressed in
CultMath names with tests instead of copied project-local helper drift.
## Build
```powershell
dotnet build packages/cultmath/src/CultMath/CultMath.csproj
dotnet test packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj
cargo test --manifest-path packages/cultmath/native/cultmath-core/Cargo.toml
cargo build --release --manifest-path packages/cultmath/native/cultmath-core/Cargo.toml
.\packages\cultmath\scripts\build-unity-package.ps1
```
## Unity package
CultMath's Unity 2021.3 surface is the tracked precompiled package at
`unity/org.gamecult.cultmath`. Unity consumes `CultMath.dll` and the numeric
`CultMath.hlsl`; it does not compile the repository's current-language C# source
tree and does not require a consumer `csc.rsp` workaround.
```json
"org.gamecult.cultmath": "https://github.com/GameCult/CultLib.git?path=/packages/cultmath/unity/org.gamecult.cultmath"
```
The repository root is not a Unity package. Build the tracked package with
`scripts/build-unity-package.ps1`; the script fresh-builds the netstandard2.1
assembly and rejects stale binaries, missing metadata, unexpected assemblies,
or leaked C# source.
## Shader Tooling
CultMath keeps portable DXC outside git under `.tools/`:
```powershell
.\tools\get-dxc.ps1
.\tools\get-spirv-cross.ps1
.\tools\compile-hlsl-spirv.ps1 `
-ShaderPath E:\Projects\Odin\crates\muninn-move-tracker\shaders\MoveSphereCandidate.comp.hlsl `
-OutputPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.spv `
-IncludePath E:\Projects\CultMath\shaders
.\tools\compile-spirv-metal.ps1 `
-SpirvPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.spv `
-OutputPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.metal
```
DXC emits Vulkan SPIR-V with `-spirv`. `-TargetEnv vulkan1.2` is exposed by the
script, but the current Windows DXC release compiled this shader successfully
with DXC's default Vulkan target while rejecting the dotted target-env value.
The SPIR-V script applies default Vulkan register shifts for cbuffers, textures,
UAVs, and samplers so cross-compiled Metal buffers do not alias by resource
class. SPIRV-Cross emits Metal Shading Language from SPIR-V on Windows; building
an iPad-ready `.metallib` still belongs to Apple's Metal compiler on an Apple
toolchain.
## Doctrine
The goal is mechanical sympathy between CPU-side authoring, native kernels, and
GPU-side shader math. If an intrinsic exists here, it should behave like the
shader concept unless a runtime's numeric rules make that impossible. Weird
deviations get tests and documentation, not folklore.
CultMath is still not a shader generator. Projects should include
`shaders/CultMath.hlsl` directly or add that directory to their shader include
path. If a shared operation needs CPU/GPU parity, put the primitive here and
test it here before copying local math helpers into a renderer or daemon.

View file

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

View file

@ -0,0 +1,11 @@
# Third-Party Notices
## 2D Simplex Noise
`math.simplex_noise` and `cultmath_simplex_noise` port the textureless 2D
simplex-noise function by Ian McEwan and Ashima Arts, as distributed by Stefan
Gustavson's `webgl-noise` project and Unity Mathematics.
- Copyright 2011 Ashima Arts.
- Upstream: <https://github.com/ashima/webgl-noise>
- License: MIT.

View file

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

View file

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

View file

@ -0,0 +1,29 @@
# Design
CultMath exists to make renderer math readable in every place it needs to live:
Rust hot kernels, C# host code, and HLSL shader code.
## Rules
- Keep type names HLSL-shaped: `float2`, `float3`, `float4`, and `math`.
- Keep Rust as the owner of native hot-path kernels and parity fixtures.
- Keep C#, shader, and scripting surfaces as runtime wrappers, not competing
math authorities.
- Keep `shaders/CultMath.hlsl` as the canonical shader mirror for shared
intrinsics. 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.
- Prefer small immutable value types.
- Prefer component-wise overloads over clever generic machinery until the API
earns that complexity.
- Keep the surface boring and exact. Noise primitives are allowed when they are
source-grounded, shader-shaped, and shared by more than one project. Matrices,
quaternions, and packing helpers still need an owner before they enter.
## Non-Goals
- Compiling C# to shaders.
- Generating HLSL from Rust or Rust from HLSL.
- Replacing `System.Numerics` for general .NET work.
- Recreating every Unity.Mathematics type because a spreadsheet somewhere says
“coverage.” That way lies decorative obesity.

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cultmath-core"
version = "0.1.0"

View file

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

View file

@ -0,0 +1,13 @@
[package]
name = "cultmath-core"
version = "0.1.0"
edition = "2021"
[lib]
name = "cultmath_core"
crate-type = ["cdylib", "staticlib"]
[profile.release]
lto = "thin"
codegen-units = 1
panic = "abort"

View file

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

View file

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

View file

@ -0,0 +1,213 @@
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CultMathColor32 {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
#[no_mangle]
pub unsafe extern "C" fn cultmath_apollonian_voronoi_tones(
xs: *const f32,
ys: *const f32,
tones: *const u8,
spans: *const f32,
count: usize,
resolution_y: f32,
frame_index: i32,
out_colors: *mut CultMathColor32,
) -> i32 {
if xs.is_null() || ys.is_null() || tones.is_null() || spans.is_null() || out_colors.is_null() {
return -1;
}
let xs = unsafe { std::slice::from_raw_parts(xs, count) };
let ys = unsafe { std::slice::from_raw_parts(ys, count) };
let tones = unsafe { std::slice::from_raw_parts(tones, count) };
let spans = unsafe { std::slice::from_raw_parts(spans, count) };
let out_colors = unsafe { std::slice::from_raw_parts_mut(out_colors, count) };
for index in 0..count {
out_colors[index] = tone_color(
xs[index],
ys[index],
resolution_y,
frame_index,
tones[index],
spans[index],
);
}
0
}
fn tone_color(px: f32, py: f32, resolution_y: f32, frame_index: i32, tone: u8, span: f32) -> CultMathColor32 {
let sample = stable_sample(px, py, resolution_y, frame_index, tone, span);
let luminance = saturate(sample.0 * 0.299 + sample.1 * 0.587 + sample.2 * 0.114);
match tone {
3 => {
let glow = 0.62 + luminance * 0.38;
color(255.0 * glow, 96.0 * glow, 14.0 * glow)
}
4 => {
let glow = 0.68 + luminance * 0.32;
color(245.0 * glow, 248.0 * glow, 252.0 * glow)
}
_ => {
let (gain, lift) = match tone {
1 => (0.42, 14.0),
2 => (1.05, 24.0),
_ => (0.24, 4.0),
};
color(
lift + sample.0 * 255.0 * gain,
lift + sample.1 * 255.0 * gain,
lift + sample.2 * 255.0 * gain,
)
}
}
}
fn stable_sample(px: f32, py: f32, resolution_y: f32, frame_index: i32, tone: u8, span: f32) -> (f32, f32, f32) {
let seed = px * 12.9898 + py * 78.233 + hash1((tone as f32 + 1.0) * 19.19);
let jitter_x = (hash1(seed) - 0.5) * span;
let jitter_y = (hash1(seed + 37.17) - 0.5) * span;
voronoi(px + jitter_x, py + jitter_y, resolution_y, frame_index)
}
fn voronoi(px: f32, py: f32, resolution_y: f32, frame_index: i32) -> (f32, f32, f32) {
let time = frame_index as f32 / 120.0;
let scale = 6.0;
let x = scale * px / resolution_y.max(1.0);
let y = scale * py / resolution_y.max(1.0);
let nx = x.floor();
let ny = y.floor();
let fx = frac(x);
let fy = frac(y);
let mut distance = 8.0;
let mut red = 0.0;
let mut green = 0.0;
let mut blue = 0.0;
let smoothness = 0.005;
for j in -2..=2 {
for i in -2..=2 {
let gx = i as f32;
let gy = j as f32;
let (mut ox, mut oy) = hash2(nx + gx, ny + gy);
let weight = ox * 0.5 + 0.5;
ox = 0.5 + 0.5 * (time + 6.2831 * ox).sin();
oy = 0.5 + 0.5 * (time + 6.2831 * oy).sin();
let dx = (gx - fx + ox).abs();
let dy = (gy - fy + oy).abs();
let d = dx.max(dy) * weight;
let seed = hash1((nx + gx) * 7.0 + (ny + gy) * 113.0);
let hue = frac((nx + gx) * 0.173 + (ny + gy) * 0.379 + seed * 0.431);
let (candidate_red, candidate_green, candidate_blue) = pastel_spectrum(hue);
let h = smoothstep(0.0, 1.0, 0.5 + 0.5 * (distance - d) / smoothness);
let correction = h * (1.0 - h) * smoothness / (1.0 + 3.0 * smoothness);
distance = lerp(distance, d, h) - correction;
red = lerp(red, candidate_red, h) - correction;
green = lerp(green, candidate_green, h) - correction;
blue = lerp(blue, candidate_blue, h) - correction;
}
}
let edge_dimming = 1.0 - 0.1 * smoothstep(0.04, 0.05, distance);
(
(red * edge_dimming).max(0.0),
(green * edge_dimming).max(0.0),
(blue * edge_dimming).max(0.0),
)
}
fn hash1(value: f32) -> f32 {
frac(value.sin() * 43758.5453)
}
fn hash2(x: f32, y: f32) -> (f32, f32) {
(
frac((x * 127.1 + y * 311.7).sin() * 43758.5453),
frac((x * 269.5 + y * 183.3).sin() * 43758.5453),
)
}
fn frac(value: f32) -> f32 {
value - value.floor()
}
fn saturate(value: f32) -> f32 {
value.clamp(0.0, 1.0)
}
fn smoothstep(edge0: f32, edge1: f32, value: f32) -> f32 {
let t = saturate((value - edge0) / (edge1 - edge0));
t * t * (3.0 - 2.0 * t)
}
fn lerp(start: f32, end: f32, amount: f32) -> f32 {
start + (end - start) * amount
}
fn pastel_spectrum(hue: f32) -> (f32, f32, f32) {
hsv_to_rgb(hue, 0.52, 0.98)
}
fn hsv_to_rgb(hue: f32, saturation: f32, value: f32) -> (f32, f32, f32) {
let h = frac(hue) * 6.0;
let c = value * saturation;
let x = c * (1.0 - ((h % 2.0) - 1.0).abs());
let m = value - c;
let (r, g, b) = if h < 1.0 {
(c, x, 0.0)
} else if h < 2.0 {
(x, c, 0.0)
} else if h < 3.0 {
(0.0, c, x)
} else if h < 4.0 {
(0.0, x, c)
} else if h < 5.0 {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
(r + m, g + m, b + m)
}
fn color(r: f32, g: f32, b: f32) -> CultMathColor32 {
CultMathColor32 {
r: r.clamp(0.0, 255.0) as u8,
g: g.clamp(0.0, 255.0) as u8,
b: b.clamp(0.0, 255.0) as u8,
a: 255,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn batch_sampler_writes_colors() {
let xs = [0.0, 20.0, 100.0];
let ys = [0.0, 30.0, 140.0];
let tones = [0, 3, 4];
let spans = [1920.0, 8.0, 8.0];
let mut out = [CultMathColor32::default(); 3];
let rc = unsafe {
cultmath_apollonian_voronoi_tones(
xs.as_ptr(),
ys.as_ptr(),
tones.as_ptr(),
spans.as_ptr(),
xs.len(),
1080.0,
12,
out.as_mut_ptr(),
)
};
assert_eq!(rc, 0);
assert!(out.iter().any(|color| color.r != 0 || color.g != 0 || color.b != 0));
}
}

View file

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

View file

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

View file

@ -0,0 +1,119 @@
param(
[string] $Configuration = "Release",
[string] $OutputDirectory = "artifacts\unity\org.gamecult.cultmath"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$projectPath = Join-Path $repoRoot "src\CultMath\CultMath.csproj"
$templateRoot = Join-Path $repoRoot "unity\org.gamecult.cultmath"
$buildRoot = Join-Path $repoRoot "artifacts\cultmath-unity-build"
$outputRoot = if ([System.IO.Path]::IsPathRooted($OutputDirectory)) {
$OutputDirectory
} else {
Join-Path $repoRoot $OutputDirectory
}
$freshAssembly = Join-Path $buildRoot "CultMath.dll"
$freshSymbols = Join-Path $buildRoot "CultMath.pdb"
$trackedPluginRoot = Join-Path $templateRoot "Runtime\Plugins"
$trackedAssembly = Join-Path $trackedPluginRoot "CultMath.dll"
$trackedSymbols = Join-Path $trackedPluginRoot "CultMath.pdb"
if (Test-Path -LiteralPath $buildRoot) {
Remove-Item -LiteralPath $buildRoot -Recurse -Force
}
if (Test-Path -LiteralPath $outputRoot) {
Remove-Item -LiteralPath $outputRoot -Recurse -Force
}
dotnet build $projectPath -c $Configuration -f netstandard2.1 -o $buildRoot `
--disable-build-servers -p:UseSharedCompilation=false -p:ContinuousIntegrationBuild=true -m:1
if ($LASTEXITCODE -ne 0) {
throw "CultMath Unity assembly build failed with exit code $LASTEXITCODE"
}
foreach ($path in @($freshAssembly, $freshSymbols)) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
throw "CultMath Unity build is missing $path"
}
}
$requiredTemplateFiles = @(
"LICENSE.md",
"LICENSE.md.meta",
"README.md",
"README.md.meta",
"THIRD-PARTY-NOTICES.md",
"THIRD-PARTY-NOTICES.md.meta",
"package.json",
"package.json.meta",
"Runtime\Plugins\CultMath.dll",
"Runtime\Plugins\CultMath.dll.meta",
"Runtime\Plugins\CultMath.pdb",
"Runtime\Plugins\CultMath.pdb.meta",
"Shaders\CultMath.hlsl",
"Shaders\CultMath.hlsl.meta"
)
foreach ($relativePath in $requiredTemplateFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $templateRoot $relativePath) -PathType Leaf)) {
throw "Tracked CultMath Unity package is incomplete: missing $relativePath"
}
}
$unityAssetFiles = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File | Where-Object {
$_.Extension -in @(".asmdef", ".dll", ".pdb", ".hlsl")
})
$missingAssetMetas = @($unityAssetFiles | Where-Object { -not (Test-Path -LiteralPath ($_.FullName + ".meta")) })
$missingDirectoryMetas = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -Directory | Where-Object {
-not (Test-Path -LiteralPath ($_.FullName + ".meta"))
})
if ($missingAssetMetas.Count -ne 0 -or $missingDirectoryMetas.Count -ne 0) {
$missing = @($missingAssetMetas.FullName) + @($missingDirectoryMetas.FullName)
throw "Tracked CultMath Unity package has assets without stable .meta files: $($missing -join ', ')"
}
$manifest = Get-Content -LiteralPath (Join-Path $templateRoot "package.json") -Raw | ConvertFrom-Json
$pluginMeta = Get-Content -LiteralPath ($trackedAssembly + ".meta") -Raw
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"
}
$facadeDefinitions = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File -Filter "*.asmdef")
if ($facadeDefinitions.Count -ne 0) {
throw "CultMath's precompiled DLL is the Unity assembly authority; empty asmdef facades are forbidden"
}
$expectedFileVersion = "$($manifest.version).0"
$trackedFileVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($trackedAssembly).FileVersion
$freshFileVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($freshAssembly).FileVersion
if ($trackedFileVersion -ne $expectedFileVersion -or $freshFileVersion -ne $expectedFileVersion) {
throw "CultMath Unity assemblies must match package version $expectedFileVersion; tracked=$trackedFileVersion fresh=$freshFileVersion"
}
foreach ($pair in @(@($freshAssembly, $trackedAssembly), @($freshSymbols, $trackedSymbols))) {
$freshHash = (Get-FileHash -LiteralPath $pair[0] -Algorithm SHA256).Hash
$trackedHash = (Get-FileHash -LiteralPath $pair[1] -Algorithm SHA256).Hash
if ($freshHash -ne $trackedHash) {
throw "Tracked Unity artifact is stale: $($pair[1]) differs from a fresh deterministic build"
}
}
$trackedSourceFiles = @(Get-ChildItem -LiteralPath $templateRoot -Recurse -File -Filter "*.cs")
if ($trackedSourceFiles.Count -ne 0) {
throw "CultMath Unity package must not compile repository C# sources: $($trackedSourceFiles.FullName -join ', ')"
}
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
Copy-Item -Path (Join-Path $templateRoot "*") -Destination $outputRoot -Recurse -Force
$stagedAssemblies = @(Get-ChildItem -LiteralPath (Join-Path $outputRoot "Runtime\Plugins") -Filter "*.dll")
$stagedSources = @(Get-ChildItem -LiteralPath $outputRoot -Recurse -File -Filter "*.cs")
$stagedFacades = @(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) {
throw "CultMath package must contain one owned auto-referenced DLL, no C# source files, and no asmdef facade"
}
Write-Host "CultMath Unity package: $outputRoot"
Write-Host "Package: $($manifest.name)@$($manifest.version)"
Write-Host "Managed assemblies: $($stagedAssemblies.Count)"
Write-Host "C# source files: $($stagedSources.Count)"
Write-Host "Asmdef facades: $($stagedFacades.Count)"

View file

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

View file

@ -0,0 +1,302 @@
#ifndef CULTMATH_HLSL
#define CULTMATH_HLSL
// CultMath HLSL mirror.
//
// HLSL already owns the float2/float3/float4 type names and most primitive
// intrinsics. This include names the CultMath semantic surface explicitly so
// shader code, C# wrappers, and Rust/native parity fixtures can share the same
// small vocabulary without local helper drift.
static const float CULTMATH_PI = 3.14159265358979323846;
static const float CULTMATH_TAU = 6.28318530717958647692;
static const float CULTMATH_HALF_PI = 1.57079632679489661923;
static const float CULTMATH_NORMALIZE_EPSILON = 1.0e-20;
float cultmath_radians(float degrees_value) { return degrees_value * (CULTMATH_PI / 180.0); }
float cultmath_degrees(float radians_value) { return radians_value * (180.0 / CULTMATH_PI); }
float cultmath_frac(float value) { return value - floor(value); }
float2 cultmath_frac(float2 value) { return value - floor(value); }
float3 cultmath_frac(float3 value) { return value - floor(value); }
float4 cultmath_frac(float4 value) { return value - floor(value); }
float cultmath_clamp(float value, float minimum, float maximum) { return min(max(value, minimum), maximum); }
float2 cultmath_clamp(float2 value, float2 minimum, float2 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); }
float cultmath_saturate(float value) { return cultmath_clamp(value, 0.0, 1.0); }
float2 cultmath_saturate(float2 value) { return cultmath_clamp(value, float2(0.0, 0.0), float2(1.0, 1.0)); }
float3 cultmath_saturate(float3 value) { return cultmath_clamp(value, float3(0.0, 0.0, 0.0), float3(1.0, 1.0, 1.0)); }
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)); }
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; }
float3 cultmath_lerp(float3 start, float3 end, float3 amount) { return start + (end - start) * amount; }
float4 cultmath_lerp(float4 start, float4 end, float4 amount) { return start + (end - start) * amount; }
float cultmath_step(float edge, float value) { return value < edge ? 0.0 : 1.0; }
float2 cultmath_step(float2 edge, float2 value) { return float2(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y)); }
float3 cultmath_step(float3 edge, float3 value) { return float3(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z)); }
float4 cultmath_step(float4 edge, float4 value) { return float4(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z), cultmath_step(edge.w, value.w)); }
float cultmath_smoothstep(float minimum, float maximum, float value)
{
float t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (3.0 - 2.0 * t);
}
float2 cultmath_smoothstep(float2 minimum, float2 maximum, float2 value)
{
float2 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (float2(3.0, 3.0) - float2(2.0, 2.0) * t);
}
float3 cultmath_smoothstep(float3 minimum, float3 maximum, float3 value)
{
float3 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (float3(3.0, 3.0, 3.0) - float3(2.0, 2.0, 2.0) * t);
}
float4 cultmath_smoothstep(float4 minimum, float4 maximum, float4 value)
{
float4 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (float4(3.0, 3.0, 3.0, 3.0) - float4(2.0, 2.0, 2.0, 2.0) * t);
}
float cultmath_smoothstep01(float value) { return cultmath_smoothstep(0.0, 1.0, value); }
float cultmath_smootherstep(float value)
{
value = cultmath_saturate(value);
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; }
float3 cultmath_simplex_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); }
float cultmath_simplex_noise(float2 value)
{
const float4 c = float4(
0.211324865405187,
0.366025403784439,
-0.577350269189626,
0.024390243902439);
float2 i = floor(value + dot(value, c.yy));
float2 x0 = value - i + dot(i, c.xx);
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);
i = cultmath_simplex_mod289(i);
float3 p = cultmath_simplex_permute(
cultmath_simplex_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);
m *= m;
m *= m;
float3 x = 2.0 * frac(p * c.w) - 1.0;
float3 h = abs(x) - 0.5;
float3 ox = floor(x + 0.5);
float3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
float3 g = float3(
a0.x * x0.x + h.x * x0.y,
a0.y * x12.x + h.y * x12.y,
a0.z * x12.z + h.z * x12.w);
return 130.0 * dot(m, g);
}
float cultmath_lengthsq(float2 value) { return dot(value, value); }
float cultmath_lengthsq(float3 value) { return dot(value, value); }
float cultmath_lengthsq(float4 value) { return dot(value, value); }
float cultmath_distance(float2 left, float2 right) { return length(left - right); }
float cultmath_distance(float3 left, float3 right) { return length(left - right); }
float cultmath_distance(float4 left, float4 right) { return length(left - right); }
float2 cultmath_normalize(float2 value) { return value / max(length(value), CULTMATH_NORMALIZE_EPSILON); }
float3 cultmath_normalize(float3 value) { return value / max(length(value), CULTMATH_NORMALIZE_EPSILON); }
float4 cultmath_normalize(float4 value) { return value / max(length(value), CULTMATH_NORMALIZE_EPSILON); }
float2 cultmath_reflect(float2 incident, float2 normal_value)
{
return incident - 2.0 * dot(normal_value, incident) * normal_value;
}
float3 cultmath_reflect(float3 incident, float3 normal_value)
{
return incident - 2.0 * dot(normal_value, incident) * normal_value;
}
float2 cultmath_rotate(float2 value, float radians_value)
{
float s = sin(radians_value);
float c = cos(radians_value);
return float2(value.x * c - value.y * s, value.x * s + value.y * c);
}
float2 cultmath_rotate_degrees(float2 value, float degrees_value)
{
return cultmath_rotate(value, cultmath_radians(degrees_value));
}
float cultmath_csum(float2 value) { return value.x + value.y; }
float cultmath_csum(float3 value) { return value.x + value.y + value.z; }
float cultmath_csum(float4 value) { return value.x + value.y + value.z + value.w; }
float cultmath_decay(float source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float2 cultmath_decay(float2 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float3 cultmath_decay(float3 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float4 cultmath_decay(float4 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float cultmath_damp(float start, float end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
float2 cultmath_damp(float2 start, float2 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
float3 cultmath_damp(float3 start, float3 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
float4 cultmath_damp(float4 start, float4 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
float cultmath_catmullrom(float p0, float p1, float p2, float p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float2 cultmath_catmullrom(float2 p0, float2 p1, float2 p2, float2 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float3 cultmath_catmullrom(float3 p0, float3 p1, float3 p2, float3 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float4 cultmath_catmullrom(float4 p0, float4 p1, float4 p2, float4 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float cultmath_quadratic_bezier(float p0, float p1, float p2, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * p0 + 2.0 * inv * t * p1 + t * t * p2;
}
float3 cultmath_quadratic_bezier(float3 p0, float3 p1, float3 p2, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * p0 + 2.0 * inv * t * p1 + t * t * p2;
}
float cultmath_cubic_bezier(float p0, float p1, float p2, float p3, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * inv * p0 + 3.0 * inv * inv * t * p1 + 3.0 * inv * t * t * p2 + t * t * t * p3;
}
float3 cultmath_cubic_bezier(float3 p0, float3 p1, float3 p2, float3 p3, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * inv * p0 + 3.0 * inv * inv * t * p1 + 3.0 * inv * t * t * p2 + t * t * t * p3;
}
float cultmath_hash(float value) { return cultmath_frac(sin(value) * 43758.5453); }
float cultmath_hash(float2 value) { return cultmath_hash(dot(value, float2(127.1, 311.7))); }
float cultmath_hash(float3 value) { return cultmath_hash(dot(value, float3(127.1, 311.7, 74.7))); }
float cultmath_value_noise(float2 position)
{
float2 cell = floor(position);
float2 local = cultmath_frac(position);
float2 u = local * local * (float2(3.0, 3.0) - float2(2.0, 2.0) * local);
float a = cultmath_hash(cell);
float b = cultmath_hash(cell + float2(1.0, 0.0));
float c = cultmath_hash(cell + float2(0.0, 1.0));
float d = cultmath_hash(cell + float2(1.0, 1.0));
return cultmath_lerp(cultmath_lerp(a, b, u.x), cultmath_lerp(c, d, u.x), u.y);
}
float cultmath_value_noise_bicubic(float2 position)
{
float2 cell = floor(position);
float2 local = cultmath_frac(position);
float y0 = cultmath_catmullrom(cultmath_hash(cell + float2(-1.0, -1.0)), cultmath_hash(cell + float2(0.0, -1.0)), cultmath_hash(cell + float2(1.0, -1.0)), cultmath_hash(cell + float2(2.0, -1.0)), local.x);
float y1 = cultmath_catmullrom(cultmath_hash(cell + float2(-1.0, 0.0)), cultmath_hash(cell + float2(0.0, 0.0)), cultmath_hash(cell + float2(1.0, 0.0)), cultmath_hash(cell + float2(2.0, 0.0)), local.x);
float y2 = cultmath_catmullrom(cultmath_hash(cell + float2(-1.0, 1.0)), cultmath_hash(cell + float2(0.0, 1.0)), cultmath_hash(cell + float2(1.0, 1.0)), cultmath_hash(cell + float2(2.0, 1.0)), local.x);
float y3 = cultmath_catmullrom(cultmath_hash(cell + float2(-1.0, 2.0)), cultmath_hash(cell + float2(0.0, 2.0)), cultmath_hash(cell + float2(1.0, 2.0)), cultmath_hash(cell + float2(2.0, 2.0)), local.x);
return cultmath_catmullrom(y0, y1, y2, y3, local.y);
}
float cultmath_value_noise_texture(Texture2D<float> noise_texture, SamplerState noise_sampler, float2 uv, float scale)
{
return noise_texture.SampleLevel(noise_sampler, uv * scale, 0.0).r;
}
float cultmath_value_noise_texture_bicubic(Texture2D<float> noise_texture, SamplerState noise_sampler, float2 uv, float scale, float2 texel_size)
{
float2 position = uv * scale;
float2 cell = floor(position);
float2 local = cultmath_frac(position);
float2 base_uv = cell / scale;
float row0 = cultmath_catmullrom(
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(-1.0, -1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(0.0, -1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(1.0, -1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(2.0, -1.0), 0.0).r,
local.x);
float row1 = cultmath_catmullrom(
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(-1.0, 0.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(0.0, 0.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(1.0, 0.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(2.0, 0.0), 0.0).r,
local.x);
float row2 = cultmath_catmullrom(
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(-1.0, 1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(0.0, 1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(1.0, 1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(2.0, 1.0), 0.0).r,
local.x);
float row3 = cultmath_catmullrom(
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(-1.0, 2.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(0.0, 2.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(1.0, 2.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(2.0, 2.0), 0.0).r,
local.x);
return cultmath_catmullrom(row0, row1, row2, row3, local.y);
}
#endif

View file

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

View file

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

View file

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

View file

@ -0,0 +1,245 @@
using System;
using System.Numerics;
namespace CultMath;
public static class BatchMath
{
private const float MinimumDistanceSquared = 1.0e-8f;
public static int LaneCount => Vector<float>.Count;
public static bool IsHardwareAccelerated => Vector.IsHardwareAccelerated;
public static void Clear(Span<float> values) => values.Clear();
public static void AddRadialFalloffAcceleration2D(
ReadOnlySpan<float> positionX,
ReadOnlySpan<float> positionY,
float centerX,
float centerY,
float strength,
float radius,
Span<float> accelerationX,
Span<float> accelerationY)
{
ValidateEqualLengths(positionX.Length, positionY.Length, accelerationX.Length, accelerationY.Length);
if (radius <= 0.0f || strength == 0.0f || positionX.Length == 0)
{
return;
}
var radiusVector = new Vector<float>(radius);
var centerXVector = new Vector<float>(centerX);
var centerYVector = new Vector<float>(centerY);
var strengthVector = new Vector<float>(strength);
var one = Vector<float>.One;
var zero = Vector<float>.Zero;
var minimumDistanceSquared = new Vector<float>(MinimumDistanceSquared);
var i = 0;
var lanes = Vector<float>.Count;
var pxBuffer = new float[lanes];
var pyBuffer = new float[lanes];
var axBuffer = new float[lanes];
var ayBuffer = new float[lanes];
for (; i <= positionX.Length - lanes; i += lanes)
{
CopySpanToArray(positionX.Slice(i, lanes), pxBuffer);
CopySpanToArray(positionY.Slice(i, lanes), pyBuffer);
CopySpanToArray(accelerationX.Slice(i, lanes), axBuffer);
CopySpanToArray(accelerationY.Slice(i, lanes), ayBuffer);
var px = new Vector<float>(pxBuffer);
var py = new Vector<float>(pyBuffer);
var dx = centerXVector - px;
var dy = centerYVector - py;
var distanceSquared = Vector.Max(dx * dx + dy * dy, minimumDistanceSquared);
var distance = Vector.SquareRoot(distanceSquared);
var inRange = Vector.LessThanOrEqual(distance, radiusVector);
var falloff = Vector.Max(zero, one - distance / radiusVector);
var magnitude = strengthVector * falloff;
var ax = Vector.ConditionalSelect(inRange, dx / distance * magnitude, zero);
var ay = Vector.ConditionalSelect(inRange, dy / distance * magnitude, zero);
(new Vector<float>(axBuffer) + ax).CopyTo(axBuffer);
(new Vector<float>(ayBuffer) + ay).CopyTo(ayBuffer);
CopyArrayToSpan(axBuffer, accelerationX.Slice(i, lanes));
CopyArrayToSpan(ayBuffer, accelerationY.Slice(i, lanes));
}
for (; i < positionX.Length; i++)
{
var dx = centerX - positionX[i];
var dy = centerY - positionY[i];
var distanceSquared = MathF.Max(dx * dx + dy * dy, MinimumDistanceSquared);
var distance = MathF.Sqrt(distanceSquared);
if (distance > radius)
{
continue;
}
var falloff = MathF.Max(0.0f, 1.0f - distance / radius);
var magnitude = strength * falloff;
accelerationX[i] += dx / distance * magnitude;
accelerationY[i] += dy / distance * magnitude;
}
}
public static void AddRadialFalloffAcceleration2D(
ReadOnlySpan<float2> positions,
float2 center,
float strength,
float radius,
Span<float2> acceleration)
{
ValidateEqualLengths(positions.Length, acceleration.Length);
if (radius <= 0.0f || strength == 0.0f || positions.Length == 0)
{
return;
}
for (var i = 0; i < positions.Length; i++)
{
var delta = center - positions[i];
var distanceSquared = MathF.Max(delta.x * delta.x + delta.y * delta.y, MinimumDistanceSquared);
var distance = MathF.Sqrt(distanceSquared);
if (distance > radius)
{
continue;
}
var falloff = MathF.Max(0.0f, 1.0f - distance / radius);
acceleration[i] += delta / distance * (strength * falloff);
}
}
public static void IntegrateSemiImplicitEuler2D(
float deltaTime,
ReadOnlySpan<float> dynamicMask,
Span<float> positionX,
Span<float> positionY,
Span<float> velocityX,
Span<float> velocityY,
ReadOnlySpan<float> accelerationX,
ReadOnlySpan<float> accelerationY)
{
if (deltaTime <= 0.0f)
{
throw new ArgumentOutOfRangeException(nameof(deltaTime), "Delta time must be positive.");
}
ValidateEqualLengths(
dynamicMask.Length,
positionX.Length,
positionY.Length,
velocityX.Length,
velocityY.Length,
accelerationX.Length,
accelerationY.Length);
var delta = new Vector<float>(deltaTime);
var i = 0;
var lanes = Vector<float>.Count;
var maskBuffer = new float[lanes];
var pxBuffer = new float[lanes];
var pyBuffer = new float[lanes];
var vxBuffer = new float[lanes];
var vyBuffer = new float[lanes];
var axBuffer = new float[lanes];
var ayBuffer = new float[lanes];
for (; i <= positionX.Length - lanes; i += lanes)
{
CopySpanToArray(dynamicMask.Slice(i, lanes), maskBuffer);
CopySpanToArray(velocityX.Slice(i, lanes), vxBuffer);
CopySpanToArray(velocityY.Slice(i, lanes), vyBuffer);
CopySpanToArray(positionX.Slice(i, lanes), pxBuffer);
CopySpanToArray(positionY.Slice(i, lanes), pyBuffer);
CopySpanToArray(accelerationX.Slice(i, lanes), axBuffer);
CopySpanToArray(accelerationY.Slice(i, lanes), ayBuffer);
var mask = new Vector<float>(maskBuffer);
var vx = new Vector<float>(vxBuffer);
var vy = new Vector<float>(vyBuffer);
var px = new Vector<float>(pxBuffer);
var py = new Vector<float>(pyBuffer);
var ax = new Vector<float>(axBuffer);
var ay = new Vector<float>(ayBuffer);
vx += ax * delta * mask;
vy += ay * delta * mask;
px += vx * delta * mask;
py += vy * delta * mask;
vx.CopyTo(vxBuffer);
vy.CopyTo(vyBuffer);
px.CopyTo(pxBuffer);
py.CopyTo(pyBuffer);
CopyArrayToSpan(vxBuffer, velocityX.Slice(i, lanes));
CopyArrayToSpan(vyBuffer, velocityY.Slice(i, lanes));
CopyArrayToSpan(pxBuffer, positionX.Slice(i, lanes));
CopyArrayToSpan(pyBuffer, positionY.Slice(i, lanes));
}
for (; i < positionX.Length; i++)
{
var mask = dynamicMask[i];
velocityX[i] += accelerationX[i] * deltaTime * mask;
velocityY[i] += accelerationY[i] * deltaTime * mask;
positionX[i] += velocityX[i] * deltaTime * mask;
positionY[i] += velocityY[i] * deltaTime * mask;
}
}
public static void IntegrateSemiImplicitEuler2D(
float deltaTime,
ReadOnlySpan<float> dynamicMask,
Span<float2> position,
Span<float2> velocity,
ReadOnlySpan<float2> acceleration)
{
if (deltaTime <= 0.0f)
{
throw new ArgumentOutOfRangeException(nameof(deltaTime), "Delta time must be positive.");
}
ValidateEqualLengths(dynamicMask.Length, position.Length, velocity.Length, acceleration.Length);
for (var i = 0; i < position.Length; i++)
{
var mask = dynamicMask[i];
velocity[i] += acceleration[i] * deltaTime * mask;
position[i] += velocity[i] * deltaTime * mask;
}
}
private static void ValidateEqualLengths(params int[] lengths)
{
var length = lengths[0];
for (var i = 1; i < lengths.Length; i++)
{
if (lengths[i] != length)
{
throw new ArgumentException("CultMath batch spans must have equal length.");
}
}
}
private static void CopySpanToArray(ReadOnlySpan<float> source, float[] destination)
{
for (var i = 0; i < source.Length; i++)
{
destination[i] = source[i];
}
}
private static void CopyArrayToSpan(float[] source, Span<float> destination)
{
for (var i = 0; i < destination.Length; i++)
{
destination[i] = source[i];
}
}
}

View file

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

View file

@ -0,0 +1,4 @@
namespace CultMath;
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly record struct Color32(byte r, byte g, byte b, byte a = 255);

View file

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

View file

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

View file

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

View file

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;netstandard2.1</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<RootNamespace>CultMath</RootNamespace>
<AssemblyName>CultMath</AssemblyName>
<PackageId>GameCult.Math</PackageId>
<Version>0.1.2</Version>
<Description>Clean-room C# math shaped for HLSL-style renderer code.</Description>
<Authors>GameCult</Authors>
<PackageReadmeFile>README.md</PackageReadmeFile>
<NoWarn>$(NoWarn);CS8981</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\shaders\CultMath.hlsl" Pack="true" PackagePath="contentFiles\any\any\shaders\CultMath.hlsl" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View file

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

View file

@ -0,0 +1,10 @@
namespace CultMath;
public enum CultMathTone : byte
{
Background = 0,
Panel = 1,
Edge = 2,
Header = 3,
Body = 4,
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,62 @@
namespace CultMath;
public struct Random
{
private const uint DefaultSeed = 0x6E624EB7u;
public uint state;
public Random(uint seed)
{
state = seed == 0 ? DefaultSeed : seed;
}
public static Random CreateFromIndex(uint index) => new(index + DefaultSeed);
public void InitState(uint seed)
{
state = seed == 0 ? DefaultSeed : seed;
}
public uint NextUInt()
{
var x = state == 0 ? DefaultSeed : state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
state = x == 0 ? DefaultSeed : x;
return state;
}
public int NextInt(int max) => NextInt(0, max);
public int NextInt(int min, int max)
{
if (max <= min)
{
return min;
}
return min + (int)(NextUInt() % (uint)(max - min));
}
public float NextFloat() => (NextUInt() >> 8) * (1.0f / 16777216.0f);
public float NextFloat(float max) => NextFloat() * max;
public float NextFloat(float min, float max) => min + NextFloat() * (max - min);
public float2 NextFloat2() => new(NextFloat(), NextFloat());
public float2 NextFloat2(float min, float max) => new(NextFloat(min, max), NextFloat(min, max));
public float2 NextFloat2(float2 min, float2 max) => min + NextFloat2() * (max - min);
public float2 NextFloat2Direction()
{
var angle = NextFloat(0.0f, math.TAU);
return new float2(math.cos(angle), math.sin(angle));
}
public float3 NextFloat3(float3 min, float3 max) => min + new float3(NextFloat(), NextFloat(), NextFloat()) * (max - min);
}

View file

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

View file

@ -0,0 +1,229 @@
using System;
using System.Runtime.InteropServices;
namespace CultMath;
public static class Voronoi
{
public static bool NativeAvailable { get; private set; } = true;
public static Color32 SampleTone(float x, float y, float resolutionY, int frameIndex, CultMathTone tone, float span)
{
Span<float> xs = stackalloc[] { x };
Span<float> ys = stackalloc[] { y };
Span<CultMathTone> tones = stackalloc[] { tone };
Span<float> spans = stackalloc[] { span };
Span<Color32> colors = stackalloc Color32[1];
SampleTones(xs, ys, tones, spans, resolutionY, frameIndex, colors);
return colors[0];
}
public static void SampleTones(
ReadOnlySpan<float> xs,
ReadOnlySpan<float> ys,
ReadOnlySpan<CultMathTone> tones,
ReadOnlySpan<float> spans,
float resolutionY,
int frameIndex,
Span<Color32> colors)
{
if (xs.Length != ys.Length || xs.Length != tones.Length || xs.Length != spans.Length || xs.Length != colors.Length)
{
throw new ArgumentException("CultMath Voronoi batch spans must have equal length.");
}
if (xs.IsEmpty)
{
return;
}
if (NativeAvailable && TrySampleNative(xs, ys, tones, spans, resolutionY, frameIndex, colors))
{
return;
}
for (var index = 0; index < xs.Length; index++)
{
colors[index] = SampleManaged(xs[index], ys[index], resolutionY, frameIndex, tones[index], spans[index]);
}
}
private static unsafe bool TrySampleNative(
ReadOnlySpan<float> xs,
ReadOnlySpan<float> ys,
ReadOnlySpan<CultMathTone> tones,
ReadOnlySpan<float> spans,
float resolutionY,
int frameIndex,
Span<Color32> colors)
{
try
{
fixed (float* xPtr = xs)
fixed (float* yPtr = ys)
fixed (CultMathTone* tonePtr = tones)
fixed (float* spanPtr = spans)
fixed (Color32* colorPtr = colors)
{
var result = Native.cultmath_apollonian_voronoi_tones(
xPtr,
yPtr,
(byte*)tonePtr,
spanPtr,
(nuint)xs.Length,
resolutionY,
frameIndex,
colorPtr);
if (result == 0)
{
return true;
}
}
}
catch (DllNotFoundException)
{
NativeAvailable = false;
}
catch (EntryPointNotFoundException)
{
NativeAvailable = false;
}
return false;
}
private static Color32 SampleManaged(float x, float y, float resolutionY, int frameIndex, CultMathTone tone, float span)
{
var sample = StableSample(x, y, resolutionY, frameIndex, tone, span);
var luminance = math.saturate(sample.x * 0.299f + sample.y * 0.587f + sample.z * 0.114f);
if (tone == CultMathTone.Header)
{
var glow = 0.62f + luminance * 0.38f;
return new Color32((byte)(255 * glow), (byte)(96 * glow), (byte)(14 * glow));
}
if (tone == CultMathTone.Body)
{
var glow = 0.68f + luminance * 0.32f;
return new Color32((byte)(245 * glow), (byte)(248 * glow), (byte)(252 * glow));
}
var gain = tone switch
{
CultMathTone.Panel => 0.42f,
CultMathTone.Edge => 1.05f,
_ => 0.24f,
};
var lift = tone switch
{
CultMathTone.Panel => 14.0f,
CultMathTone.Edge => 24.0f,
_ => 4.0f,
};
return new Color32(
(byte)Math.Clamp(lift + sample.x * 255.0f * gain, 0, 255),
(byte)Math.Clamp(lift + sample.y * 255.0f * gain, 0, 255),
(byte)Math.Clamp(lift + sample.z * 255.0f * gain, 0, 255));
}
private static float3 StableSample(float x, float y, float resolutionY, int frameIndex, CultMathTone tone, float span)
{
var seed = x * 12.9898f + y * 78.233f + Hash1(((byte)tone + 1.0f) * 19.19f);
var jitter = new float2((Hash1(seed) - 0.5f) * span, (Hash1(seed + 37.17f) - 0.5f) * span);
return SampleField(x + jitter.x, y + jitter.y, resolutionY, frameIndex);
}
private static float3 SampleField(float px, float py, float resolutionY, int frameIndex)
{
var time = frameIndex / 120.0f;
var p = new float2(6.0f * px / Math.Max(1.0f, resolutionY), 6.0f * py / Math.Max(1.0f, resolutionY));
var n = math.floor(p);
var f = math.frac(p);
var distance = 8.0f;
var color = float3.zero;
const float smoothness = 0.005f;
for (var j = -2; j <= 2; j++)
{
for (var i = -2; i <= 2; i++)
{
var g = new float2(i, j);
var o = Hash2(n + g);
var weight = o.x * 0.5f + 0.5f;
o = 0.5f + 0.5f * new float2(math.sin(time + 6.2831f * o.x), math.sin(time + 6.2831f * o.y));
var d2 = math.abs(g - f + o);
var d = Math.Max(d2.x, d2.y) * weight;
var seed = Hash1(math.dot(n + g, new float2(7.0f, 113.0f)));
var hue = math.frac((n.x + g.x) * 0.173f + (n.y + g.y) * 0.379f + seed * 0.431f);
var candidate = PastelSpectrum(hue);
var h = math.smoothstep(0.0f, 1.0f, 0.5f + 0.5f * (distance - d) / smoothness);
var correction = h * (1.0f - h) * smoothness / (1.0f + 3.0f * smoothness);
distance = math.lerp(distance, d, h) - correction;
color = math.lerp(color, candidate, h) - correction;
}
}
return math.max(float3.zero, color * (1.0f - 0.1f * math.smoothstep(0.04f, 0.05f, distance)));
}
private static float Hash1(float value) => math.frac(math.sin(value) * 43758.5453f);
private static float3 PastelSpectrum(float hue)
{
return HsvToRgb(hue, 0.52f, 0.98f);
}
private static float3 HsvToRgb(float hue, float saturation, float value)
{
var h = math.frac(hue) * 6.0f;
var c = value * saturation;
var x = c * (1.0f - Math.Abs(h % 2.0f - 1.0f));
var m = value - c;
float3 rgb;
if (h < 1.0f)
{
rgb = new float3(c, x, 0.0f);
}
else if (h < 2.0f)
{
rgb = new float3(x, c, 0.0f);
}
else if (h < 3.0f)
{
rgb = new float3(0.0f, c, x);
}
else if (h < 4.0f)
{
rgb = new float3(0.0f, x, c);
}
else if (h < 5.0f)
{
rgb = new float3(x, 0.0f, c);
}
else
{
rgb = new float3(c, 0.0f, x);
}
return rgb + m;
}
private static float2 Hash2(float2 value)
{
var p = new float2(math.dot(value, new float2(127.1f, 311.7f)), math.dot(value, new float2(269.5f, 183.3f)));
return math.frac(new float2(math.sin(p.x) * 43758.5453f, math.sin(p.y) * 43758.5453f));
}
private static unsafe partial class Native
{
[DllImport("cultmath_core", CallingConvention = CallingConvention.Cdecl)]
public static extern int cultmath_apollonian_voronoi_tones(
float* xs,
float* ys,
byte* tones,
float* spans,
nuint count,
float resolutionY,
int frameIndex,
Color32* outColors);
}
}

View file

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

View file

@ -0,0 +1,18 @@
using System;
namespace CultMath;
public record struct bool2(bool x, bool y)
{
public static readonly bool2 @false = new(false, false);
public static readonly bool2 @true = new(true, true);
public bool this[int index] => index switch
{
0 => x,
1 => y,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public static implicit operator bool2(bool value) => new(value, value);
}

View file

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

View file

@ -0,0 +1,25 @@
using System;
namespace CultMath;
public record struct double2(double x, double y)
{
public static readonly double2 zero = new(0.0, 0.0);
public static readonly double2 one = new(1.0, 1.0);
public double this[int index] => index switch
{
0 => x,
1 => y,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public static implicit operator double2(double value) => new(value, value);
public static double2 operator +(double2 value) => value;
public static double2 operator -(double2 value) => new(-value.x, -value.y);
public static double2 operator +(double2 left, double2 right) => new(left.x + right.x, left.y + right.y);
public static double2 operator -(double2 left, double2 right) => new(left.x - right.x, left.y - right.y);
public static double2 operator *(double2 left, double2 right) => new(left.x * right.x, left.y * right.y);
public static double2 operator /(double2 left, double2 right) => new(left.x / right.x, left.y / right.y);
}

View file

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

View file

@ -0,0 +1,36 @@
using System;
namespace CultMath;
public record struct double3(double x, double y, double z)
{
public static readonly double3 zero = new(0.0, 0.0, 0.0);
public static readonly double3 one = new(1.0, 1.0, 1.0);
public double2 xy
{
get => new(x, y);
set
{
x = value.x;
y = value.y;
}
}
public double this[int index] => index switch
{
0 => x,
1 => y,
2 => z,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public static implicit operator double3(double value) => new(value, value, value);
public static double3 operator +(double3 value) => value;
public static double3 operator -(double3 value) => new(-value.x, -value.y, -value.z);
public static double3 operator +(double3 left, double3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z);
public static double3 operator -(double3 left, double3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z);
public static double3 operator *(double3 left, double3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z);
public static double3 operator /(double3 left, double3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z);
}

View file

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

View file

@ -0,0 +1,28 @@
using System;
namespace CultMath;
public record struct float2(float x, float y)
{
public static readonly float2 zero = new(0.0f, 0.0f);
public static readonly float2 one = new(1.0f, 1.0f);
public float this[int index] => index switch
{
0 => x,
1 => y,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
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 float2(System.Numerics.Vector2 value) => new(value.X, value.Y);
public static float2 operator +(float2 value) => value;
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, 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, float2 right) => new(left.x % right.x, left.y % right.y);
}

View file

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

View file

@ -0,0 +1,79 @@
using System;
namespace CultMath;
public record struct float3(float x, float y, float z)
{
public static readonly float3 zero = new(0.0f, 0.0f, 0.0f);
public static readonly float3 one = new(1.0f, 1.0f, 1.0f);
public float3(float2 xy, float z)
: this(xy.x, xy.y, z)
{
}
public float2 xy
{
get => new(x, y);
set
{
x = value.x;
y = value.y;
}
}
public float2 xz
{
get => new(x, z);
set
{
x = value.x;
z = value.y;
}
}
public float2 yz
{
get => new(y, z);
set
{
y = value.x;
z = value.y;
}
}
public float2 zy
{
get => new(z, y);
set
{
z = value.x;
y = value.y;
}
}
public float3 yzx => new(y, z, x);
public float3 zyx => new(z, y, x);
public float this[int index] => index switch
{
0 => x,
1 => y,
2 => z,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
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 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) => 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, 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, float3 right) => new(left.x % right.x, left.y % right.y, left.z % right.z);
}

View file

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

View file

@ -0,0 +1,58 @@
using System;
namespace CultMath;
public record struct float4(float x, float y, float z, float w)
{
public static readonly float4 zero = new(0.0f, 0.0f, 0.0f, 0.0f);
public static readonly float4 one = new(1.0f, 1.0f, 1.0f, 1.0f);
public float4(float2 xy, float z, float w)
: this(xy.x, xy.y, z, w)
{
}
public float4(float3 xyz, float w)
: this(xyz.x, xyz.y, xyz.z, w)
{
}
public float4(float x, float3 yzw)
: this(x, yzw.x, yzw.y, yzw.z)
{
}
public float2 xy
{
get => new(x, y);
set
{
x = value.x;
y = value.y;
}
}
public float3 xyw => new(x, y, w);
public float3 yzx => new(y, z, x);
public float3 xyz => new(x, y, z);
public float this[int index] => index switch
{
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 explicit operator System.Numerics.Vector4(float4 value) => new(value.x, value.y, value.z, value.w);
public static explicit operator float4(System.Numerics.Vector4 value) => new(value.X, value.Y, value.Z, value.W);
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 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, 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, float4 right) => new(left.x % right.x, left.y % right.y, left.z % right.z, left.w % right.w);
}

View file

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

View file

@ -0,0 +1,29 @@
using System;
namespace CultMath;
public record struct int2(int x, int y)
{
public static readonly int2 zero = new(0, 0);
public static readonly int2 one = new(1, 1);
public int this[int index] => index switch
{
0 => x,
1 => y,
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
public static implicit operator int2(int value) => new(value, value);
public static implicit operator float2(int2 value) => new(value.x, value.y);
public static explicit operator System.Numerics.Vector2(int2 value) => new(value.x, value.y);
public static explicit operator int2(System.Numerics.Vector2 value) => new((int)value.X, (int)value.Y);
public static int2 operator +(int2 value) => value;
public static int2 operator -(int2 value) => new(-value.x, -value.y);
public static int2 operator +(int2 left, int2 right) => new(left.x + right.x, left.y + right.y);
public static int2 operator -(int2 left, int2 right) => new(left.x - right.x, left.y - right.y);
public static int2 operator *(int2 left, int2 right) => new(left.x * right.x, left.y * right.y);
public static int2 operator /(int2 left, int2 right) => new(left.x / right.x, left.y / right.y);
public static int2 operator %(int2 left, int2 right) => new(left.x % right.x, left.y % right.y);
}

View file

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

View file

@ -0,0 +1,417 @@
using System;
namespace CultMath;
public static class math
{
public const float PI = MathF.PI;
public const float TAU = MathF.PI * 2.0f;
public const float HALF_PI = MathF.PI * 0.5f;
public static float2 float2(float x, float y) => new(x, y);
public static float2 float2(float value) => new(value, value);
public static bool2 bool2(bool x, bool y) => new(x, y);
public static bool2 bool2(bool value) => new(value, value);
public static int2 int2(int x, int y) => new(x, y);
public static int2 int2(int value) => new(value, value);
public static float3 float3(float x, float y, float z) => new(x, y, z);
public static float3 float3(float2 xy, float z) => new(xy, z);
public static float3 float3(float value) => new(value, value, value);
public static float4 float4(float x, float y, float z, float w) => new(x, y, z, w);
public static float4 float4(float2 xy, float z, float w) => new(xy, z, w);
public static float4 float4(float3 xyz, float w) => new(xyz, w);
public static float4 float4(float x, float3 yzw) => new(x, yzw);
public static float4 float4(float value) => new(value, value, value, value);
public static float radians(float degrees) => degrees * (PI / 180.0f);
public static float degrees(float radians) => radians * (180.0f / PI);
public static float sin(float value) => MathF.Sin(value);
public static float cos(float value) => MathF.Cos(value);
public static float tan(float value) => MathF.Tan(value);
public static float asin(float value) => MathF.Asin(value);
public static float acos(float value) => MathF.Acos(value);
public static float atan(float value) => MathF.Atan(value);
public static float atan2(float y, float x) => MathF.Atan2(y, x);
public static float sqrt(float value) => MathF.Sqrt(value);
public static float pow(float value, float power) => MathF.Pow(value, power);
public static bool isinf(float value) => float.IsInfinity(value);
public static float exp(float value) => MathF.Exp(value);
public static double exp(double value) => Math.Exp(value);
public static float2 exp(float2 value) => new(exp(value.x), exp(value.y));
public static float3 exp(float3 value) => new(exp(value.x), exp(value.y), exp(value.z));
public static float4 exp(float4 value) => new(exp(value.x), exp(value.y), exp(value.z), exp(value.w));
public static double2 exp(double2 value) => new(exp(value.x), exp(value.y));
public static double3 exp(double3 value) => new(exp(value.x), exp(value.y), exp(value.z));
public static float abs(float value) => MathF.Abs(value);
public static float2 abs(float2 value) => new(abs(value.x), abs(value.y));
public static float3 abs(float3 value) => new(abs(value.x), abs(value.y), abs(value.z));
public static float4 abs(float4 value) => new(abs(value.x), abs(value.y), abs(value.z), abs(value.w));
public static float sign(float value) => MathF.Sign(value);
public static float2 sign(float2 value) => new(sign(value.x), sign(value.y));
public static float3 sign(float3 value) => new(sign(value.x), sign(value.y), sign(value.z));
public static float4 sign(float4 value) => new(sign(value.x), sign(value.y), sign(value.z), sign(value.w));
public static float floor(float value) => MathF.Floor(value);
public static float2 floor(float2 value) => new(floor(value.x), floor(value.y));
public static float3 floor(float3 value) => new(floor(value.x), floor(value.y), floor(value.z));
public static float4 floor(float4 value) => new(floor(value.x), floor(value.y), floor(value.z), floor(value.w));
public static float ceil(float value) => MathF.Ceiling(value);
public static float2 ceil(float2 value) => new(ceil(value.x), ceil(value.y));
public static float3 ceil(float3 value) => new(ceil(value.x), ceil(value.y), ceil(value.z));
public static float4 ceil(float4 value) => new(ceil(value.x), ceil(value.y), ceil(value.z), ceil(value.w));
public static float frac(float value) => value - floor(value);
public static float2 frac(float2 value) => value - floor(value);
public static float3 frac(float3 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);
public static int min(int left, int right) => Math.Min(left, right);
public static float2 min(float2 left, float2 right) => new(min(left.x, right.x), min(left.y, right.y));
public static float3 min(float3 left, float3 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z));
public static float4 min(float4 left, float4 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z), min(left.w, right.w));
public static float max(float left, float right) => MathF.Max(left, right);
public static int max(int left, int right) => Math.Max(left, right);
public static float2 max(float2 left, float2 right) => new(max(left.x, right.x), max(left.y, right.y));
public static float3 max(float3 left, float3 right) => new(max(left.x, right.x), max(left.y, right.y), max(left.z, right.z));
public static float4 max(float4 left, float4 right) => new(max(left.x, right.x), max(left.y, right.y), max(left.z, right.z), max(left.w, right.w));
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 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 float saturate(float value) => clamp(value, 0.0f, 1.0f);
public static float2 saturate(float2 value) => clamp(value, 0.0f, 1.0f);
public static float3 saturate(float3 value) => clamp(value, 0.0f, 1.0f);
public static float4 saturate(float4 value) => clamp(value, 0.0f, 1.0f);
public static float lerp(float start, float end, float amount) => start + (end - start) * amount;
public static double lerp(double start, double end, double amount) => start + (end - start) * amount;
public static float2 lerp(float2 start, float2 end, float2 amount) => start + (end - start) * amount;
public static float3 lerp(float3 start, float3 end, float3 amount) => start + (end - start) * amount;
public static float4 lerp(float4 start, float4 end, float4 amount) => start + (end - start) * amount;
public static double2 lerp(double2 start, double2 end, double2 amount) => start + (end - start) * amount;
public static double3 lerp(double3 start, double3 end, double3 amount) => start + (end - start) * amount;
public static float unlerp(float start, float end, float value) => (value - start) / (end - start);
public static double unlerp(double start, double end, double value) => (value - start) / (end - start);
public static float2 unlerp(float2 start, float2 end, float2 value) => (value - start) / (end - start);
public static float3 unlerp(float3 start, float3 end, float3 value) => (value - start) / (end - start);
public static float4 unlerp(float4 start, float4 end, float4 value) => (value - start) / (end - start);
public static double2 unlerp(double2 start, double2 end, double2 value) => (value - start) / (end - start);
public static double3 unlerp(double3 start, double3 end, double3 value) => (value - start) / (end - start);
public static float step(float edge, float value) => value < edge ? 0.0f : 1.0f;
public static float2 step(float2 edge, float2 value) => new(step(edge.x, value.x), step(edge.y, value.y));
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 float4 step(float4 edge, float4 value) => new(step(edge.x, value.x), step(edge.y, value.y), step(edge.z, value.z), step(edge.w, value.w));
public static float smoothstep(float minimum, float maximum, float value)
{
var t = saturate((value - minimum) / (maximum - minimum));
return t * t * (3.0f - 2.0f * t);
}
public static float2 smoothstep(float2 minimum, float2 maximum, float2 value)
{
var t = saturate((value - minimum) / (maximum - minimum));
return t * t * (3.0f - 2.0f * t);
}
public static float3 smoothstep(float3 minimum, float3 maximum, float3 value)
{
var t = saturate((value - minimum) / (maximum - minimum));
return t * t * (3.0f - 2.0f * t);
}
public static float4 smoothstep(float4 minimum, float4 maximum, float4 value)
{
var t = saturate((value - minimum) / (maximum - minimum));
return t * t * (3.0f - 2.0f * t);
}
public static float dot(float2 left, float2 right) => left.x * right.x + left.y * right.y;
public static float dot(float3 left, float3 right) => left.x * right.x + left.y * right.y + left.z * right.z;
public static float dot(float4 left, float4 right) => left.x * right.x + left.y * right.y + left.z * right.z + left.w * right.w;
public static float3 cross(float3 left, float3 right) =>
new(
left.y * right.z - left.z * right.y,
left.z * right.x - left.x * right.z,
left.x * right.y - left.y * right.x);
public static float lengthsq(float2 value) => dot(value, value);
public static float lengthsq(float3 value) => dot(value, value);
public static float lengthsq(float4 value) => dot(value, value);
public static float length(float2 value) => MathF.Sqrt(lengthsq(value));
public static float length(float3 value) => MathF.Sqrt(lengthsq(value));
public static float length(float4 value) => MathF.Sqrt(lengthsq(value));
public static float distance(float2 left, float2 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 float2 normalize(float2 value) => value / MathF.Max(length(value), 1.0e-20f);
public static float3 normalize(float3 value) => value / MathF.Max(length(value), 1.0e-20f);
public static float4 normalize(float4 value) => value / MathF.Max(length(value), 1.0e-20f);
public static quaternion normalize(quaternion value)
{
var length = MathF.Sqrt(value.x * value.x + value.y * value.y + value.z * value.z + value.w * value.w);
if (length <= 1.0e-20f)
{
return quaternion.identity;
}
var invLength = 1.0f / length;
return new quaternion(value.x * invLength, value.y * invLength, value.z * invLength, value.w * invLength);
}
public static float2 reflect(float2 incident, float2 normal) => incident - 2.0f * dot(normal, incident) * normal;
public static float3 reflect(float3 incident, float3 normal) => incident - 2.0f * dot(normal, incident) * normal;
public static float2 rotate(float2 value, float radians)
{
var s = sin(radians);
var c = cos(radians);
return new float2(value.x * c - value.y * s, value.x * s + value.y * c);
}
public static float2 rotate_degrees(float2 value, float degrees) => rotate(value, radians(degrees));
public static float csum(float2 value) => value.x + value.y;
public static float csum(float3 value) => value.x + value.y + value.z;
public static float csum(float4 value) => value.x + value.y + value.z + value.w;
public static float decay(float source, float lambda, float dt) => source * exp(-lambda * dt);
public static float2 decay(float2 source, float lambda, float dt) => source * exp(-lambda * dt);
public static float3 decay(float3 source, float lambda, float dt) => source * exp(-lambda * dt);
public static float4 decay(float4 source, float lambda, float dt) => source * exp(-lambda * dt);
public static float2 decay(float2 source, float2 lambda, float dt) => source * exp(-lambda * dt);
public static float3 decay(float3 source, float3 lambda, float dt) => source * exp(-lambda * dt);
public static float4 decay(float4 source, float4 lambda, float dt) => source * exp(-lambda * dt);
public static float damp(float start, float end, float lambda, float dt) => lerp(start, end, 1.0f - exp(-lambda * dt));
public static float2 damp(float2 start, float2 end, float lambda, float dt) => lerp(start, end, 1.0f - exp(-lambda * dt));
public static float3 damp(float3 start, float3 end, float lambda, float dt) => lerp(start, end, 1.0f - exp(-lambda * dt));
public static float4 damp(float4 start, float4 end, float lambda, float dt) => lerp(start, end, 1.0f - exp(-lambda * dt));
public static float first_order_intercept_time(float shotSpeed, float3 targetRelativePosition, float3 targetRelativeVelocity)
{
var velocitySquared = lengthsq(targetRelativeVelocity);
if (velocitySquared < 0.001f)
{
return 0.0f;
}
var a = velocitySquared - shotSpeed * shotSpeed;
if (abs(a) < 0.001f)
{
var time = -lengthsq(targetRelativePosition) / (2.0f * dot(targetRelativeVelocity, targetRelativePosition));
return max(time, 0.0f);
}
var b = 2.0f * dot(targetRelativeVelocity, targetRelativePosition);
var c = lengthsq(targetRelativePosition);
var determinant = b * b - 4.0f * a * c;
if (determinant > 0.0f)
{
var root = sqrt(determinant);
var t1 = (-b + root) / (2.0f * a);
var t2 = (-b - root) / (2.0f * a);
if (t1 > 0.0f)
{
return t2 > 0.0f ? min(t1, t2) : t1;
}
return max(t2, 0.0f);
}
if (determinant < 0.0f)
{
return 0.0f;
}
return max(-b / (2.0f * a), 0.0f);
}
public static float3 first_order_intercept(
float3 shooterPosition,
float3 shooterVelocity,
float shotSpeed,
float3 targetPosition,
float3 targetVelocity)
{
var targetRelativePosition = targetPosition - shooterPosition;
var targetRelativeVelocity = targetVelocity - shooterVelocity;
var time = first_order_intercept_time(shotSpeed, targetRelativePosition, targetRelativeVelocity);
return targetPosition + time * targetRelativeVelocity;
}
public static float distance_to_segment(float2 point, float2 start, float2 end, out float2 closest)
{
var segment = end - start;
var segmentLengthSq = lengthsq(segment);
if (segmentLengthSq <= 0.0f)
{
closest = start;
return distance(point, start);
}
var t = saturate(dot(point - start, segment) / segmentLengthSq);
closest = start + segment * t;
return distance(point, closest);
}
public static float catmullrom(float p0, float p1, float p2, float p3, float t)
{
var t2 = t * t;
var t3 = t2 * t;
return 0.5f * ((2.0f * p1) + (p2 - p0) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3);
}
public static float2 catmullrom(float2 p0, float2 p1, float2 p2, float2 p3, float t) =>
0.5f * ((2.0f * p1) + (p2 - p0) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * (t * t) + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * (t * t * t));
public static float3 catmullrom(float3 p0, float3 p1, float3 p2, float3 p3, float t) =>
0.5f * ((2.0f * p1) + (p2 - p0) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * (t * t) + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * (t * t * t));
public static float4 catmullrom(float4 p0, float4 p1, float4 p2, float4 p3, float t) =>
0.5f * ((2.0f * p1) + (p2 - p0) * t + (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * (t * t) + (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * (t * t * t));
public static float quadratic_bezier(float p0, float p1, float p2, float t)
{
t = saturate(t);
var inv = 1.0f - t;
return inv * inv * p0 + 2.0f * inv * t * p1 + t * t * p2;
}
public static float3 quadratic_bezier(float3 p0, float3 p1, float3 p2, float t)
{
t = saturate(t);
var inv = 1.0f - t;
return inv * inv * p0 + 2.0f * inv * t * p1 + t * t * p2;
}
public static float cubic_bezier(float p0, float p1, float p2, float p3, float t)
{
t = saturate(t);
var inv = 1.0f - t;
return inv * inv * inv * p0 + 3.0f * inv * inv * t * p1 + 3.0f * inv * t * t * p2 + t * t * t * p3;
}
public static float3 cubic_bezier(float3 p0, float3 p1, float3 p2, float3 p3, float t)
{
t = saturate(t);
var inv = 1.0f - t;
return inv * inv * inv * p0 + 3.0f * inv * inv * t * p1 + 3.0f * inv * t * t * p2 + t * t * t * p3;
}
public static float smoothstep01(float value) => smoothstep(0.0f, 1.0f, value);
public static float smootherstep(float value)
{
value = saturate(value);
return value * value * value * (value * (value * 6.0f - 15.0f) + 10.0f);
}
public static float hash(float value) => frac(sin(value) * 43758.5453f);
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)));
// Ashima Arts / Ian McEwan 2D simplex noise, kept component-explicit so
// the C# and HLSL mirrors preserve the same float32 evaluation order.
public static float simplex_noise(float2 value)
{
var c = new float4(
0.211324865405187f,
0.366025403784439f,
-0.577350269189626f,
0.024390243902439f);
var i = floor(value + dot(value, new float2(c.y, c.y)));
var x0 = value - i + dot(i, new float2(c.x, c.x));
var i1 = x0.x > x0.y ? new float2(1.0f, 0.0f) : new float2(0.0f, 1.0f);
var x12 = new float4(
x0.x + c.x - i1.x,
x0.y + c.x - i1.y,
x0.x + c.z,
x0.y + c.z);
i = simplex_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 m = max(0.5f - new float3(
dot(x0, x0),
x12.x * x12.x + x12.y * x12.y,
x12.z * x12.z + x12.w * x12.w), 0.0f);
m = m * m;
m = m * m;
var x = 2.0f * frac(p * c.w) - 1.0f;
var h = abs(x) - 0.5f;
var ox = floor(x + 0.5f);
var a0 = x - ox;
m *= 1.79284291400159f - 0.85373472095314f * (a0 * a0 + h * h);
var g = new float3(
a0.x * x0.x + h.x * x0.y,
a0.y * x12.x + h.y * x12.y,
a0.z * x12.z + h.z * x12.w);
return 130.0f * dot(m, g);
}
private static float2 simplex_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 simplex_permute(float3 value) => simplex_mod289(((value * 34.0f) + 1.0f) * value);
public static float value_noise(float2 position)
{
var cell = floor(position);
var local = frac(position);
var u = local * local * (3.0f - 2.0f * local);
var a = hash(cell);
var b = hash(cell + new float2(1.0f, 0.0f));
var c = hash(cell + new float2(0.0f, 1.0f));
var d = hash(cell + new float2(1.0f, 1.0f));
return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y);
}
public static float value_noise_bicubic(float2 position)
{
var cell = floor(position);
var local = frac(position);
var y0 = catmullrom(
hash(cell + new float2(-1.0f, -1.0f)),
hash(cell + new float2(0.0f, -1.0f)),
hash(cell + new float2(1.0f, -1.0f)),
hash(cell + new float2(2.0f, -1.0f)),
local.x);
var y1 = catmullrom(
hash(cell + new float2(-1.0f, 0.0f)),
hash(cell + new float2(0.0f, 0.0f)),
hash(cell + new float2(1.0f, 0.0f)),
hash(cell + new float2(2.0f, 0.0f)),
local.x);
var y2 = catmullrom(
hash(cell + new float2(-1.0f, 1.0f)),
hash(cell + new float2(0.0f, 1.0f)),
hash(cell + new float2(1.0f, 1.0f)),
hash(cell + new float2(2.0f, 1.0f)),
local.x);
var y3 = catmullrom(
hash(cell + new float2(-1.0f, 2.0f)),
hash(cell + new float2(0.0f, 2.0f)),
hash(cell + new float2(1.0f, 2.0f)),
hash(cell + new float2(2.0f, 2.0f)),
local.x);
return catmullrom(y0, y1, y2, y3, local.y);
}
}

View file

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

View file

@ -0,0 +1,68 @@
namespace CultMath;
public record struct quaternion(float x, float y, float z, float w)
{
public static readonly quaternion identity = new(0.0f, 0.0f, 0.0f, 1.0f);
public static quaternion LookRotation(float3 forward, float3 up)
{
forward = math.normalize(forward);
up = math.normalize(up);
var right = math.normalize(math.cross(up, forward));
up = math.cross(forward, right);
var m00 = right.x;
var m01 = up.x;
var m02 = forward.x;
var m10 = right.y;
var m11 = up.y;
var m12 = forward.y;
var m20 = right.z;
var m21 = up.z;
var m22 = forward.z;
var trace = m00 + m11 + m22;
if (trace > 0.0f)
{
var s = math.sqrt(trace + 1.0f) * 2.0f;
return math.normalize(new quaternion(
(m21 - m12) / s,
(m02 - m20) / s,
(m10 - m01) / s,
0.25f * s));
}
if (m00 > m11 && m00 > m22)
{
var s = math.sqrt(1.0f + m00 - m11 - m22) * 2.0f;
return math.normalize(new quaternion(
0.25f * s,
(m01 + m10) / s,
(m02 + m20) / s,
(m21 - m12) / s));
}
if (m11 > m22)
{
var s = math.sqrt(1.0f + m11 - m00 - m22) * 2.0f;
return math.normalize(new quaternion(
(m01 + m10) / s,
0.25f * s,
(m12 + m21) / s,
(m02 - m20) / s));
}
{
var s = math.sqrt(1.0f + m22 - m00 - m11) * 2.0f;
return math.normalize(new quaternion(
(m02 + m20) / s,
(m12 + m21) / s,
0.25f * s,
(m10 - m01) / s));
}
}
public static implicit operator quaternion(float4 value) => new(value.x, value.y, value.z, value.w);
public static implicit operator float4(quaternion value) => new(value.x, value.y, value.z, value.w);
}

View file

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

View file

@ -0,0 +1,59 @@
namespace CultMath;
/// <summary>
/// Axis-aligned 2D bounds stored canonically as normalized minimum and maximum
/// corners. Point containment and intersection include the boundary.
/// </summary>
public readonly record struct rect
{
public rect(float minX, float minY, float maxX, float maxY)
: this(new float2(minX, minY), new float2(maxX, maxY))
{
}
public rect(float2 min, float2 max)
{
this.min = new float2(MathF.Min(min.x, max.x), MathF.Min(min.y, max.y));
this.max = new float2(MathF.Max(min.x, max.x), MathF.Max(min.y, max.y));
}
public float2 min { get; }
public float2 max { get; }
public float width => max.x - min.x;
public float height => max.y - min.y;
public float2 size => max - min;
public float2 center => (min + max) * 0.5f;
public float area => width * height;
public bool isEmpty => width == 0.0f || height == 0.0f;
public bool Contains(float2 point) =>
point.x >= min.x && point.x <= max.x &&
point.y >= min.y && point.y <= max.y;
public bool Contains(rect other) =>
other.min.x >= min.x && other.max.x <= max.x &&
other.min.y >= min.y && other.max.y <= max.y;
public bool Intersects(rect other) =>
min.x <= other.max.x && max.x >= other.min.x &&
min.y <= other.max.y && max.y >= other.min.y;
public rect Intersection(rect other)
{
if (!Intersects(other))
return new rect(0.0f, 0.0f, 0.0f, 0.0f);
return new rect(
MathF.Max(min.x, other.min.x),
MathF.Max(min.y, other.min.y),
MathF.Min(max.x, other.max.x),
MathF.Min(max.y, other.max.y));
}
public rect Encapsulate(float2 point) => new(
MathF.Min(min.x, point.x),
MathF.Min(min.y, point.y),
MathF.Max(max.x, point.x),
MathF.Max(max.y, point.y));
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5aab0d8e1ca94d42b85bba780a7ee449

View file

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

View file

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

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit.v3" Version="3.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CultMath\CultMath.csproj" />
</ItemGroup>
</Project>

View file

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

View file

@ -0,0 +1,76 @@
using Xunit;
namespace CultMath.Tests;
public sealed class HlslMirrorTests
{
[Fact]
public void HlslMirrorDoesNotPublishGeometryKernels()
{
var source = File.ReadAllText(Path.Combine(GetCultMathRoot(), "shaders", "CultMath.hlsl"));
Assert.DoesNotContain("AdvancedErosionFilter.hlsl", source);
Assert.DoesNotContain("Planetary.hlsl", source);
Assert.DoesNotContain("spherical_erosion", source);
Assert.DoesNotContain("planetary_radial_refinement", source);
}
[Fact]
public void HlslMirrorPublishesCultMathPrimitives()
{
var include = File.ReadAllText(Path.Combine(GetCultMathRoot(), "shaders", "CultMath.hlsl"));
var requiredSymbols = new[]
{
"cultmath_radians",
"cultmath_degrees",
"cultmath_frac",
"cultmath_clamp",
"cultmath_saturate",
"cultmath_lerp",
"cultmath_step",
"cultmath_smoothstep",
"cultmath_smootherstep",
"cultmath_lengthsq",
"cultmath_distance",
"cultmath_normalize",
"cultmath_reflect",
"cultmath_rotate",
"cultmath_csum",
"cultmath_decay",
"cultmath_damp",
"cultmath_catmullrom",
"cultmath_quadratic_bezier",
"cultmath_cubic_bezier",
"cultmath_hash",
"cultmath_value_noise",
"cultmath_simplex_noise",
"cultmath_value_noise_bicubic",
"cultmath_value_noise_texture",
"cultmath_value_noise_texture_bicubic",
};
foreach (var symbol in requiredSymbols)
{
Assert.Contains(symbol, include);
}
Assert.Contains("CULTMATH_NORMALIZE_EPSILON", include);
Assert.Contains("max(length(value), CULTMATH_NORMALIZE_EPSILON)", include);
}
private static string GetCultMathRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null)
{
var monorepoPackage = Path.Combine(directory.FullName, "packages", "cultmath");
if (File.Exists(Path.Combine(monorepoPackage, "shaders", "CultMath.hlsl")))
{
return monorepoPackage;
}
directory = directory.Parent;
}
throw new InvalidOperationException("Could not locate the CultMath package root.");
}
}

View file

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

View file

@ -0,0 +1,335 @@
using CultMath;
using Xunit;
namespace CultMath.Tests;
public sealed class MathTests
{
[Fact]
public void FracMatchesShaderStylePositiveAndNegativeValues()
{
Assert.Equal(0.25f, math.frac(1.25f));
Assert.Equal(0.75f, math.frac(-1.25f));
}
[Fact]
public void VectorOperationsAreComponentWise()
{
var value = new float3(1.0f, 2.0f, 3.0f) * new float3(4.0f, 5.0f, 6.0f);
Assert.Equal(new float3(4.0f, 10.0f, 18.0f), value);
}
[Fact]
public void VectorConstructorsAcceptLowerDimensionVectors()
{
Assert.Equal(new int2(1, 2), math.int2(1, 2));
Assert.Equal(new float2(1.0f, 2.0f), math.float2(1.0f, 2.0f));
Assert.Equal(new float3(1.0f, 1.0f, 1.0f), math.float3(1.0f));
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), math.float4(new float3(1.0f, 2.0f, 3.0f), 4.0f));
Assert.Equal(new float3(1.0f, 2.0f, 3.0f), new float3(new float2(1.0f, 2.0f), 3.0f));
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), new float4(new float2(1.0f, 2.0f), 3.0f, 4.0f));
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), new float4(new float3(1.0f, 2.0f, 3.0f), 4.0f));
}
[Fact]
public void VectorsExposeCommonReadOnlySwizzles()
{
Assert.Equal(new float3(2.0f, 3.0f, 1.0f), new float3(1.0f, 2.0f, 3.0f).yzx);
Assert.Equal(new float3(3.0f, 2.0f, 1.0f), new float3(1.0f, 2.0f, 3.0f).zyx);
Assert.Equal(new float2(3.0f, 2.0f), new float3(1.0f, 2.0f, 3.0f).zy);
Assert.Equal(new float3(1.0f, 2.0f, 4.0f), new float4(1.0f, 2.0f, 3.0f, 4.0f).xyw);
Assert.Equal(new float3(2.0f, 3.0f, 1.0f), new float4(1.0f, 2.0f, 3.0f, 4.0f).yzx);
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), new float4(1.0f, new float3(2.0f, 3.0f, 4.0f)));
Assert.Equal(new float4(1.0f, 2.0f, 3.0f, 4.0f), math.float4(1.0f, new float3(2.0f, 3.0f, 4.0f)));
}
[Fact]
public void VectorsSupportUnityStyleFieldAndSwizzleMutation()
{
var position = new float3(1.0f, 2.0f, 3.0f);
position.y = 5.0f;
position.xz = new float2(8.0f, 13.0f);
Assert.Equal(new float3(8.0f, 5.0f, 13.0f), position);
var bounds = new float4(1.0f, 2.0f, 3.0f, 4.0f);
bounds.xy = new float2(-1.0f, -2.0f);
Assert.Equal(new float4(-1.0f, -2.0f, 3.0f, 4.0f), bounds);
}
[Fact]
public void DoubleVectorsSupportDampedMath()
{
var value = new double3(1.0, 2.0, 3.0);
value.xy = new double2(5.0, 8.0);
Assert.Equal(new double3(5.0, 8.0, 3.0), value);
Assert.Equal(new double2(Math.Exp(-2.0), Math.Exp(-4.0)), math.exp(new double2(-2.0, -4.0)));
Assert.Equal(new double3(2.0, 4.0, 6.0), math.lerp(new double3(0.0, 0.0, 0.0), new double3(4.0, 8.0, 12.0), new double3(0.5, 0.5, 0.5)));
}
[Fact]
public void Int2SupportsGridArithmeticAndFloatPromotion()
{
var cell = new int2(2, 3);
Assert.Equal(new int2(3, 5), cell + new int2(1, 2));
Assert.Equal(new int2(4, 6), cell * new int2(2, 2));
Assert.Equal(3, cell[1]);
float2 promoted = cell;
Assert.Equal(new float2(2.0f, 3.0f), promoted);
}
[Fact]
public void Bool2SupportsHullGridStyleStorage()
{
bool2 conductivity = true;
conductivity.y = false;
Assert.True(conductivity.x);
Assert.False(conductivity[1]);
Assert.Equal(new bool2(false, true), math.bool2(false, true));
}
[Fact]
public void QuaternionLookRotationProducesUnitOrientation()
{
var rotation = quaternion.LookRotation(new float3(0.0f, 0.0f, 1.0f), new float3(0.0f, 1.0f, 0.0f));
Assert.Equal(quaternion.identity, rotation);
Assert.Equal(1.0f, math.length((float4)quaternion.LookRotation(new float3(1.0f, 0.0f, 0.0f), new float3(0.0f, 1.0f, 0.0f))), precision: 5);
}
[Fact]
public void RandomUsesStableXorShift32Sequence()
{
var a = new Random(1234);
var b = new Random(1234);
Assert.Equal(332584831u, a.NextUInt());
Assert.Equal(1855942593u, a.NextUInt());
Assert.Equal(4018585650u, a.NextUInt());
Assert.Equal(3348358578u, a.NextUInt());
Assert.Equal(332584831u, b.NextUInt());
}
[Fact]
public void RandomProvidesDeterministicPortableSampling()
{
var a = new Random(1234);
var b = new Random(1234);
Assert.Equal(a.NextUInt(), b.NextUInt());
Assert.Equal(a.NextFloat(), b.NextFloat());
var bounded = a.NextFloat(-2.0f, 4.0f);
Assert.True(bounded >= -2.0f && bounded < 4.0f);
var direction = a.NextFloat2Direction();
Assert.Equal(1.0f, math.length(direction), precision: 5);
var point = a.NextFloat3(new float3(-1.0f, -2.0f, -3.0f), new float3(1.0f, 2.0f, 3.0f));
Assert.True(point.x >= -1.0f && point.x < 1.0f);
Assert.True(point.y >= -2.0f && point.y < 2.0f);
Assert.True(point.z >= -3.0f && point.z < 3.0f);
}
[Fact]
public void NormalizeProducesUnitLength()
{
var value = math.normalize(new float3(3.0f, 4.0f, 0.0f));
Assert.Equal(1.0f, math.length(value), precision: 5);
}
[Fact]
public void SmoothstepUsesHermiteRamp()
{
Assert.Equal(0.0f, math.smoothstep(0.0f, 1.0f, -1.0f));
Assert.Equal(0.5f, math.smoothstep(0.0f, 1.0f, 0.5f), precision: 5);
Assert.Equal(1.0f, math.smoothstep(0.0f, 1.0f, 2.0f));
Assert.Equal(0.5f, math.smootherstep(0.5f), precision: 5);
}
[Fact]
public void TrigIntrinsicsUseRadians()
{
Assert.Equal(1.0f, math.sin(math.HALF_PI), precision: 5);
Assert.Equal(-1.0f, math.cos(math.PI), precision: 5);
Assert.Equal(1.0f, math.tan(math.PI * 0.25f), precision: 5);
Assert.Equal(math.HALF_PI, math.atan2(1.0f, 0.0f), precision: 5);
Assert.Equal(2, math.min(2, 5));
Assert.Equal(5, math.max(2, 5));
Assert.True(math.isinf(float.PositiveInfinity));
}
[Fact]
public void SignIsComponentWiseForSimulationVectors()
{
Assert.Equal(-1.0f, math.sign(-12.0f));
Assert.Equal(0.0f, math.sign(0.0f));
Assert.Equal(1.0f, math.sign(12.0f));
Assert.Equal(new float3(-1.0f, 0.0f, 1.0f), math.sign(new float3(-2.0f, 0.0f, 4.0f)));
}
[Fact]
public void RotateTurnsFloat2CounterClockwiseInRadians()
{
var rotated = math.rotate(new float2(1.0f, 0.0f), math.HALF_PI);
Assert.Equal(0.0f, rotated.x, precision: 5);
Assert.Equal(1.0f, rotated.y, precision: 5);
var rotatedDegrees = math.rotate_degrees(new float2(0.0f, 1.0f), 90.0f);
Assert.Equal(-1.0f, rotatedDegrees.x, precision: 5);
Assert.Equal(0.0f, rotatedDegrees.y, precision: 5);
}
[Fact]
public void AetheriaPrimitivesRemainSmallAndDeterministic()
{
Assert.Equal(6.0f, math.csum(new float3(1.0f, 2.0f, 3.0f)));
Assert.Equal(0.25f, math.unlerp(10.0f, 30.0f, 15.0f));
Assert.Equal(new float2(0.25f, 0.75f), math.unlerp(new float2(10.0f, 10.0f), new float2(30.0f, 30.0f), new float2(15.0f, 25.0f)));
Assert.Equal(MathF.Exp(-2.0f), math.decay(1.0f, 2.0f, 1.0f), precision: 5);
Assert.Equal(1.0f - MathF.Exp(-2.0f), math.damp(0.0f, 1.0f, 2.0f, 1.0f), precision: 5);
Assert.Equal(15.0f, math.catmullrom(0.0f, 10.0f, 20.0f, 30.0f, 0.5f), precision: 5);
Assert.Equal(0.25f, math.quadratic_bezier(0.0f, 0.0f, 1.0f, 0.5f), precision: 5);
Assert.Equal(0.5f, math.cubic_bezier(0.0f, 0.0f, 1.0f, 1.0f, 0.5f), precision: 5);
}
[Fact]
public void SegmentDistanceReportsClosestPoint()
{
var distance = math.distance_to_segment(new float2(2.0f, 3.0f), new float2(0.0f, 0.0f), new float2(4.0f, 0.0f), out var closest);
Assert.Equal(3.0f, distance, precision: 5);
Assert.Equal(new float2(2.0f, 0.0f), closest);
}
[Fact]
public void RectNormalizesAndDerivesFromMinMax()
{
var bounds = new rect(8.0f, 6.0f, 2.0f, -2.0f);
Assert.Equal(new float2(2.0f, -2.0f), bounds.min);
Assert.Equal(new float2(8.0f, 6.0f), bounds.max);
Assert.Equal(new float2(6.0f, 8.0f), bounds.size);
Assert.Equal(new float2(5.0f, 2.0f), bounds.center);
Assert.Equal(48.0f, bounds.area);
}
[Fact]
public void RectContainmentIncludesBoundary()
{
var bounds = new rect(-2.0f, -1.0f, 4.0f, 3.0f);
Assert.True(bounds.Contains(new float2(-2.0f, -1.0f)));
Assert.True(bounds.Contains(new float2(4.0f, 3.0f)));
Assert.True(bounds.Contains(new rect(-1.0f, 0.0f, 1.0f, 2.0f)));
Assert.False(bounds.Contains(new float2(4.001f, 3.0f)));
}
[Fact]
public void RectIntersectionTreatsTouchingEdgesAsContact()
{
var left = new rect(0.0f, 0.0f, 4.0f, 4.0f);
var touching = new rect(4.0f, 1.0f, 8.0f, 3.0f);
var separate = new rect(4.001f, 1.0f, 8.0f, 3.0f);
Assert.True(left.Intersects(touching));
Assert.Equal(0.0f, left.Intersection(touching).area);
Assert.False(left.Intersects(separate));
}
[Fact]
public void FirstOrderInterceptSolvesSimpleClosingTarget()
{
var time = math.first_order_intercept_time(
2.0f,
new float3(10.0f, 0.0f, 0.0f),
new float3(-1.0f, 0.0f, 0.0f));
Assert.Equal(10.0f / 3.0f, time, precision: 5);
}
[Fact]
public void ValueNoiseIsDeterministicAndSmoothAtCellCorners()
{
var position = new float2(12.25f, -4.5f);
Assert.Equal(math.value_noise(position), math.value_noise(position));
Assert.Equal(math.hash(new float2(3.0f, 5.0f)), math.value_noise(new float2(3.0f, 5.0f)), precision: 5);
var bicubic = math.value_noise_bicubic(position);
Assert.True(bicubic > -0.25f && bicubic < 1.25f);
}
[Theory]
[InlineData(0.0f, 0.0f, 0.0f)]
[InlineData(0.25f, -0.5f, -0.425866783f)]
[InlineData(12.25f, -4.5f, 0.258312f)]
[InlineData(536.5106f, 536.5106f, -0.6901103f)]
public void SimplexNoiseMatchesUnityMathematics(float x, float y, float expected)
{
Assert.Equal(expected, math.simplex_noise(new float2(x, y)), precision: 6);
}
[Fact]
public void VoronoiBatchProducesToneMappedColors()
{
var xs = new[] { 0.0f, 24.0f, 96.0f };
var ys = new[] { 0.0f, 36.0f, 144.0f };
var tones = new[] { CultMathTone.Background, CultMathTone.Header, CultMathTone.Body };
var spans = new[] { 1920.0f, 8.0f, 8.0f };
var colors = new Color32[xs.Length];
Voronoi.SampleTones(xs, ys, tones, spans, 1080.0f, 12, colors);
Assert.NotEqual(new Color32(), colors[0]);
Assert.True(colors[1].r > colors[1].g);
Assert.True(colors[2].r > 120 && colors[2].g > 120 && colors[2].b > 120);
}
[Fact]
public void BatchRadialFalloffAccelerationMatchesScalarContract()
{
var xs = new[] { 0.0f, 5.0f, 10.0f };
var ys = new[] { 0.0f, 0.0f, 0.0f };
var ax = new float[xs.Length];
var ay = new float[xs.Length];
BatchMath.AddRadialFalloffAcceleration2D(xs, ys, 10.0f, 0.0f, 10.0f, 20.0f, ax, ay);
Assert.True(BatchMath.LaneCount >= 4);
Assert.Equal(5.0f, ax[0], precision: 5);
Assert.Equal(7.5f, ax[1], precision: 5);
Assert.Equal(0.0f, ax[2], precision: 5);
Assert.Equal(0.0f, ay[0], precision: 5);
}
[Fact]
public void BatchEulerIntegrationHonorsDynamicMask()
{
var dynamicMask = new[] { 1.0f, 0.0f };
var px = new[] { 0.0f, 0.0f };
var py = new[] { 0.0f, 0.0f };
var vx = new[] { 1.0f, 1.0f };
var vy = new[] { 0.0f, 0.0f };
var ax = new[] { 2.0f, 2.0f };
var ay = new[] { 0.0f, 4.0f };
BatchMath.IntegrateSemiImplicitEuler2D(0.5f, dynamicMask, px, py, vx, vy, ax, ay);
Assert.Equal(2.0f, vx[0], precision: 5);
Assert.Equal(1.0f, px[0], precision: 5);
Assert.Equal(1.0f, vx[1], precision: 5);
Assert.Equal(0.0f, px[1], precision: 5);
Assert.Equal(0.0f, py[1], precision: 5);
}
}

View file

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

View file

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

View file

@ -0,0 +1,69 @@
param(
[Parameter(Mandatory = $true)]
[string]$ShaderPath,
[Parameter(Mandatory = $true)]
[string]$OutputPath,
[string]$EntryPoint = "main",
[string]$Profile = "cs_6_0",
[string[]]$IncludePath = @(),
[string]$TargetEnv = "",
[int]$VulkanCBufferShift = 0,
[int]$VulkanTextureShift = 10,
[int]$VulkanUavShift = 20,
[int]$VulkanSamplerShift = 30,
[string]$DxcPath = ""
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
if (-not $DxcPath) {
$DxcPath = Join-Path $repoRoot ".tools\dxc\current\bin\x64\dxc.exe"
}
if (-not (Test-Path $DxcPath)) {
throw "DXC not found at '$DxcPath'. Run tools\get-dxc.ps1 first or pass -DxcPath."
}
$resolvedShader = Resolve-Path $ShaderPath
$resolvedIncludes = @((Join-Path $repoRoot "shaders"))
foreach ($path in $IncludePath) {
$resolvedIncludes += (Resolve-Path $path)
}
$outputDirectory = Split-Path -Parent $OutputPath
if ($outputDirectory) {
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
}
$arguments = @(
"-T", $Profile,
"-E", $EntryPoint,
"-spirv"
)
if ($TargetEnv) {
$arguments += "-fspv-target-env=$TargetEnv"
}
$arguments += @(
"-fvk-b-shift", $VulkanCBufferShift.ToString(), "0",
"-fvk-t-shift", $VulkanTextureShift.ToString(), "0",
"-fvk-u-shift", $VulkanUavShift.ToString(), "0",
"-fvk-s-shift", $VulkanSamplerShift.ToString(), "0"
)
foreach ($include in $resolvedIncludes) {
$arguments += @("-I", $include)
}
$arguments += @($resolvedShader, "-Fo", $OutputPath)
& $DxcPath @arguments
if ($LASTEXITCODE -ne 0) {
throw "dxc.exe failed with exit code $LASTEXITCODE."
}
Get-Item -LiteralPath $OutputPath

View file

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

View file

@ -0,0 +1,34 @@
param(
[Parameter(Mandatory = $true)]
[string]$SpirvPath,
[Parameter(Mandatory = $true)]
[string]$OutputPath,
[int]$MslVersion = 23000,
[string]$SpirvCrossPath = ""
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
if (-not $SpirvCrossPath) {
$SpirvCrossPath = Join-Path $repoRoot ".tools\spirv-cross\build\spirv-cross.exe"
}
if (-not (Test-Path $SpirvCrossPath)) {
throw "SPIRV-Cross not found at '$SpirvCrossPath'. Run tools\get-spirv-cross.ps1 first or pass -SpirvCrossPath."
}
$resolvedSpirv = Resolve-Path $SpirvPath
$outputDirectory = Split-Path -Parent $OutputPath
if ($outputDirectory) {
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
}
& $SpirvCrossPath $resolvedSpirv --msl --msl-version $MslVersion --output $OutputPath
if ($LASTEXITCODE -ne 0) {
throw "spirv-cross failed with exit code $LASTEXITCODE."
}
Get-Item -LiteralPath $OutputPath

View file

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

View file

@ -0,0 +1,42 @@
param(
[string]$InstallRoot = ".tools\dxc"
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$installPath = Join-Path $repoRoot $InstallRoot
New-Item -ItemType Directory -Force -Path $installPath | Out-Null
$release = Invoke-RestMethod `
-Uri "https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/latest" `
-Headers @{ "User-Agent" = "CultMath-DXC-Bootstrap" }
$asset = $release.assets |
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
& curl.exe -L --retry 5 --retry-delay 2 --fail -o $zipPath $asset.browser_download_url
if ($LASTEXITCODE -ne 0) {
throw "curl.exe failed with exit code $LASTEXITCODE."
}
$currentPath = Join-Path $installPath "current"
Remove-Item -LiteralPath $currentPath -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $currentPath | Out-Null
Expand-Archive -LiteralPath $zipPath -DestinationPath $currentPath -Force
$dxcPath = Join-Path $currentPath "bin\x64\dxc.exe"
if (-not (Test-Path $dxcPath)) {
throw "DXC x64 executable was not found after extraction."
}
& $dxcPath --version
Write-Host "DXC installed at $dxcPath"

View file

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

View file

@ -0,0 +1,34 @@
param(
[string]$InstallRoot = ".tools\spirv-cross"
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$installPath = Join-Path $repoRoot $InstallRoot
$sourcePath = Join-Path $installPath "src"
$buildPath = Join-Path $installPath "build"
New-Item -ItemType Directory -Force -Path $installPath | Out-Null
if (-not (Test-Path $sourcePath)) {
git clone --depth 1 https://github.com/KhronosGroup/SPIRV-Cross.git $sourcePath
}
cmake `
-S $sourcePath `
-B $buildPath `
-G Ninja `
-DSPIRV_CROSS_CLI=ON `
-DSPIRV_CROSS_ENABLE_TESTS=OFF `
-DSPIRV_CROSS_ENABLE_C_API=OFF
cmake --build $buildPath --target spirv-cross --config Release
$spirvCrossPath = Join-Path $buildPath "spirv-cross.exe"
if (-not (Test-Path $spirvCrossPath)) {
throw "SPIRV-Cross executable was not found after build."
}
& $spirvCrossPath --version
Write-Host "SPIRV-Cross installed at $spirvCrossPath"

View file

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

View file

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

View file

@ -0,0 +1,10 @@
# CultMath License
CultMath is licensed under the Mozilla Public License 2.0 (`MPL-2.0`).
The complete license text is available from the Mozilla Foundation at
<https://www.mozilla.org/MPL/2.0/>. Source and modification history for the
precompiled assembly in this package are available in the GameCult/CultMath
repository at the corresponding package tag.
Third-party code retains its original license. See `THIRD-PARTY-NOTICES.md`.

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7172bce28532878409e57f5fbbb12719

View file

@ -0,0 +1,28 @@
# CultMath for Unity
This is CultMath's Unity 2021.3-compatible Git-UPM surface. It contains the
precompiled `CultMath.dll`, portable symbols, and the numeric
`Shaders/CultMath.hlsl` mirror.
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
imports only the tracked auto-referenced precompiled assembly, so consumers do
not need `csc.rsp` language overrides or an asmdef propagation shim.
Build and inspect the package from the repository root:
```powershell
.\scripts\build-unity-package.ps1
```
The builder fresh-builds CultMath for `netstandard2.1`, verifies the tracked
DLL/PDB and stable Unity metadata, and stages an inspectable package under
`artifacts/unity/org.gamecult.cultmath`.
Consume the repository package with:
```json
"org.gamecult.cultmath": "https://github.com/GameCult/CultLib.git?path=/packages/cultmath/unity/org.gamecult.cultmath"
```
Production consumers should pin that URL to a commit or release tag.

View file

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

View file

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

View file

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

View file

@ -0,0 +1,21 @@
fileFormatVersion: 2
guid: 063b8a850f0f474aa7e89c4167f97a19
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
userData:
assetBundleName:
assetBundleVariant:

View file

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

View file

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

View file

@ -0,0 +1,302 @@
#ifndef CULTMATH_HLSL
#define CULTMATH_HLSL
// CultMath HLSL mirror.
//
// HLSL already owns the float2/float3/float4 type names and most primitive
// intrinsics. This include names the CultMath semantic surface explicitly so
// shader code, C# wrappers, and Rust/native parity fixtures can share the same
// small vocabulary without local helper drift.
static const float CULTMATH_PI = 3.14159265358979323846;
static const float CULTMATH_TAU = 6.28318530717958647692;
static const float CULTMATH_HALF_PI = 1.57079632679489661923;
static const float CULTMATH_NORMALIZE_EPSILON = 1.0e-20;
float cultmath_radians(float degrees_value) { return degrees_value * (CULTMATH_PI / 180.0); }
float cultmath_degrees(float radians_value) { return radians_value * (180.0 / CULTMATH_PI); }
float cultmath_frac(float value) { return value - floor(value); }
float2 cultmath_frac(float2 value) { return value - floor(value); }
float3 cultmath_frac(float3 value) { return value - floor(value); }
float4 cultmath_frac(float4 value) { return value - floor(value); }
float cultmath_clamp(float value, float minimum, float maximum) { return min(max(value, minimum), maximum); }
float2 cultmath_clamp(float2 value, float2 minimum, float2 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); }
float cultmath_saturate(float value) { return cultmath_clamp(value, 0.0, 1.0); }
float2 cultmath_saturate(float2 value) { return cultmath_clamp(value, float2(0.0, 0.0), float2(1.0, 1.0)); }
float3 cultmath_saturate(float3 value) { return cultmath_clamp(value, float3(0.0, 0.0, 0.0), float3(1.0, 1.0, 1.0)); }
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)); }
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; }
float3 cultmath_lerp(float3 start, float3 end, float3 amount) { return start + (end - start) * amount; }
float4 cultmath_lerp(float4 start, float4 end, float4 amount) { return start + (end - start) * amount; }
float cultmath_step(float edge, float value) { return value < edge ? 0.0 : 1.0; }
float2 cultmath_step(float2 edge, float2 value) { return float2(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y)); }
float3 cultmath_step(float3 edge, float3 value) { return float3(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z)); }
float4 cultmath_step(float4 edge, float4 value) { return float4(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z), cultmath_step(edge.w, value.w)); }
float cultmath_smoothstep(float minimum, float maximum, float value)
{
float t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (3.0 - 2.0 * t);
}
float2 cultmath_smoothstep(float2 minimum, float2 maximum, float2 value)
{
float2 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (float2(3.0, 3.0) - float2(2.0, 2.0) * t);
}
float3 cultmath_smoothstep(float3 minimum, float3 maximum, float3 value)
{
float3 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (float3(3.0, 3.0, 3.0) - float3(2.0, 2.0, 2.0) * t);
}
float4 cultmath_smoothstep(float4 minimum, float4 maximum, float4 value)
{
float4 t = cultmath_saturate((value - minimum) / (maximum - minimum));
return t * t * (float4(3.0, 3.0, 3.0, 3.0) - float4(2.0, 2.0, 2.0, 2.0) * t);
}
float cultmath_smoothstep01(float value) { return cultmath_smoothstep(0.0, 1.0, value); }
float cultmath_smootherstep(float value)
{
value = cultmath_saturate(value);
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; }
float3 cultmath_simplex_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); }
float cultmath_simplex_noise(float2 value)
{
const float4 c = float4(
0.211324865405187,
0.366025403784439,
-0.577350269189626,
0.024390243902439);
float2 i = floor(value + dot(value, c.yy));
float2 x0 = value - i + dot(i, c.xx);
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);
i = cultmath_simplex_mod289(i);
float3 p = cultmath_simplex_permute(
cultmath_simplex_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);
m *= m;
m *= m;
float3 x = 2.0 * frac(p * c.w) - 1.0;
float3 h = abs(x) - 0.5;
float3 ox = floor(x + 0.5);
float3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
float3 g = float3(
a0.x * x0.x + h.x * x0.y,
a0.y * x12.x + h.y * x12.y,
a0.z * x12.z + h.z * x12.w);
return 130.0 * dot(m, g);
}
float cultmath_lengthsq(float2 value) { return dot(value, value); }
float cultmath_lengthsq(float3 value) { return dot(value, value); }
float cultmath_lengthsq(float4 value) { return dot(value, value); }
float cultmath_distance(float2 left, float2 right) { return length(left - right); }
float cultmath_distance(float3 left, float3 right) { return length(left - right); }
float cultmath_distance(float4 left, float4 right) { return length(left - right); }
float2 cultmath_normalize(float2 value) { return value / max(length(value), CULTMATH_NORMALIZE_EPSILON); }
float3 cultmath_normalize(float3 value) { return value / max(length(value), CULTMATH_NORMALIZE_EPSILON); }
float4 cultmath_normalize(float4 value) { return value / max(length(value), CULTMATH_NORMALIZE_EPSILON); }
float2 cultmath_reflect(float2 incident, float2 normal_value)
{
return incident - 2.0 * dot(normal_value, incident) * normal_value;
}
float3 cultmath_reflect(float3 incident, float3 normal_value)
{
return incident - 2.0 * dot(normal_value, incident) * normal_value;
}
float2 cultmath_rotate(float2 value, float radians_value)
{
float s = sin(radians_value);
float c = cos(radians_value);
return float2(value.x * c - value.y * s, value.x * s + value.y * c);
}
float2 cultmath_rotate_degrees(float2 value, float degrees_value)
{
return cultmath_rotate(value, cultmath_radians(degrees_value));
}
float cultmath_csum(float2 value) { return value.x + value.y; }
float cultmath_csum(float3 value) { return value.x + value.y + value.z; }
float cultmath_csum(float4 value) { return value.x + value.y + value.z + value.w; }
float cultmath_decay(float source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float2 cultmath_decay(float2 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float3 cultmath_decay(float3 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float4 cultmath_decay(float4 source, float lambda_value, float dt) { return source * exp(-lambda_value * dt); }
float cultmath_damp(float start, float end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
float2 cultmath_damp(float2 start, float2 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
float3 cultmath_damp(float3 start, float3 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
float4 cultmath_damp(float4 start, float4 end, float lambda_value, float dt)
{
return cultmath_lerp(start, end, 1.0 - exp(-lambda_value * dt));
}
float cultmath_catmullrom(float p0, float p1, float p2, float p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float2 cultmath_catmullrom(float2 p0, float2 p1, float2 p2, float2 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float3 cultmath_catmullrom(float3 p0, float3 p1, float3 p2, float3 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float4 cultmath_catmullrom(float4 p0, float4 p1, float4 p2, float4 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5 * ((2.0 * p1) + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3);
}
float cultmath_quadratic_bezier(float p0, float p1, float p2, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * p0 + 2.0 * inv * t * p1 + t * t * p2;
}
float3 cultmath_quadratic_bezier(float3 p0, float3 p1, float3 p2, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * p0 + 2.0 * inv * t * p1 + t * t * p2;
}
float cultmath_cubic_bezier(float p0, float p1, float p2, float p3, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * inv * p0 + 3.0 * inv * inv * t * p1 + 3.0 * inv * t * t * p2 + t * t * t * p3;
}
float3 cultmath_cubic_bezier(float3 p0, float3 p1, float3 p2, float3 p3, float t)
{
t = cultmath_saturate(t);
float inv = 1.0 - t;
return inv * inv * inv * p0 + 3.0 * inv * inv * t * p1 + 3.0 * inv * t * t * p2 + t * t * t * p3;
}
float cultmath_hash(float value) { return cultmath_frac(sin(value) * 43758.5453); }
float cultmath_hash(float2 value) { return cultmath_hash(dot(value, float2(127.1, 311.7))); }
float cultmath_hash(float3 value) { return cultmath_hash(dot(value, float3(127.1, 311.7, 74.7))); }
float cultmath_value_noise(float2 position)
{
float2 cell = floor(position);
float2 local = cultmath_frac(position);
float2 u = local * local * (float2(3.0, 3.0) - float2(2.0, 2.0) * local);
float a = cultmath_hash(cell);
float b = cultmath_hash(cell + float2(1.0, 0.0));
float c = cultmath_hash(cell + float2(0.0, 1.0));
float d = cultmath_hash(cell + float2(1.0, 1.0));
return cultmath_lerp(cultmath_lerp(a, b, u.x), cultmath_lerp(c, d, u.x), u.y);
}
float cultmath_value_noise_bicubic(float2 position)
{
float2 cell = floor(position);
float2 local = cultmath_frac(position);
float y0 = cultmath_catmullrom(cultmath_hash(cell + float2(-1.0, -1.0)), cultmath_hash(cell + float2(0.0, -1.0)), cultmath_hash(cell + float2(1.0, -1.0)), cultmath_hash(cell + float2(2.0, -1.0)), local.x);
float y1 = cultmath_catmullrom(cultmath_hash(cell + float2(-1.0, 0.0)), cultmath_hash(cell + float2(0.0, 0.0)), cultmath_hash(cell + float2(1.0, 0.0)), cultmath_hash(cell + float2(2.0, 0.0)), local.x);
float y2 = cultmath_catmullrom(cultmath_hash(cell + float2(-1.0, 1.0)), cultmath_hash(cell + float2(0.0, 1.0)), cultmath_hash(cell + float2(1.0, 1.0)), cultmath_hash(cell + float2(2.0, 1.0)), local.x);
float y3 = cultmath_catmullrom(cultmath_hash(cell + float2(-1.0, 2.0)), cultmath_hash(cell + float2(0.0, 2.0)), cultmath_hash(cell + float2(1.0, 2.0)), cultmath_hash(cell + float2(2.0, 2.0)), local.x);
return cultmath_catmullrom(y0, y1, y2, y3, local.y);
}
float cultmath_value_noise_texture(Texture2D<float> noise_texture, SamplerState noise_sampler, float2 uv, float scale)
{
return noise_texture.SampleLevel(noise_sampler, uv * scale, 0.0).r;
}
float cultmath_value_noise_texture_bicubic(Texture2D<float> noise_texture, SamplerState noise_sampler, float2 uv, float scale, float2 texel_size)
{
float2 position = uv * scale;
float2 cell = floor(position);
float2 local = cultmath_frac(position);
float2 base_uv = cell / scale;
float row0 = cultmath_catmullrom(
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(-1.0, -1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(0.0, -1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(1.0, -1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(2.0, -1.0), 0.0).r,
local.x);
float row1 = cultmath_catmullrom(
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(-1.0, 0.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(0.0, 0.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(1.0, 0.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(2.0, 0.0), 0.0).r,
local.x);
float row2 = cultmath_catmullrom(
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(-1.0, 1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(0.0, 1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(1.0, 1.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(2.0, 1.0), 0.0).r,
local.x);
float row3 = cultmath_catmullrom(
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(-1.0, 2.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(0.0, 2.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(1.0, 2.0), 0.0).r,
noise_texture.SampleLevel(noise_sampler, base_uv + texel_size * float2(2.0, 2.0), 0.0).r,
local.x);
return cultmath_catmullrom(row0, row1, row2, row3, local.y);
}
#endif

View file

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

View file

@ -0,0 +1,11 @@
# Third-Party Notices
## 2D Simplex Noise
`math.simplex_noise` and `cultmath_simplex_noise` port the textureless 2D
simplex-noise function by Ian McEwan and Ashima Arts, as distributed by Stefan
Gustavson's `webgl-noise` project and Unity Mathematics.
- Copyright 2011 Ashima Arts.
- Upstream: <https://github.com/ashima/webgl-noise>
- License: MIT.

View file

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

View file

@ -0,0 +1,17 @@
{
"name": "org.gamecult.cultmath",
"displayName": "CultMath",
"version": "0.1.2",
"unity": "2021.3",
"description": "Precompiled CultMath numeric primitives and matching HLSL semantics for GameCult Unity runtimes.",
"license": "MPL-2.0",
"author": {
"name": "GameCult"
},
"keywords": [
"GameCult",
"Math",
"HLSL",
"Simulation"
]
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 33d8d4af49bf0f946ad00a375f26b1d1

View file

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