Add physical erosion frequency bands
This commit is contained in:
parent
33c017b284
commit
ddeac557cb
4 changed files with 136 additions and 11 deletions
|
|
@ -19,6 +19,28 @@ struct CultMathAdvancedErosionResult
|
|||
float3 delta; float magnitude; float ridge_map; float fade_target;
|
||||
};
|
||||
|
||||
struct CultMathErosionBandSelection
|
||||
{
|
||||
int active_octaves; float final_octave_weight; float finest_included_wavelength; float unresolved_height_bound;
|
||||
};
|
||||
|
||||
CultMathErosionBandSelection cultmath_select_erosion_bands(float base_wavelength, float sample_spacing, int maximum_octaves, float lacunarity, float base_amplitude, float gain, float transition_ratio)
|
||||
{
|
||||
base_wavelength=max(base_wavelength,1.0e-6); sample_spacing=max(sample_spacing,0.0); lacunarity=max(lacunarity,1.01); transition_ratio=max(transition_ratio,1.01); maximum_octaves=min(max(maximum_octaves,0),16);
|
||||
float nyquist=sample_spacing*2.0, wavelength=base_wavelength, final_weight=0.0, finest=0.0; int active=0;
|
||||
[loop] for(int octave=0; octave<maximum_octaves; octave++)
|
||||
{
|
||||
float weight=nyquist<=0.0?1.0:smoothstep(nyquist,nyquist*transition_ratio,wavelength);
|
||||
if(weight<=0.0) break; active++; final_weight=weight; finest=wavelength; wavelength/=lacunarity;
|
||||
}
|
||||
float omitted=0.0, amplitude=abs(base_amplitude);
|
||||
[loop] for(int index=0; index<maximum_octaves; index++)
|
||||
{
|
||||
if(index>=active) omitted+=amplitude; else if(index==active-1) omitted+=amplitude*(1.0-final_weight); amplitude*=abs(gain);
|
||||
}
|
||||
CultMathErosionBandSelection result; result.active_octaves=active; result.final_octave_weight=active>0?final_weight:0.0; result.finest_included_wavelength=finest; result.unresolved_height_bound=omitted; return result;
|
||||
}
|
||||
|
||||
float cultmath_erosion_hash_unit(int2 cell, uint salt)
|
||||
{
|
||||
uint value = (uint)cell.x * 0x8da6b343u ^ (uint)cell.y * 0xd8163841u ^ salt;
|
||||
|
|
@ -68,7 +90,7 @@ float2 cultmath_erosion_safe_normalize(float2 value)
|
|||
return abs(value_length) > 1.0e-10 ? value / value_length : value;
|
||||
}
|
||||
|
||||
CultMathAdvancedErosionResult cultmath_advanced_erosion_filter(float2 position, float3 base_height_and_slope, float fade_target, CultMathAdvancedErosionParameters p)
|
||||
CultMathAdvancedErosionResult cultmath_advanced_erosion_filter_banded(float2 position, float3 base_height_and_slope, float fade_target, CultMathAdvancedErosionParameters p, CultMathErosionBandSelection band)
|
||||
{
|
||||
float strength = p.strength * p.scale;
|
||||
fade_target = clamp(fade_target, -1.0, 1.0);
|
||||
|
|
@ -85,7 +107,8 @@ CultMathAdvancedErosionResult cultmath_advanced_erosion_filter(float2 position,
|
|||
float ridge_fade = fade_target;
|
||||
float2 gully_slope = lerp(slope, slope / slope_length * p.assumed_slope.x, p.assumed_slope.y);
|
||||
|
||||
[loop] for (int octave = 0; octave < min(max(p.octaves, 0), 8); octave++)
|
||||
int octave_count=min(min(max(p.octaves,0),8),max(band.active_octaves,0));
|
||||
[loop] for (int octave = 0; octave < octave_count; octave++)
|
||||
{
|
||||
float4 phacelle = cultmath_phacelle_noise(position * frequency, cultmath_erosion_safe_normalize(gully_slope), p.cell_scale, 0.25, p.normalization);
|
||||
float2 derivative_direction = phacelle.zw * -frequency;
|
||||
|
|
@ -93,14 +116,15 @@ CultMathAdvancedErosionResult cultmath_advanced_erosion_filter(float2 position,
|
|||
gully_slope += sign(phacelle.y) * derivative_direction * strength * p.gully_weight;
|
||||
float3 gullies = float3(phacelle.x, phacelle.y * derivative_direction.x, phacelle.y * derivative_direction.y);
|
||||
float3 faded = lerp(float3(fade_target, 0.0, 0.0), gullies * p.gully_weight, combined_mask);
|
||||
height_and_slope += faded * strength;
|
||||
magnitude += strength;
|
||||
float octave_weight=octave==octave_count-1?saturate(band.final_octave_weight):1.0;
|
||||
height_and_slope += faded * strength * octave_weight;
|
||||
magnitude += strength * octave_weight;
|
||||
fade_target = faded.x;
|
||||
float octave_rounding = lerp(p.rounding.y, p.rounding.x, saturate(phacelle.x + 0.5)) * rounding_multiplier;
|
||||
float new_mask = cultmath_erosion_ease_out(cultmath_erosion_smooth_start(sloping * p.onset.y, octave_rounding * p.onset.y));
|
||||
combined_mask = cultmath_erosion_pow_inverse(combined_mask, p.detail) * new_mask;
|
||||
ridge_fade = lerp(ridge_fade, gullies.x, ridge_mask);
|
||||
ridge_mask *= cultmath_erosion_ease_out(sloping * p.onset.w);
|
||||
ridge_fade = lerp(ridge_fade, lerp(ridge_fade, gullies.x, ridge_mask), octave_weight);
|
||||
ridge_mask *= lerp(1.0, cultmath_erosion_ease_out(sloping * p.onset.w), octave_weight);
|
||||
strength *= p.gain;
|
||||
frequency *= p.lacunarity;
|
||||
rounding_multiplier *= p.rounding.w;
|
||||
|
|
@ -114,4 +138,10 @@ CultMathAdvancedErosionResult cultmath_advanced_erosion_filter(float2 position,
|
|||
return result;
|
||||
}
|
||||
|
||||
CultMathAdvancedErosionResult cultmath_advanced_erosion_filter(float2 position, float3 base_height_and_slope, float fade_target, CultMathAdvancedErosionParameters p)
|
||||
{
|
||||
CultMathErosionBandSelection band; band.active_octaves=p.octaves; band.final_octave_weight=1.0; band.finest_included_wavelength=0.0; band.unresolved_height_bound=0.0;
|
||||
return cultmath_advanced_erosion_filter_banded(position, base_height_and_slope, fade_target, p, band);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ public readonly record struct AdvancedErosionResult(float3 Delta, float Magnitud
|
|||
public static class AdvancedErosionFilter
|
||||
{
|
||||
public static AdvancedErosionResult Sample(float2 position, float3 baseHeightAndSlope, float fadeTarget, AdvancedErosionParameters p)
|
||||
=> Sample(position, baseHeightAndSlope, fadeTarget, p, new ErosionBandSelection(p.Octaves, 1.0f, 0.0f, 0.0f));
|
||||
|
||||
public static AdvancedErosionResult Sample(float2 position, float3 baseHeightAndSlope, float fadeTarget, AdvancedErosionParameters p, ErosionBandSelection band)
|
||||
{
|
||||
var strength = p.Strength * p.Scale;
|
||||
fadeTarget = math.clamp(fadeTarget, -1.0f, 1.0f);
|
||||
|
|
@ -51,7 +54,7 @@ public static class AdvancedErosionFilter
|
|||
var ridgeFade = fadeTarget;
|
||||
var gullySlope = math.lerp(slope, slope / slopeLength * p.AssumedSlope.x, p.AssumedSlope.y);
|
||||
|
||||
var octaves = math.min(math.max(p.Octaves, 0), 8);
|
||||
var octaves = math.min(math.min(math.max(p.Octaves, 0), 8), math.max(band.ActiveOctaves, 0));
|
||||
for (var octave = 0; octave < octaves; octave++)
|
||||
{
|
||||
var phacelle = Phacelle(position * frequency, SafeNormalize(gullySlope), p.CellScale, 0.25f, p.Normalization);
|
||||
|
|
@ -61,16 +64,17 @@ public static class AdvancedErosionFilter
|
|||
|
||||
var gullies = new float3(phacelle.x, phacelle.y * derivativeDirection.x, phacelle.y * derivativeDirection.y);
|
||||
var faded = math.lerp(new float3(fadeTarget, 0.0f, 0.0f), gullies * p.GullyWeight, combinedMask);
|
||||
heightAndSlope += faded * strength;
|
||||
magnitude += strength;
|
||||
var octaveWeight = octave == octaves - 1 ? math.saturate(band.FinalOctaveWeight) : 1.0f;
|
||||
heightAndSlope += faded * strength * octaveWeight;
|
||||
magnitude += strength * octaveWeight;
|
||||
fadeTarget = faded.x;
|
||||
|
||||
var octaveRounding = math.lerp(p.Rounding.y, p.Rounding.x, math.saturate(phacelle.x + 0.5f)) * roundingMultiplier;
|
||||
var newMask = EaseOut(SmoothStart(sloping * p.Onset.y, octaveRounding * p.Onset.y));
|
||||
combinedMask = PowInverse(combinedMask, p.Detail) * newMask;
|
||||
|
||||
ridgeFade = math.lerp(ridgeFade, gullies.x, ridgeMask);
|
||||
ridgeMask *= EaseOut(sloping * p.Onset.w);
|
||||
ridgeFade = math.lerp(ridgeFade, math.lerp(ridgeFade, gullies.x, ridgeMask), octaveWeight);
|
||||
ridgeMask *= math.lerp(1.0f, EaseOut(sloping * p.Onset.w), octaveWeight);
|
||||
strength *= p.Gain;
|
||||
frequency *= p.Lacunarity;
|
||||
roundingMultiplier *= p.Rounding.w;
|
||||
|
|
|
|||
57
src/CultMath/ErosionFrequencyBands.cs
Normal file
57
src/CultMath/ErosionFrequencyBands.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
namespace CultMath;
|
||||
|
||||
public readonly record struct ErosionBandSelection(
|
||||
int ActiveOctaves,
|
||||
float FinalOctaveWeight,
|
||||
float FinestIncludedWavelength,
|
||||
float UnresolvedHeightBound)
|
||||
{
|
||||
public bool HasSamples => ActiveOctaves > 0;
|
||||
}
|
||||
|
||||
public static class ErosionFrequencyBands
|
||||
{
|
||||
public static ErosionBandSelection Select(
|
||||
float baseWavelength,
|
||||
float sampleSpacing,
|
||||
int maximumOctaves,
|
||||
float lacunarity,
|
||||
float baseAmplitude,
|
||||
float gain,
|
||||
float transitionRatio = 2.0f)
|
||||
{
|
||||
baseWavelength = math.max(baseWavelength, 1.0e-6f);
|
||||
sampleSpacing = math.max(sampleSpacing, 0.0f);
|
||||
lacunarity = math.max(lacunarity, 1.01f);
|
||||
transitionRatio = math.max(transitionRatio, 1.01f);
|
||||
maximumOctaves = math.min(math.max(maximumOctaves, 0), 16);
|
||||
var nyquistWavelength = sampleSpacing * 2.0f;
|
||||
var wavelength = baseWavelength;
|
||||
var active = 0;
|
||||
var finalWeight = 0.0f;
|
||||
var finest = 0.0f;
|
||||
|
||||
for (var octave = 0; octave < maximumOctaves; octave++)
|
||||
{
|
||||
var weight = nyquistWavelength <= 0.0f
|
||||
? 1.0f
|
||||
: math.smoothstep(nyquistWavelength, nyquistWavelength * transitionRatio, wavelength);
|
||||
if (weight <= 0.0f) break;
|
||||
active++;
|
||||
finalWeight = weight;
|
||||
finest = wavelength;
|
||||
wavelength /= lacunarity;
|
||||
}
|
||||
|
||||
var omittedAmplitude = 0.0f;
|
||||
var amplitude = math.abs(baseAmplitude);
|
||||
for (var octave = 0; octave < maximumOctaves; octave++)
|
||||
{
|
||||
if (octave >= active) omittedAmplitude += amplitude;
|
||||
else if (octave == active - 1) omittedAmplitude += amplitude * (1.0f - finalWeight);
|
||||
amplitude *= math.abs(gain);
|
||||
}
|
||||
|
||||
return new ErosionBandSelection(active, active > 0 ? finalWeight : 0.0f, finest, omittedAmplitude);
|
||||
}
|
||||
}
|
||||
34
tests/CultMath.Tests/ErosionFrequencyBandsTests.cs
Normal file
34
tests/CultMath.Tests/ErosionFrequencyBandsTests.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using Xunit;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
public sealed class ErosionFrequencyBandsTests
|
||||
{
|
||||
[Fact]
|
||||
public void RadiusIndependentWorldWavelengthSelectsSameBands()
|
||||
{
|
||||
var small = ErosionFrequencyBands.Select(1_000.0f, 100.0f, 8, 2.0f, 20.0f, 0.5f);
|
||||
var large = ErosionFrequencyBands.Select(1_000.0f, 100.0f, 8, 2.0f, 20.0f, 0.5f);
|
||||
Assert.Equal(small, large);
|
||||
Assert.Equal(3, small.ActiveOctaves);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransitionPartiallyWeightsLastResolvableOctave()
|
||||
{
|
||||
var selection = ErosionFrequencyBands.Select(1_000.0f, 50.0f, 8, 2.0f, 1.0f, 0.5f);
|
||||
Assert.Equal(4, selection.ActiveOctaves);
|
||||
Assert.InRange(selection.FinalOctaveWeight, 0.0f, 1.0f);
|
||||
Assert.True(selection.UnresolvedHeightBound > 0.0f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BandedFilterPreservesLowOctaveResultExactly()
|
||||
{
|
||||
var p = AdvancedErosionParameters.Default;
|
||||
var band = new ErosionBandSelection(2, 1.0f, 0.0f, 0.0f);
|
||||
var banded = AdvancedErosionFilter.Sample(new float2(0.42f, 0.31f), new float3(0.5f, 0.1f, -0.2f), 0.0f, p, band);
|
||||
var truncated = AdvancedErosionFilter.Sample(new float2(0.42f, 0.31f), new float3(0.5f, 0.1f, -0.2f), 0.0f, p with { Octaves = 2 });
|
||||
Assert.Equal(truncated, banded);
|
||||
}
|
||||
}
|
||||
Reference in a new issue