Aetheria/Assets/Scripts/ServerShared/SampleElimination.cs
Meta Vens 6140a06a0a Swap Unity.Mathematics usings for CultMath
Every Unity.Mathematics using, alias and qualified name under Assets\Scripts
and tools now names CultMath. The nine `using static ...noise;` lines are
gone: snoise lives on CultMath's math. UnityExtensions loses float4.ToColor
and Color.ToFloat4, which duplicate the bridge's ToColor and ToCultMath; the
float3 colour pair stays because the bridge has none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:39:12 +02:00

171 lines
No EOL
6.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using DataStructures.ViliWonka.Heap;
using DataStructures.ViliWonka.KDTree;
using CultMath;
using static CultMath.math;
using float2 = CultMath.float2;
using Random = CultMath.Random;
public static class WeightedSampleElimination
{
private static float2 boundsMin = float2.zero; // The minimum bounds of the sampling domain
private static float2 boundsMax = float2(1); // The maximum bounds of the sampling domain
// Returns the minimum bounds of the sampling domain.
// The sampling domain boundaries are used for tiling and computing the maximum possible
// Poisson disk radius for the sampling domain. The default boundaries are between 0 and 1.
public static float2 BoundsMin => boundsMin;
// Returns the maximum bounds of the sampling domain.
// The sampling domain boundaries are used for tiling and computing the maximum possible
// Poisson disk radius for the sampling domain. The default boundaries are between 0 and 1.
public static float2 BoundsMax => boundsMax;
// This is the main method that uses weighted sample elimination for selecting a subset of samples
// with blue noise (Poisson disk) characteristics from a given input sample set (inputPoints).
// The selected samples are copied to outputPoints. The output size must be smaller than the input size.
//
// If the progressive parameter is true, the output sample points are ordered for progressive sampling,
// such that when the samples are introduced one by one in this order, each subset in the sequence
// exhibits blue noise characteristics.
//
// The d_max parameter defines radius within which the weight function is non-zero.
//
// The weight function is the crucial component of weighted sample elimination. It computes the weight
// of a sample point based on the placement of its neighbors within d_max radius. The weight function
// must have the following form:
//
// float weightFunction( float2 p0, float2 p1, float dist2, float dmax, float density )
//
// The arguments p0 and p1 are the two neighboring points, dist2 is the square of the Euclidean distance
// between these two points, and d_max is the current radius for the weight function.
// Note that if the progressive parameter is on, the d_max value sent to the weight function can be
// different than the d_max value passed to this method.
public static void Eliminate (
float2[] inputPoints,
float2[] outputPoints,
Func<float2, float2, float, float, float, float> weightFunction,
Func<float2, float> densityFunction,
float d_max = 0)
{
if ( d_max < .001f ) d_max = 2 * GetMaxPoissonDiskRadius( outputPoints.Length );
DoEliminate( inputPoints, outputPoints, d_max, weightFunction, densityFunction );
}
public static void Eliminate(
float2[] inputPoints,
float2[] outputPoints,
Func<float2, float> densityFunc = null,
float d_max = 0)
{
if (densityFunc == null) densityFunc = v => 1;
if ( d_max < .001f ) d_max = 2 * GetMaxPoissonDiskRadius( outputPoints.Length );
Eliminate( inputPoints, outputPoints, (p0, p1, d2, dmax, density) =>
{
float d = sqrt(d2);
return pow( 1 - d / dmax, 5 + 6 * density );
}, densityFunc, d_max);
}
public static float2[] GeneratePoints(int count, ref Random random, Func<float2, float> density = null, Func<float2, float> envelope = null, Action<string> progressCallback = null)
{
if (density == null) density = v => .5f;
if (envelope == null) envelope = v => 1;
var inputSamples = new float2[count * 8];
var sample = 0;
var accumulator = 0f;
while (sample < inputSamples.Length)
{
var v = random.NextFloat2();
accumulator += pow(saturate(density(v)), 2f) * saturate(envelope(v));
if (accumulator > .5f)
{
accumulator = 0;
inputSamples[sample++] = v;
progressCallback?.Invoke($"Generating Samples: {sample} / {inputSamples.Length}");
}
}
var outputSamples = new float2[count];
progressCallback?.Invoke("Eliminating Samples");
Eliminate(inputSamples, outputSamples, density);
return outputSamples;
}
// Returns the maximum possible Poisson disk radius in the given dimensions for the given sampleCount
// to spread over the given domainSize. If the domainSize argument is zero or negative, it is computed
// as the area or N-dimensional volume of the box defined by the minimum and maximum bounds.
// This method is used for the default weight function.
static float GetMaxPoissonDiskRadius( int sampleCount )
{
var domainSize = boundsMax[0] - boundsMin[0];
domainSize *= domainSize;
float sampleArea = domainSize / sampleCount;
return sqrt( sampleArea / ( 2 * sqrt(3) ) );
}
// This is the method that performs weighted sample elimination.
static void DoEliminate(
float2[] inputPoints,
float2[] outputPoints,
float d_max,
Func<float2, float2, float, float, float, float> weightFunction,
Func<float2, float> density
)
{
// Build a k-d tree for samples
var kdtree = new KDTree(inputPoints);
var query = new KDQuery();
var dmax2 = d_max * d_max;
// Assign weights to each sample
float[] weights = new float[inputPoints.Length];
float[] densities = new float[inputPoints.Length];
for (int i = 0; i < inputPoints.Length; i++)
{
var point = inputPoints[i];
densities[i] += density(point);
foreach (var (qi, d2) in query.Radius(kdtree, inputPoints[i], d_max))
{
var otherPoint = inputPoints[qi];
weights[i] += weightFunction(point, otherPoint, d2, d_max, densities[i]);
}
}
// Build a heap for the samples using their weights
MaxHeap<int> heap = new MaxHeap<int>(inputPoints.Length);
for (int i = 0; i < inputPoints.Length; i++)
heap.PushObj(i, weights[i]);
// While the number of samples is greater than desired
int sampleCount = inputPoints.Length;
while (sampleCount > outputPoints.Length)
{
// Pull the top sample from heap
var elim = heap.PopObj();
var pElim = inputPoints[elim];
// For each sample around it, remove its weight contribution and update the heap
foreach (var (qi, d2) in query.Radius(kdtree, pElim, d_max))
{
var point = inputPoints[qi];
weights[qi] -= weightFunction(point, pElim, d2, d_max, densities[qi]);
heap.SetValue(qi, weights[qi]);
}
sampleCount--;
}
var outputIndex = 0;
foreach (var i in heap.FlushResult())
{
outputPoints[outputIndex++] = inputPoints[i];
}
}
// Returns the minimum radius fraction used by the default weight function.
static float GetWeightLimitFraction( int inputSize, int outputSize, float beta, float gamma )
{
float ratio = (float) outputSize / inputSize;
return ( 1 - pow( ratio, gamma ) ) * beta;
}
}