Add Rust-backed Voronoi batch kernel
This commit is contained in:
parent
5cef6d951e
commit
bfe356fe92
11 changed files with 461 additions and 16 deletions
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);
|
||||
}
|
||||
}
|
||||
Reference in a new issue