diff --git a/shaders/AdvancedErosionFilter.hlsl b/shaders/AdvancedErosionFilter.hlsl index 38c3ef1..a0a350e 100644 --- a/shaders/AdvancedErosionFilter.hlsl +++ b/shaders/AdvancedErosionFilter.hlsl @@ -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=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 diff --git a/src/CultMath/AdvancedErosionFilter.cs b/src/CultMath/AdvancedErosionFilter.cs index 6780223..65db367 100644 --- a/src/CultMath/AdvancedErosionFilter.cs +++ b/src/CultMath/AdvancedErosionFilter.cs @@ -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; diff --git a/src/CultMath/ErosionFrequencyBands.cs b/src/CultMath/ErosionFrequencyBands.cs new file mode 100644 index 0000000..3c63eb6 --- /dev/null +++ b/src/CultMath/ErosionFrequencyBands.cs @@ -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); + } +} diff --git a/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs b/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs new file mode 100644 index 0000000..f57213a --- /dev/null +++ b/tests/CultMath.Tests/ErosionFrequencyBandsTests.cs @@ -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); + } +}