Add Rust-backed Voronoi batch kernel
This commit is contained in:
parent
5cef6d951e
commit
bfe356fe92
11 changed files with 461 additions and 16 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -1,7 +1,6 @@
|
|||
bin/
|
||||
obj/
|
||||
**/bin/
|
||||
**/obj/
|
||||
artifacts/
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
.idea/
|
||||
target/
|
||||
**/target/
|
||||
|
|
|
|||
32
README.md
32
README.md
|
|
@ -1,11 +1,12 @@
|
|||
# CultMath
|
||||
|
||||
Clean-room C# math shaped for people who think in HLSL.
|
||||
Clean-room math shaped for people who think in HLSL.
|
||||
|
||||
CultMath provides lowercase vector types and shader-style intrinsics so
|
||||
math-heavy renderer code can move between C# 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 and plain .NET.
|
||||
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
|
||||
|
||||
|
|
@ -17,6 +18,17 @@ small, Aquarium-born library built from public shader semantics and plain .NET.
|
|||
- `math` intrinsics: `radians`, `degrees`, `abs`, `floor`, `ceil`, `frac`,
|
||||
`min`, `max`, `clamp`, `saturate`, `lerp`, `step`, `smoothstep`, `dot`,
|
||||
`cross`, `length`, `distance`, `normalize`, and `reflect`
|
||||
- `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
|
||||
|
||||
|
|
@ -33,11 +45,13 @@ float glow = math.smoothstep(0.2f, 1.0f, rim);
|
|||
```powershell
|
||||
dotnet build CultMath.slnx
|
||||
dotnet test CultMath.slnx
|
||||
cargo test --manifest-path native/cultmath-core/Cargo.toml
|
||||
cargo build --release --manifest-path native/cultmath-core/Cargo.toml
|
||||
```
|
||||
|
||||
## Doctrine
|
||||
|
||||
The goal is mechanical sympathy between CPU-side authoring and GPU-side shader
|
||||
math. If an intrinsic exists here, it should behave like the shader concept
|
||||
unless .NET numeric rules make that impossible. Weird deviations get tests and
|
||||
documentation, not folklore.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
# Design
|
||||
|
||||
CultMath exists to make renderer math readable in both places it needs to live:
|
||||
C# host code and HLSL shader code.
|
||||
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.
|
||||
- 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
|
||||
|
|
|
|||
7
native/cultmath-core/Cargo.lock
generated
Normal file
7
native/cultmath-core/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "cultmath-core"
|
||||
version = "0.1.0"
|
||||
13
native/cultmath-core/Cargo.toml
Normal file
13
native/cultmath-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "cultmath-core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "cultmath_core"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
188
native/cultmath-core/src/lib.rs
Normal file
188
native/cultmath-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#[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.25, 6.0),
|
||||
2 => (0.95, 20.0),
|
||||
_ => (0.16, 2.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 candidate_red = 0.5 + 0.5 * (seed * 2.5 + 3.5 + 2.0).sin();
|
||||
let candidate_green = 0.5 + 0.5 * (seed * 2.5 + 3.5 + 3.0).sin();
|
||||
let candidate_blue = 0.5 + 0.5 * (seed * 2.5 + 3.5 + 2.0).sin();
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
4
src/CultMath/Color32.cs
Normal file
4
src/CultMath/Color32.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace CultMath;
|
||||
|
||||
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||
public readonly record struct Color32(byte r, byte g, byte b, byte a = 255);
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
<RepositoryUrl>https://github.com/GameCult/CultMath</RepositoryUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<NoWarn>$(NoWarn);CS8981</NoWarn>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
10
src/CultMath/CultMathTone.cs
Normal file
10
src/CultMath/CultMathTone.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace CultMath;
|
||||
|
||||
public enum CultMathTone : byte
|
||||
{
|
||||
Background = 0,
|
||||
Panel = 1,
|
||||
Edge = 2,
|
||||
Header = 3,
|
||||
Body = 4,
|
||||
}
|
||||
190
src/CultMath/Voronoi.cs
Normal file
190
src/CultMath/Voronoi.cs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CultMath;
|
||||
|
||||
public static class Voronoi
|
||||
{
|
||||
public static bool NativeAvailable { get; private set; } = true;
|
||||
|
||||
public static Color32 SampleTone(float x, float y, float resolutionY, int frameIndex, CultMathTone tone, float span)
|
||||
{
|
||||
Span<float> xs = stackalloc[] { x };
|
||||
Span<float> ys = stackalloc[] { y };
|
||||
Span<CultMathTone> tones = stackalloc[] { tone };
|
||||
Span<float> spans = stackalloc[] { span };
|
||||
Span<Color32> colors = stackalloc Color32[1];
|
||||
SampleTones(xs, ys, tones, spans, resolutionY, frameIndex, colors);
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
public static void SampleTones(
|
||||
ReadOnlySpan<float> xs,
|
||||
ReadOnlySpan<float> ys,
|
||||
ReadOnlySpan<CultMathTone> tones,
|
||||
ReadOnlySpan<float> spans,
|
||||
float resolutionY,
|
||||
int frameIndex,
|
||||
Span<Color32> colors)
|
||||
{
|
||||
if (xs.Length != ys.Length || xs.Length != tones.Length || xs.Length != spans.Length || xs.Length != colors.Length)
|
||||
{
|
||||
throw new ArgumentException("CultMath Voronoi batch spans must have equal length.");
|
||||
}
|
||||
|
||||
if (xs.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (NativeAvailable && TrySampleNative(xs, ys, tones, spans, resolutionY, frameIndex, colors))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var index = 0; index < xs.Length; index++)
|
||||
{
|
||||
colors[index] = SampleManaged(xs[index], ys[index], resolutionY, frameIndex, tones[index], spans[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool TrySampleNative(
|
||||
ReadOnlySpan<float> xs,
|
||||
ReadOnlySpan<float> ys,
|
||||
ReadOnlySpan<CultMathTone> tones,
|
||||
ReadOnlySpan<float> spans,
|
||||
float resolutionY,
|
||||
int frameIndex,
|
||||
Span<Color32> colors)
|
||||
{
|
||||
try
|
||||
{
|
||||
fixed (float* xPtr = xs)
|
||||
fixed (float* yPtr = ys)
|
||||
fixed (CultMathTone* tonePtr = tones)
|
||||
fixed (float* spanPtr = spans)
|
||||
fixed (Color32* colorPtr = colors)
|
||||
{
|
||||
var result = Native.cultmath_apollonian_voronoi_tones(
|
||||
xPtr,
|
||||
yPtr,
|
||||
(byte*)tonePtr,
|
||||
spanPtr,
|
||||
(nuint)xs.Length,
|
||||
resolutionY,
|
||||
frameIndex,
|
||||
colorPtr);
|
||||
if (result == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
NativeAvailable = false;
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
NativeAvailable = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Color32 SampleManaged(float x, float y, float resolutionY, int frameIndex, CultMathTone tone, float span)
|
||||
{
|
||||
var sample = StableSample(x, y, resolutionY, frameIndex, tone, span);
|
||||
var luminance = math.saturate(sample.x * 0.299f + sample.y * 0.587f + sample.z * 0.114f);
|
||||
if (tone == CultMathTone.Header)
|
||||
{
|
||||
var glow = 0.62f + luminance * 0.38f;
|
||||
return new Color32((byte)(255 * glow), (byte)(96 * glow), (byte)(14 * glow));
|
||||
}
|
||||
|
||||
if (tone == CultMathTone.Body)
|
||||
{
|
||||
var glow = 0.68f + luminance * 0.32f;
|
||||
return new Color32((byte)(245 * glow), (byte)(248 * glow), (byte)(252 * glow));
|
||||
}
|
||||
|
||||
var gain = tone switch
|
||||
{
|
||||
CultMathTone.Panel => 0.25f,
|
||||
CultMathTone.Edge => 0.95f,
|
||||
_ => 0.16f,
|
||||
};
|
||||
var lift = tone switch
|
||||
{
|
||||
CultMathTone.Panel => 6.0f,
|
||||
CultMathTone.Edge => 20.0f,
|
||||
_ => 2.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 candidate = 0.5f + 0.5f * new float3(
|
||||
math.sin(seed * 2.5f + 3.5f + 2.0f),
|
||||
math.sin(seed * 2.5f + 3.5f + 3.0f),
|
||||
math.sin(seed * 2.5f + 3.5f + 2.0f));
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,4 +52,20 @@ public sealed class MathTests
|
|||
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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue