Add physical erosion frequency bands

This commit is contained in:
Meta Vens 2026-07-12 20:02:45 +01:00
commit ddeac557cb
4 changed files with 136 additions and 11 deletions

View file

@ -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

View file

@ -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;

View 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);
}
}

View 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);
}
}