diff --git a/CultLib.sln b/CultLib.sln index c40b6b41..2b50a153 100644 --- a/CultLib.sln +++ b/CultLib.sln @@ -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 diff --git a/README.md b/README.md index df4dd6f0..4775a040 100644 --- a/README.md +++ b/README.md @@ -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/ diff --git a/packages/cultmath/.gitignore b/packages/cultmath/.gitignore new file mode 100644 index 00000000..c9b141d0 --- /dev/null +++ b/packages/cultmath/.gitignore @@ -0,0 +1,9 @@ +bin/ +obj/ +**/bin/ +**/obj/ +target/ +**/target/ +/native/cultmath-core/target.meta +.tools/ +/artifacts/ diff --git a/packages/cultmath/README.md b/packages/cultmath/README.md new file mode 100644 index 00000000..edd24d37 --- /dev/null +++ b/packages/cultmath/README.md @@ -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` / `ReadOnlySpan` using + `System.Numerics.Vector` 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. diff --git a/packages/cultmath/README.md.meta b/packages/cultmath/README.md.meta new file mode 100644 index 00000000..4411da72 --- /dev/null +++ b/packages/cultmath/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 867daebafe0ce48479cf043461ee6bdf +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/THIRD-PARTY-NOTICES.md b/packages/cultmath/THIRD-PARTY-NOTICES.md new file mode 100644 index 00000000..c283df2b --- /dev/null +++ b/packages/cultmath/THIRD-PARTY-NOTICES.md @@ -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: +- License: MIT. diff --git a/packages/cultmath/THIRD-PARTY-NOTICES.md.meta b/packages/cultmath/THIRD-PARTY-NOTICES.md.meta new file mode 100644 index 00000000..4b43ddaa --- /dev/null +++ b/packages/cultmath/THIRD-PARTY-NOTICES.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7c28c8631c7d4054eb751d7ce952df33 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/docs.meta b/packages/cultmath/docs.meta new file mode 100644 index 00000000..ee5fab29 --- /dev/null +++ b/packages/cultmath/docs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: de535b89253410b41b5989f712b80af4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md new file mode 100644 index 00000000..8250ba78 --- /dev/null +++ b/packages/cultmath/docs/design.md @@ -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. diff --git a/packages/cultmath/docs/design.md.meta b/packages/cultmath/docs/design.md.meta new file mode 100644 index 00000000..0d31ca6a --- /dev/null +++ b/packages/cultmath/docs/design.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 07021a8be816e3448a9490c10845b0aa +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/native.meta b/packages/cultmath/native.meta new file mode 100644 index 00000000..f4135336 --- /dev/null +++ b/packages/cultmath/native.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9d1cb2ac88c530b4d9da456523115e12 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/native/cultmath-core.meta b/packages/cultmath/native/cultmath-core.meta new file mode 100644 index 00000000..48a2ef76 --- /dev/null +++ b/packages/cultmath/native/cultmath-core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a42d65478346c354c8b4f353c5581b49 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/native/cultmath-core/Cargo.lock b/packages/cultmath/native/cultmath-core/Cargo.lock new file mode 100644 index 00000000..09598b97 --- /dev/null +++ b/packages/cultmath/native/cultmath-core/Cargo.lock @@ -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" diff --git a/packages/cultmath/native/cultmath-core/Cargo.lock.meta b/packages/cultmath/native/cultmath-core/Cargo.lock.meta new file mode 100644 index 00000000..9ca3ec60 --- /dev/null +++ b/packages/cultmath/native/cultmath-core/Cargo.lock.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f1c365f7eead2684ba621124e653bdab +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/native/cultmath-core/Cargo.toml b/packages/cultmath/native/cultmath-core/Cargo.toml new file mode 100644 index 00000000..cf7819a0 --- /dev/null +++ b/packages/cultmath/native/cultmath-core/Cargo.toml @@ -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" diff --git a/packages/cultmath/native/cultmath-core/Cargo.toml.meta b/packages/cultmath/native/cultmath-core/Cargo.toml.meta new file mode 100644 index 00000000..8875de09 --- /dev/null +++ b/packages/cultmath/native/cultmath-core/Cargo.toml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cdd2d4eab053b764b9a06d36b080e43e +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/native/cultmath-core/src.meta b/packages/cultmath/native/cultmath-core/src.meta new file mode 100644 index 00000000..8e5232db --- /dev/null +++ b/packages/cultmath/native/cultmath-core/src.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7de448b7e258d2c448bb7391051cc628 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/native/cultmath-core/src/lib.rs b/packages/cultmath/native/cultmath-core/src/lib.rs new file mode 100644 index 00000000..46e7ced6 --- /dev/null +++ b/packages/cultmath/native/cultmath-core/src/lib.rs @@ -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)); + } +} diff --git a/packages/cultmath/native/cultmath-core/src/lib.rs.meta b/packages/cultmath/native/cultmath-core/src/lib.rs.meta new file mode 100644 index 00000000..2fa8aee8 --- /dev/null +++ b/packages/cultmath/native/cultmath-core/src/lib.rs.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b06c69c432a633f4584830d6b28d4473 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/scripts.meta b/packages/cultmath/scripts.meta new file mode 100644 index 00000000..6f03c566 --- /dev/null +++ b/packages/cultmath/scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0ddff5b782cf14e43ae5cda048e040d8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/scripts/build-unity-package.ps1 b/packages/cultmath/scripts/build-unity-package.ps1 new file mode 100644 index 00000000..f5fcacc0 --- /dev/null +++ b/packages/cultmath/scripts/build-unity-package.ps1 @@ -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)" diff --git a/packages/cultmath/shaders.meta b/packages/cultmath/shaders.meta new file mode 100644 index 00000000..46f9d445 --- /dev/null +++ b/packages/cultmath/shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3bed74ebcba445543800218dbe185034 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl new file mode 100644 index 00000000..4c00fb69 --- /dev/null +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -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 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 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 diff --git a/packages/cultmath/shaders/CultMath.hlsl.meta b/packages/cultmath/shaders/CultMath.hlsl.meta new file mode 100644 index 00000000..d6b8542e --- /dev/null +++ b/packages/cultmath/shaders/CultMath.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 33174674795143c4a905408718f956f8 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/src.meta b/packages/cultmath/src.meta new file mode 100644 index 00000000..3e88fcb9 --- /dev/null +++ b/packages/cultmath/src.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 64f48d0cae0810e4ba9903e9094fe4c6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/src/CultMath.meta b/packages/cultmath/src/CultMath.meta new file mode 100644 index 00000000..381b726d --- /dev/null +++ b/packages/cultmath/src/CultMath.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e857b457b93397d4fb4ad8481cb72e4c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/src/CultMath/BatchMath.cs b/packages/cultmath/src/CultMath/BatchMath.cs new file mode 100644 index 00000000..04c042d9 --- /dev/null +++ b/packages/cultmath/src/CultMath/BatchMath.cs @@ -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.Count; + + public static bool IsHardwareAccelerated => Vector.IsHardwareAccelerated; + + public static void Clear(Span values) => values.Clear(); + + public static void AddRadialFalloffAcceleration2D( + ReadOnlySpan positionX, + ReadOnlySpan positionY, + float centerX, + float centerY, + float strength, + float radius, + Span accelerationX, + Span 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(radius); + var centerXVector = new Vector(centerX); + var centerYVector = new Vector(centerY); + var strengthVector = new Vector(strength); + var one = Vector.One; + var zero = Vector.Zero; + var minimumDistanceSquared = new Vector(MinimumDistanceSquared); + + var i = 0; + var lanes = Vector.Count; + var pxBuffer = new float[lanes]; + var pyBuffer = new float[lanes]; + var axBuffer = new float[lanes]; + var ayBuffer = new float[lanes]; + for (; i <= positionX.Length - lanes; i += lanes) + { + CopySpanToArray(positionX.Slice(i, lanes), pxBuffer); + CopySpanToArray(positionY.Slice(i, lanes), pyBuffer); + CopySpanToArray(accelerationX.Slice(i, lanes), axBuffer); + CopySpanToArray(accelerationY.Slice(i, lanes), ayBuffer); + + var px = new Vector(pxBuffer); + var py = new Vector(pyBuffer); + var dx = centerXVector - px; + var dy = centerYVector - py; + var distanceSquared = Vector.Max(dx * dx + dy * dy, minimumDistanceSquared); + 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(axBuffer) + ax).CopyTo(axBuffer); + (new Vector(ayBuffer) + ay).CopyTo(ayBuffer); + CopyArrayToSpan(axBuffer, accelerationX.Slice(i, lanes)); + CopyArrayToSpan(ayBuffer, accelerationY.Slice(i, lanes)); + } + + for (; i < positionX.Length; i++) + { + 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 positions, + float2 center, + float strength, + float radius, + Span acceleration) + { + ValidateEqualLengths(positions.Length, acceleration.Length); + + if (radius <= 0.0f || strength == 0.0f || positions.Length == 0) + { + return; + } + + for (var i = 0; i < positions.Length; i++) + { + var delta = center - positions[i]; + var distanceSquared = MathF.Max(delta.x * delta.x + delta.y * delta.y, MinimumDistanceSquared); + var distance = MathF.Sqrt(distanceSquared); + if (distance > radius) + { + continue; + } + + var falloff = MathF.Max(0.0f, 1.0f - distance / radius); + acceleration[i] += delta / distance * (strength * falloff); + } + } + + public static void IntegrateSemiImplicitEuler2D( + float deltaTime, + ReadOnlySpan dynamicMask, + Span positionX, + Span positionY, + Span velocityX, + Span velocityY, + ReadOnlySpan accelerationX, + ReadOnlySpan 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(deltaTime); + var i = 0; + var lanes = Vector.Count; + var maskBuffer = new float[lanes]; + var pxBuffer = new float[lanes]; + var pyBuffer = new float[lanes]; + var vxBuffer = new float[lanes]; + var vyBuffer = new float[lanes]; + var axBuffer = new float[lanes]; + var ayBuffer = new float[lanes]; + for (; i <= positionX.Length - lanes; i += lanes) + { + CopySpanToArray(dynamicMask.Slice(i, lanes), maskBuffer); + CopySpanToArray(velocityX.Slice(i, lanes), vxBuffer); + CopySpanToArray(velocityY.Slice(i, lanes), vyBuffer); + CopySpanToArray(positionX.Slice(i, lanes), pxBuffer); + CopySpanToArray(positionY.Slice(i, lanes), pyBuffer); + CopySpanToArray(accelerationX.Slice(i, lanes), axBuffer); + CopySpanToArray(accelerationY.Slice(i, lanes), ayBuffer); + + var mask = new Vector(maskBuffer); + var vx = new Vector(vxBuffer); + var vy = new Vector(vyBuffer); + var px = new Vector(pxBuffer); + var py = new Vector(pyBuffer); + var ax = new Vector(axBuffer); + var ay = new Vector(ayBuffer); + + vx += ax * delta * mask; + vy += ay * delta * mask; + px += vx * delta * mask; + py += vy * delta * mask; + + vx.CopyTo(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 dynamicMask, + Span position, + Span velocity, + ReadOnlySpan acceleration) + { + if (deltaTime <= 0.0f) + { + throw new ArgumentOutOfRangeException(nameof(deltaTime), "Delta time must be positive."); + } + + ValidateEqualLengths(dynamicMask.Length, position.Length, velocity.Length, acceleration.Length); + + for (var i = 0; i < position.Length; i++) + { + var mask = dynamicMask[i]; + velocity[i] += acceleration[i] * deltaTime * mask; + position[i] += velocity[i] * deltaTime * mask; + } + } + + private static void ValidateEqualLengths(params int[] lengths) + { + var length = lengths[0]; + 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 source, float[] destination) + { + for (var i = 0; i < source.Length; i++) + { + destination[i] = source[i]; + } + } + + private static void CopyArrayToSpan(float[] source, Span destination) + { + for (var i = 0; i < destination.Length; i++) + { + destination[i] = source[i]; + } + } +} diff --git a/packages/cultmath/src/CultMath/BatchMath.cs.meta b/packages/cultmath/src/CultMath/BatchMath.cs.meta new file mode 100644 index 00000000..fe7c6033 --- /dev/null +++ b/packages/cultmath/src/CultMath/BatchMath.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d4771320e190bdd408dd296c293a9d0e \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/Color32.cs b/packages/cultmath/src/CultMath/Color32.cs new file mode 100644 index 00000000..e9a2b481 --- /dev/null +++ b/packages/cultmath/src/CultMath/Color32.cs @@ -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); diff --git a/packages/cultmath/src/CultMath/Color32.cs.meta b/packages/cultmath/src/CultMath/Color32.cs.meta new file mode 100644 index 00000000..71e1ba55 --- /dev/null +++ b/packages/cultmath/src/CultMath/Color32.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bfa14a9aacb8ac4448761568643f2bc6 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/CultMath.asmdef b/packages/cultmath/src/CultMath/CultMath.asmdef new file mode 100644 index 00000000..449549f6 --- /dev/null +++ b/packages/cultmath/src/CultMath/CultMath.asmdef @@ -0,0 +1,14 @@ +{ + "name": "CultMath", + "rootNamespace": "CultMath", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": true +} diff --git a/packages/cultmath/src/CultMath/CultMath.asmdef.meta b/packages/cultmath/src/CultMath/CultMath.asmdef.meta new file mode 100644 index 00000000..5a26d2c5 --- /dev/null +++ b/packages/cultmath/src/CultMath/CultMath.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f0b66b909d13e18468f7aa56221531e9 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/src/CultMath/CultMath.csproj b/packages/cultmath/src/CultMath/CultMath.csproj new file mode 100644 index 00000000..0ca99063 --- /dev/null +++ b/packages/cultmath/src/CultMath/CultMath.csproj @@ -0,0 +1,24 @@ + + + + net8.0;netstandard2.1 + enable + enable + latest + CultMath + CultMath + GameCult.Math + 0.1.2 + Clean-room C# math shaped for HLSL-style renderer code. + GameCult + README.md + $(NoWarn);CS8981 + true + + + + + + + + diff --git a/packages/cultmath/src/CultMath/CultMath.csproj.meta b/packages/cultmath/src/CultMath/CultMath.csproj.meta new file mode 100644 index 00000000..78fc304d --- /dev/null +++ b/packages/cultmath/src/CultMath/CultMath.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 77e1a0a2512910f41885650a7d0d6f60 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/src/CultMath/CultMathTone.cs b/packages/cultmath/src/CultMath/CultMathTone.cs new file mode 100644 index 00000000..25dfffeb --- /dev/null +++ b/packages/cultmath/src/CultMath/CultMathTone.cs @@ -0,0 +1,10 @@ +namespace CultMath; + +public enum CultMathTone : byte +{ + Background = 0, + Panel = 1, + Edge = 2, + Header = 3, + Body = 4, +} diff --git a/packages/cultmath/src/CultMath/CultMathTone.cs.meta b/packages/cultmath/src/CultMath/CultMathTone.cs.meta new file mode 100644 index 00000000..f41d1475 --- /dev/null +++ b/packages/cultmath/src/CultMath/CultMathTone.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d4bfc66f9f7e992448f884c2c338d50a \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/IsExternalInit.cs b/packages/cultmath/src/CultMath/IsExternalInit.cs new file mode 100644 index 00000000..b05eb4a1 --- /dev/null +++ b/packages/cultmath/src/CultMath/IsExternalInit.cs @@ -0,0 +1,5 @@ +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit +{ +} diff --git a/packages/cultmath/src/CultMath/IsExternalInit.cs.meta b/packages/cultmath/src/CultMath/IsExternalInit.cs.meta new file mode 100644 index 00000000..ca761bcb --- /dev/null +++ b/packages/cultmath/src/CultMath/IsExternalInit.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: de2b3c3e7f03c354aa2b84fbbc55b497 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/Random.cs b/packages/cultmath/src/CultMath/Random.cs new file mode 100644 index 00000000..9931a87f --- /dev/null +++ b/packages/cultmath/src/CultMath/Random.cs @@ -0,0 +1,62 @@ +namespace CultMath; + +public struct Random +{ + private const uint DefaultSeed = 0x6E624EB7u; + + public uint state; + + public Random(uint seed) + { + state = seed == 0 ? DefaultSeed : seed; + } + + public static Random CreateFromIndex(uint index) => new(index + DefaultSeed); + + public void InitState(uint seed) + { + state = seed == 0 ? DefaultSeed : seed; + } + + public uint NextUInt() + { + var x = state == 0 ? DefaultSeed : state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + state = x == 0 ? DefaultSeed : x; + return state; + } + + public int NextInt(int max) => NextInt(0, max); + + public int NextInt(int min, int max) + { + if (max <= min) + { + return min; + } + + return min + (int)(NextUInt() % (uint)(max - min)); + } + + public float NextFloat() => (NextUInt() >> 8) * (1.0f / 16777216.0f); + + public float NextFloat(float max) => NextFloat() * max; + + public float NextFloat(float min, float max) => min + NextFloat() * (max - min); + + public float2 NextFloat2() => new(NextFloat(), NextFloat()); + + public float2 NextFloat2(float min, float max) => new(NextFloat(min, max), NextFloat(min, max)); + + public float2 NextFloat2(float2 min, float2 max) => min + NextFloat2() * (max - min); + + public float2 NextFloat2Direction() + { + var angle = NextFloat(0.0f, math.TAU); + return new float2(math.cos(angle), math.sin(angle)); + } + + public float3 NextFloat3(float3 min, float3 max) => min + new float3(NextFloat(), NextFloat(), NextFloat()) * (max - min); +} diff --git a/packages/cultmath/src/CultMath/Random.cs.meta b/packages/cultmath/src/CultMath/Random.cs.meta new file mode 100644 index 00000000..55032ded --- /dev/null +++ b/packages/cultmath/src/CultMath/Random.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7c1a557259026da4ca20b54264852c13 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/Voronoi.cs b/packages/cultmath/src/CultMath/Voronoi.cs new file mode 100644 index 00000000..38b52d81 --- /dev/null +++ b/packages/cultmath/src/CultMath/Voronoi.cs @@ -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 xs = stackalloc[] { x }; + Span ys = stackalloc[] { y }; + Span tones = stackalloc[] { tone }; + Span spans = stackalloc[] { span }; + Span colors = stackalloc Color32[1]; + SampleTones(xs, ys, tones, spans, resolutionY, frameIndex, colors); + return colors[0]; + } + + public static void SampleTones( + ReadOnlySpan xs, + ReadOnlySpan ys, + ReadOnlySpan tones, + ReadOnlySpan spans, + float resolutionY, + int frameIndex, + Span 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 xs, + ReadOnlySpan ys, + ReadOnlySpan tones, + ReadOnlySpan spans, + float resolutionY, + int frameIndex, + Span 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); + } +} diff --git a/packages/cultmath/src/CultMath/Voronoi.cs.meta b/packages/cultmath/src/CultMath/Voronoi.cs.meta new file mode 100644 index 00000000..6c738085 --- /dev/null +++ b/packages/cultmath/src/CultMath/Voronoi.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cb530f1428ac8ff43939b986986ae959 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/bool2.cs b/packages/cultmath/src/CultMath/bool2.cs new file mode 100644 index 00000000..95ab29eb --- /dev/null +++ b/packages/cultmath/src/CultMath/bool2.cs @@ -0,0 +1,18 @@ +using System; + +namespace CultMath; + +public record struct bool2(bool x, bool y) +{ + public static readonly bool2 @false = new(false, false); + public static readonly bool2 @true = new(true, true); + + public bool this[int index] => index switch + { + 0 => x, + 1 => y, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public static implicit operator bool2(bool value) => new(value, value); +} diff --git a/packages/cultmath/src/CultMath/bool2.cs.meta b/packages/cultmath/src/CultMath/bool2.cs.meta new file mode 100644 index 00000000..477ff144 --- /dev/null +++ b/packages/cultmath/src/CultMath/bool2.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 725efb2725e987449a3782452f5a95e0 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/double2.cs b/packages/cultmath/src/CultMath/double2.cs new file mode 100644 index 00000000..000c3cb0 --- /dev/null +++ b/packages/cultmath/src/CultMath/double2.cs @@ -0,0 +1,25 @@ +using System; + +namespace CultMath; + +public record struct double2(double x, double y) +{ + public static readonly double2 zero = new(0.0, 0.0); + public static readonly double2 one = new(1.0, 1.0); + + public double this[int index] => index switch + { + 0 => x, + 1 => y, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public static implicit operator double2(double value) => new(value, value); + + public static double2 operator +(double2 value) => value; + public static double2 operator -(double2 value) => new(-value.x, -value.y); + public static double2 operator +(double2 left, double2 right) => new(left.x + right.x, left.y + right.y); + public static double2 operator -(double2 left, double2 right) => new(left.x - right.x, left.y - right.y); + public static double2 operator *(double2 left, double2 right) => new(left.x * right.x, left.y * right.y); + public static double2 operator /(double2 left, double2 right) => new(left.x / right.x, left.y / right.y); +} diff --git a/packages/cultmath/src/CultMath/double2.cs.meta b/packages/cultmath/src/CultMath/double2.cs.meta new file mode 100644 index 00000000..61243b2a --- /dev/null +++ b/packages/cultmath/src/CultMath/double2.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bbe739a5f0216c3459762d2b5ee7401a \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/double3.cs b/packages/cultmath/src/CultMath/double3.cs new file mode 100644 index 00000000..b1427505 --- /dev/null +++ b/packages/cultmath/src/CultMath/double3.cs @@ -0,0 +1,36 @@ +using System; + +namespace CultMath; + +public record struct double3(double x, double y, double z) +{ + public static readonly double3 zero = new(0.0, 0.0, 0.0); + public static readonly double3 one = new(1.0, 1.0, 1.0); + + public double2 xy + { + get => new(x, y); + set + { + x = value.x; + y = value.y; + } + } + + public double this[int index] => index switch + { + 0 => x, + 1 => y, + 2 => z, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public static implicit operator double3(double value) => new(value, value, value); + + public static double3 operator +(double3 value) => value; + public static double3 operator -(double3 value) => new(-value.x, -value.y, -value.z); + public static double3 operator +(double3 left, double3 right) => new(left.x + right.x, left.y + right.y, left.z + right.z); + public static double3 operator -(double3 left, double3 right) => new(left.x - right.x, left.y - right.y, left.z - right.z); + public static double3 operator *(double3 left, double3 right) => new(left.x * right.x, left.y * right.y, left.z * right.z); + public static double3 operator /(double3 left, double3 right) => new(left.x / right.x, left.y / right.y, left.z / right.z); +} diff --git a/packages/cultmath/src/CultMath/double3.cs.meta b/packages/cultmath/src/CultMath/double3.cs.meta new file mode 100644 index 00000000..302d13d8 --- /dev/null +++ b/packages/cultmath/src/CultMath/double3.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1cbaafd219703ee43ae279aa5cbf407f \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/float2.cs b/packages/cultmath/src/CultMath/float2.cs new file mode 100644 index 00000000..5298494b --- /dev/null +++ b/packages/cultmath/src/CultMath/float2.cs @@ -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); +} diff --git a/packages/cultmath/src/CultMath/float2.cs.meta b/packages/cultmath/src/CultMath/float2.cs.meta new file mode 100644 index 00000000..c82f69ae --- /dev/null +++ b/packages/cultmath/src/CultMath/float2.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fa066a0aed5494f499d462a2ebc51e50 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/float3.cs b/packages/cultmath/src/CultMath/float3.cs new file mode 100644 index 00000000..4a4efeba --- /dev/null +++ b/packages/cultmath/src/CultMath/float3.cs @@ -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); +} diff --git a/packages/cultmath/src/CultMath/float3.cs.meta b/packages/cultmath/src/CultMath/float3.cs.meta new file mode 100644 index 00000000..3c649c4a --- /dev/null +++ b/packages/cultmath/src/CultMath/float3.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 58a355dd1cf6b9f48a7595eea0c0e1f2 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/float4.cs b/packages/cultmath/src/CultMath/float4.cs new file mode 100644 index 00000000..b6eed097 --- /dev/null +++ b/packages/cultmath/src/CultMath/float4.cs @@ -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); +} diff --git a/packages/cultmath/src/CultMath/float4.cs.meta b/packages/cultmath/src/CultMath/float4.cs.meta new file mode 100644 index 00000000..206444e0 --- /dev/null +++ b/packages/cultmath/src/CultMath/float4.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fe24fbdab33538849bc6be0afbb0a692 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/int2.cs b/packages/cultmath/src/CultMath/int2.cs new file mode 100644 index 00000000..7c2f350b --- /dev/null +++ b/packages/cultmath/src/CultMath/int2.cs @@ -0,0 +1,29 @@ +using System; + +namespace CultMath; + +public record struct int2(int x, int y) +{ + public static readonly int2 zero = new(0, 0); + public static readonly int2 one = new(1, 1); + + public int this[int index] => index switch + { + 0 => x, + 1 => y, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public static implicit operator int2(int value) => new(value, value); + public static implicit operator float2(int2 value) => new(value.x, value.y); + public static explicit operator System.Numerics.Vector2(int2 value) => new(value.x, value.y); + public static explicit operator int2(System.Numerics.Vector2 value) => new((int)value.X, (int)value.Y); + + public static int2 operator +(int2 value) => value; + public static int2 operator -(int2 value) => new(-value.x, -value.y); + public static int2 operator +(int2 left, int2 right) => new(left.x + right.x, left.y + right.y); + public static int2 operator -(int2 left, int2 right) => new(left.x - right.x, left.y - right.y); + public static int2 operator *(int2 left, int2 right) => new(left.x * right.x, left.y * right.y); + public static int2 operator /(int2 left, int2 right) => new(left.x / right.x, left.y / right.y); + public static int2 operator %(int2 left, int2 right) => new(left.x % right.x, left.y % right.y); +} diff --git a/packages/cultmath/src/CultMath/int2.cs.meta b/packages/cultmath/src/CultMath/int2.cs.meta new file mode 100644 index 00000000..5920328a --- /dev/null +++ b/packages/cultmath/src/CultMath/int2.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 577b174a998aaa4478ec26e7a6782f46 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/math.cs b/packages/cultmath/src/CultMath/math.cs new file mode 100644 index 00000000..d0a56e76 --- /dev/null +++ b/packages/cultmath/src/CultMath/math.cs @@ -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); + } +} diff --git a/packages/cultmath/src/CultMath/math.cs.meta b/packages/cultmath/src/CultMath/math.cs.meta new file mode 100644 index 00000000..9222af7d --- /dev/null +++ b/packages/cultmath/src/CultMath/math.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0d9b4cacc40d169429ee743d19577cb7 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/quaternion.cs b/packages/cultmath/src/CultMath/quaternion.cs new file mode 100644 index 00000000..fe8dda8e --- /dev/null +++ b/packages/cultmath/src/CultMath/quaternion.cs @@ -0,0 +1,68 @@ +namespace CultMath; + +public record struct quaternion(float x, float y, float z, float w) +{ + public static readonly quaternion identity = new(0.0f, 0.0f, 0.0f, 1.0f); + + public static quaternion LookRotation(float3 forward, float3 up) + { + forward = math.normalize(forward); + up = math.normalize(up); + + var right = math.normalize(math.cross(up, forward)); + up = math.cross(forward, right); + + var m00 = right.x; + var m01 = up.x; + var m02 = forward.x; + var m10 = right.y; + var m11 = up.y; + var m12 = forward.y; + var m20 = right.z; + var m21 = up.z; + var m22 = forward.z; + + var trace = m00 + m11 + m22; + if (trace > 0.0f) + { + var s = math.sqrt(trace + 1.0f) * 2.0f; + return math.normalize(new quaternion( + (m21 - m12) / s, + (m02 - m20) / s, + (m10 - m01) / s, + 0.25f * s)); + } + + if (m00 > m11 && m00 > m22) + { + var s = math.sqrt(1.0f + m00 - m11 - m22) * 2.0f; + return math.normalize(new quaternion( + 0.25f * s, + (m01 + m10) / s, + (m02 + m20) / s, + (m21 - m12) / s)); + } + + if (m11 > m22) + { + var s = math.sqrt(1.0f + m11 - m00 - m22) * 2.0f; + return math.normalize(new quaternion( + (m01 + m10) / s, + 0.25f * s, + (m12 + m21) / s, + (m02 - m20) / s)); + } + + { + var s = math.sqrt(1.0f + m22 - m00 - m11) * 2.0f; + return math.normalize(new quaternion( + (m02 + m20) / s, + (m12 + m21) / s, + 0.25f * s, + (m10 - m01) / s)); + } + } + + public static implicit operator quaternion(float4 value) => new(value.x, value.y, value.z, value.w); + public static implicit operator float4(quaternion value) => new(value.x, value.y, value.z, value.w); +} diff --git a/packages/cultmath/src/CultMath/quaternion.cs.meta b/packages/cultmath/src/CultMath/quaternion.cs.meta new file mode 100644 index 00000000..ae6e8a11 --- /dev/null +++ b/packages/cultmath/src/CultMath/quaternion.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 12f3627d9250dc348be9c1a1056ec7d0 \ No newline at end of file diff --git a/packages/cultmath/src/CultMath/rect.cs b/packages/cultmath/src/CultMath/rect.cs new file mode 100644 index 00000000..51e05c19 --- /dev/null +++ b/packages/cultmath/src/CultMath/rect.cs @@ -0,0 +1,59 @@ +namespace CultMath; + +/// +/// Axis-aligned 2D bounds stored canonically as normalized minimum and maximum +/// corners. Point containment and intersection include the boundary. +/// +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)); +} diff --git a/packages/cultmath/src/CultMath/rect.cs.meta b/packages/cultmath/src/CultMath/rect.cs.meta new file mode 100644 index 00000000..0b04a5ce --- /dev/null +++ b/packages/cultmath/src/CultMath/rect.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5aab0d8e1ca94d42b85bba780a7ee449 diff --git a/packages/cultmath/tests.meta b/packages/cultmath/tests.meta new file mode 100644 index 00000000..a35356c4 --- /dev/null +++ b/packages/cultmath/tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4ca61dedbfb3c5d46b0acaf23202b5c2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/tests/CultMath.Tests.meta b/packages/cultmath/tests/CultMath.Tests.meta new file mode 100644 index 00000000..3f1451c6 --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bcee65f13f85daa45b0cf3fdaa901779 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj b/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj new file mode 100644 index 00000000..1e77eba1 --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + diff --git a/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj.meta b/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj.meta new file mode 100644 index 00000000..709184bb --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 636051db121e19146a0baf92b790415c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs new file mode 100644 index 00000000..0034a283 --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs @@ -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."); + } +} diff --git a/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs.meta b/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs.meta new file mode 100644 index 00000000..1fcf44b3 --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/HlslMirrorTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3d7eb9df849ea0148a71419bbfe2352c \ No newline at end of file diff --git a/packages/cultmath/tests/CultMath.Tests/MathTests.cs b/packages/cultmath/tests/CultMath.Tests/MathTests.cs new file mode 100644 index 00000000..652ba23a --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/MathTests.cs @@ -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); + } +} diff --git a/packages/cultmath/tests/CultMath.Tests/MathTests.cs.meta b/packages/cultmath/tests/CultMath.Tests/MathTests.cs.meta new file mode 100644 index 00000000..277df5a3 --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/MathTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c7a709e5345da214aa650e8c796fcaff \ No newline at end of file diff --git a/packages/cultmath/tools.meta b/packages/cultmath/tools.meta new file mode 100644 index 00000000..3594fa6a --- /dev/null +++ b/packages/cultmath/tools.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 697162910ffd9614ba9655d31af9afa4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/tools/compile-hlsl-spirv.ps1 b/packages/cultmath/tools/compile-hlsl-spirv.ps1 new file mode 100644 index 00000000..280e08d5 --- /dev/null +++ b/packages/cultmath/tools/compile-hlsl-spirv.ps1 @@ -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 diff --git a/packages/cultmath/tools/compile-hlsl-spirv.ps1.meta b/packages/cultmath/tools/compile-hlsl-spirv.ps1.meta new file mode 100644 index 00000000..31e7948a --- /dev/null +++ b/packages/cultmath/tools/compile-hlsl-spirv.ps1.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b47e7016a99a5d148bc44c399c13c4d3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/tools/compile-spirv-metal.ps1 b/packages/cultmath/tools/compile-spirv-metal.ps1 new file mode 100644 index 00000000..490e6331 --- /dev/null +++ b/packages/cultmath/tools/compile-spirv-metal.ps1 @@ -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 diff --git a/packages/cultmath/tools/compile-spirv-metal.ps1.meta b/packages/cultmath/tools/compile-spirv-metal.ps1.meta new file mode 100644 index 00000000..2e7e74c2 --- /dev/null +++ b/packages/cultmath/tools/compile-spirv-metal.ps1.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6150c8f461e471b4c8641fd32d1984ef +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/tools/get-dxc.ps1 b/packages/cultmath/tools/get-dxc.ps1 new file mode 100644 index 00000000..2f3363f3 --- /dev/null +++ b/packages/cultmath/tools/get-dxc.ps1 @@ -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" diff --git a/packages/cultmath/tools/get-dxc.ps1.meta b/packages/cultmath/tools/get-dxc.ps1.meta new file mode 100644 index 00000000..c9ab0d82 --- /dev/null +++ b/packages/cultmath/tools/get-dxc.ps1.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 86d0b9a1006042745b7fff523831049c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/tools/get-spirv-cross.ps1 b/packages/cultmath/tools/get-spirv-cross.ps1 new file mode 100644 index 00000000..f5105b9a --- /dev/null +++ b/packages/cultmath/tools/get-spirv-cross.ps1 @@ -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" diff --git a/packages/cultmath/tools/get-spirv-cross.ps1.meta b/packages/cultmath/tools/get-spirv-cross.ps1.meta new file mode 100644 index 00000000..12ccdd8e --- /dev/null +++ b/packages/cultmath/tools/get-spirv-cross.ps1.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dcf41da07502f4b439f8fc5ad25d16c9 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/unity.meta b/packages/cultmath/unity.meta new file mode 100644 index 00000000..de47a618 --- /dev/null +++ b/packages/cultmath/unity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 366efeaef9f68224b80229332c7ecfc6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/unity/org.gamecult.cultmath/LICENSE.md b/packages/cultmath/unity/org.gamecult.cultmath/LICENSE.md new file mode 100644 index 00000000..5e50f2d6 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/LICENSE.md @@ -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 +. 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`. diff --git a/packages/cultmath/unity/org.gamecult.cultmath/LICENSE.md.meta b/packages/cultmath/unity/org.gamecult.cultmath/LICENSE.md.meta new file mode 100644 index 00000000..c234cd28 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/LICENSE.md.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7172bce28532878409e57f5fbbb12719 \ No newline at end of file diff --git a/packages/cultmath/unity/org.gamecult.cultmath/README.md b/packages/cultmath/unity/org.gamecult.cultmath/README.md new file mode 100644 index 00000000..7d9c34bf --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/README.md @@ -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. diff --git a/packages/cultmath/unity/org.gamecult.cultmath/README.md.meta b/packages/cultmath/unity/org.gamecult.cultmath/README.md.meta new file mode 100644 index 00000000..0ea270c2 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/README.md.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b7429934026d747489a3876fd15576e5 \ No newline at end of file diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Runtime.meta b/packages/cultmath/unity/org.gamecult.cultmath/Runtime.meta new file mode 100644 index 00000000..b0e76599 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a31bf2bb35e54dd1ad1c765337682769 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins.meta b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins.meta new file mode 100644 index 00000000..fe8acafa --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f84156013e814c14b73f668b6610b9e0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.dll b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.dll new file mode 100644 index 00000000..379dad44 Binary files /dev/null and b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.dll differ diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.dll.meta b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.dll.meta new file mode 100644 index 00000000..917e3849 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.dll.meta @@ -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: diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.pdb b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.pdb new file mode 100644 index 00000000..223b4de8 Binary files /dev/null and b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.pdb differ diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.pdb.meta b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.pdb.meta new file mode 100644 index 00000000..e18fedbe --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.pdb.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a8cc555f643a4d7eb4dd340025b208f6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders.meta b/packages/cultmath/unity/org.gamecult.cultmath/Shaders.meta new file mode 100644 index 00000000..1e7e7110 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ca5d773061ed49b3a173526048d40cb3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl new file mode 100644 index 00000000..4c00fb69 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -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 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 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 diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl.meta b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl.meta new file mode 100644 index 00000000..d3b6d2b3 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a46136cb4acb4ff2bc47b6a4f786e4b3 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/cultmath/unity/org.gamecult.cultmath/THIRD-PARTY-NOTICES.md b/packages/cultmath/unity/org.gamecult.cultmath/THIRD-PARTY-NOTICES.md new file mode 100644 index 00000000..c283df2b --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/THIRD-PARTY-NOTICES.md @@ -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: +- License: MIT. diff --git a/packages/cultmath/unity/org.gamecult.cultmath/THIRD-PARTY-NOTICES.md.meta b/packages/cultmath/unity/org.gamecult.cultmath/THIRD-PARTY-NOTICES.md.meta new file mode 100644 index 00000000..a53f08d8 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/THIRD-PARTY-NOTICES.md.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0c88669e4d1db604588be2a726b7d231 \ No newline at end of file diff --git a/packages/cultmath/unity/org.gamecult.cultmath/package.json b/packages/cultmath/unity/org.gamecult.cultmath/package.json new file mode 100644 index 00000000..80ceddad --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/package.json @@ -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" + ] +} diff --git a/packages/cultmath/unity/org.gamecult.cultmath/package.json.meta b/packages/cultmath/unity/org.gamecult.cultmath/package.json.meta new file mode 100644 index 00000000..337ba716 --- /dev/null +++ b/packages/cultmath/unity/org.gamecult.cultmath/package.json.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 33d8d4af49bf0f946ad00a375f26b1d1 \ No newline at end of file diff --git a/packages/cultmath/web.meta b/packages/cultmath/web.meta new file mode 100644 index 00000000..d65ff3e6 --- /dev/null +++ b/packages/cultmath/web.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: daf45471f8b5f4f44a2cb86e77e010db +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: