Better Terrain Noise

A better noise to make advanced terrain erosion filter based on layered smoothing of gullies, featuring controls for erosion intensity, detail, ridge and groove rounding, and capable of outputting ridge maps. It’s suitable for uses like water flow simulation.

 

For more information about this technology, see:

https://www.youtube.com/watch?v=gsJHzBTPG0Y

https://blog.runevision.com/2026/03/fast-and-gorgeous-erosion-filter.html

Shader code
/*
=====================================================================================
基于叠加淡化沟壑的高级地形侵蚀滤镜,
包含侵蚀强度、细节、山脊和沟槽圆角控制,
并可输出脊线图,适用于例如水流模拟等用途。

有关该技术的更多信息,请参阅:
https://www.youtube.com/watch?v=gsJHzBTPG0Y
https://blog.runevision.com/2026/03/fast-and-gorgeous-erosion-filter.html
本着色器包含三个部分:

 - Phacelle 噪声函数(供侵蚀函数使用)
 - 侵蚀函数

关于侵蚀参数的说明,请参见演示部分。

此侵蚀技术最初衍生自 Clay John 的版本(https://www.shadertoy.com/view/MtGcWh)
和 Fewes 的版本(https://www.shadertoy.com/view/7ljcRW),
以及 Runevision 的清理改进版(https://www.shadertoy.com/view/33cXW8),
但就目前而言,除了高层概念外,它与这些早期版本已几乎没有什么共同之处。

原始着色器代码所在网址:
https://www.shadertoy.com/view/wXcfWn
原始着色器代码带有交互式高度图的变体“鼠标绘制侵蚀山脉”:
https://www.shadertoy.com/view/sf23W1
=====================================================================================
*/

shader_type canvas_item;

#define clamp01(x) clamp(x, 0.0, 1.0)

// -----------------------------------------------------------------------------
// PHACELLE 噪声函数
// -----------------------------------------------------------------------------
vec2 hash(in vec2 x) {
    const vec2 k = vec2(0.3183099, 0.3678794);
    x = x * k + k.yx;
    return -1.0 + 2.0 * fract(16.0 * k * fract(x.x * x.y * (x.x + x.y)));
}

// 简易 Phacelle 噪声函数,生成与输入方向对齐的条纹图案。
// Phacelle 这个名称是“相位”(phase)与“单元格”(cell)的合成词,
// 因为该函数通过对多个单元格的余弦波和正弦波进行插值来产生相位。
// 
// 参数说明:
//  - p            :当前正在求值的输入点。
//  - normDir      :该点处条纹的方向,必须为单位向量。
//  - freq         :每个单元格内条纹的频率。建议保持在 1.0 附近,因为过高的值会导致扭曲和其他伪影。
//  - offset       :条纹的相位偏移,其中 1.0 表示一个完整周期。
//  - normalization:归一化程度,取值在 0 到 1 之间。例如,设为 0.4 时,
//                   原始输出中幅度低于 0.6 的部分将不会被完全归一化到幅度 1.0。
//
// Phacelle 噪声函数版权所有 (c) 2025 Rune Skovbo Johansen
// 本源代码采用 Mozilla 公共许可证 2.0 版授权。
// 如果未随此文件提供许可证副本,可访问 https://mozilla.org/MPL/2.0/ 获取。
vec4 PhacelleNoise(in vec2 p, vec2 normDir, float freq, float offset, float normalization) {
	// 得到一个与输入方向正交的向量,其大小与条纹的频率成比例。
	vec2 sideDir = normDir.yx * vec2(-1.0, 1.0) * freq * TAU;
	offset *= TAU;

	// 遍历 4x4 的单元格,为每个单元格计算条纹图案并在它们之间混合。
	// pInt 是当前坐标 p 的整数部分,pFrac 是余数。
	//
	// o   o   o   o
	//
	// o   o   o   o
	//       p
	// o   i   o   o
	//
	// o   o   o   o
	//
	// p: 当前坐标    i: p 的整数坐标    o: 4x4 网格的网点
	//
	vec2 pInt = floor(p);
	vec2 pFrac = fract(p);
	vec2 phaseDir = vec2(0.0);
	float weightSum = 0.0;
	for (int i = -1; i <= 2; i++) {
		for (int j = -1; j <= 2; j++) {
			vec2 gridOffset = vec2(float(i), float(j));

			// 通过从整数网格中的一个点开始来计算一个单元格的点。
			vec2 gridPoint = pInt + gridOffset;

			// 为每个轴上的单元格点计算一个在 -0.5 到 0.5 之间的随机偏移。
			vec2 randomOffset = hash(gridPoint) * 0.5;

			// 最终的单元点(我们不存储它)是 gridPoint + randomOffset。
			// 计算一个表示输入点相对于这个单元点的向量:
			// p - (gridPoint + randomOffset)
			// = (pFrac + pInt) - ((pInt + gridOffset) + randomOffset)
			// = pFrac + pInt - pInt - gridOffset - randomOffset
			// = pFrac - gridOffset - randomOffset
			vec2 vectorFromCellPoint = pFrac - gridOffset - randomOffset;

			// 钟形权重函数,在距离 0 时为 1,在距离 1.5 时接近 0。
			// 由于最多0.5的随机偏移,4x4网格外的单元点到当前点p的最近距离是1.5个单位。
			float sqrDist = dot(vectorFromCellPoint, vectorFromCellPoint);
			float weight = exp(-sqrDist * 2.0);
			// 减去 0.01111,让函数在距离 1.5 时实际上为 0,这样可以避免一些(非常微妙的)网格线伪影。
			weight = max(0.0, weight - 0.01111);

			// 记录所有权重的总和。
			weightSum += weight;

			// waveInput 是一个沿 sideDir 方向数值增加的梯度。
			// 它的变化率是 freq 乘以 TAU,因为在 sideDir 上预先应用了这个乘数。
			float waveInput = dot(vectorFromCellPoint, sideDir) + offset;

			// 把这个单元格的余弦和正弦波贡献加到插值值上。
			phaseDir += vec2(cos(waveInput), sin(waveInput)) * weight;
		}
	}

	// 获取 interpolated 的原始值。
	vec2 interpolated = phaseDir / weightSum;
	// 把这个值理解为一个向量,它的长度表示两种波的大小。
	float magnitude = sqrt(dot(interpolated, interpolated));
	// 因为我们将对其进行完全归一化,需要施加一个较低阈值以显现小幅值(防止被放大为噪点)。
	magnitude = max(1.0 - normalization, magnitude);
	// 返回一个包含归一化余弦和正弦波的向量,以及方向向量,该方向向量可以乘以正弦来得到余弦的导数。
	return vec4(interpolated / magnitude, sideDir);
}

// -----------------------------------------------------------------------------
// 侵蚀函数
// -----------------------------------------------------------------------------

// 首先几个实用函数。

float pow_inv(float t, float power) {
    // 先翻转,再按指定幂次乘方,最后翻转回来。
    return 1.0 - pow(1.0 - clamp01(t), power);
}

float ease_out(float t) {
    // 先翻转(用 1 减 t)。
    float v = 1.0 - clamp01(t);
    // 求二次幂,再翻转回来。
    return 1.0 - v * v;
}

float smooth_start(float t, float smoothing) {
    if (t >= smoothing)
        return t - 0.5 * smoothing;
    return 0.5 * t * t / smoothing;
}

vec2 safe_normalize(vec2 n) {
 	// 用于 normalize 的除零安全替代方法。
    float l = length(n);
	return (abs(l) > 1e-10) ? (n / l) : n;	
}

// 高级地形侵蚀滤镜版权所有 (c) 2025 Rune Skovbo Johansen
// 本源代码采用 Mozilla 公共许可证 2.0 版授权。
// 如果未随此文件提供许可证副本,可访问 https://mozilla.org/MPL/2.0/ 获取。
vec4 ErosionFilter(
    // 逐像素变化的输入参数。
    in vec2 p, vec3 heightAndSlope, float fadeTarget,
    // 可逐像素变化的风格化参数。
    float strength, float gullyWeight, float detail, vec4 rounding, vec4 onset, vec2 assumedSlope,
    // 不支持逐像素变化的缩放相关参数。
    float scale, int octaves, float lacunarity,
    // 其他参数。
    float gain, float cellScale, float normalization,
    // 输出参数。
    out float ridgeMap, out float debug
) {
    strength *= scale;
    fadeTarget = clamp(fadeTarget, -1.0, 1.0);
    
    vec3 inputHeightAndSlope = heightAndSlope;
    float freq = 1.0 / (scale * cellScale);
    float slopeLength = max(length(heightAndSlope.yz), 1e-10);
    float magnitude = 0.0;
    float roundingMult = 1.0;
    
    float roundingForInput = mix(rounding.y, rounding.x, clamp01(fadeTarget + 0.5)) * rounding.z;
    // 组合累积遮罩,首先基于初始斜率,之后也基于每个八度的斜率。
    float combiMask = ease_out(smooth_start(slopeLength * onset.x, roundingForInput * onset.x));

    // 初始化 ridgeMap 的 fadeTarget 和遮罩。
    float ridgeMapCombiMask = ease_out(slopeLength * onset.z);
    float ridgeMapFadeTarget = fadeTarget;
    
    // 确定用于沟壑方向的初始斜率强度,
    // 基于实际斜率与假设斜率的指定混合比例。
    vec2 gullySlope = mix(heightAndSlope.yz, heightAndSlope.yz / slopeLength * assumedSlope.x, assumedSlope.y);
    
    for (int i = 0; i < octaves; i++) {
        // 计算沟壑并累加到高度和斜率上。
        vec4 phacelle = PhacelleNoise(p * freq, safe_normalize(gullySlope), cellScale, 0.25, normalization);
        // 乘以 freq,因为 p 已经乘以了 freq。
        // 取反,因为我们使用指向下方的斜率方向。
        phacelle.zw *= -freq;
        // 斜率大小(0 到 1 的值)。
        float sloping = abs(phacelle.y);
        
        // 将未遮罩的、归一化的斜率添加到 gullySlope,供后续八度使用。
        // 归一化是为了在所有位置都使用正弦波中最陡峭的部分。
        gullySlope += sign(phacelle.y) * phacelle.zw * strength * gullyWeight;
        
        // 处理高度偏移和近似的输出斜率。
        
        // 沟壑的高度偏移(范围 -1 到 1)在 x 分量中,导数在 yz 分量中。
        vec3 gullies = vec3(phacelle.x, phacelle.y * phacelle.zw);
        // 基于 combiMask 将沟壑向 fadeTarget 淡出。
        vec3 fadedGullies = mix(vec3(fadeTarget, 0.0, 0.0), gullies * gullyWeight, combiMask);
        // 根据当前八度的强度应用高度偏移和导数(斜率)。
        heightAndSlope += fadedGullies * strength;
        magnitude += strength;
        
        // 更新 fadeTarget 以包含新的八度。
        fadeTarget = fadedGullies.x;
        
        // 更新遮罩以包含新的八度。
        float roundingForOctave = mix(rounding.y, rounding.x, clamp01(phacelle.x + 0.5)) * roundingMult;
        float newMask = ease_out(smooth_start(sloping * onset.y, roundingForOctave * onset.y));
        combiMask = pow_inv(combiMask, detail) * newMask;
        
        // 更新 ridgeMap 的 fadeTarget 和遮罩。
        ridgeMapFadeTarget = mix(ridgeMapFadeTarget, gullies.x, ridgeMapCombiMask);
        float newRidgeMapMask = ease_out(sloping * onset.w);
        ridgeMapCombiMask = ridgeMapCombiMask * newRidgeMapMask;

        // 准备下一个八度。
        strength *= gain;
        freq *= lacunarity;
        roundingMult *= rounding.w;
    }
    
    ridgeMap = ridgeMapFadeTarget * (1.0 - ridgeMapCombiMask);
    debug = fadeTarget;
    
    vec3 heightAndSlopeDelta = heightAndSlope - inputHeightAndSlope;
    return vec4(heightAndSlopeDelta, magnitude);
}

// ------------------------------------------------------------------------
// 侵蚀参数。
// ------------------------------------------------------------------------

group_uniforms ErosionParameters;

// 侵蚀效果的缩放,同时影响水平和垂直方向。
uniform float EROSION_SCALE = 0.15;

// 侵蚀效果的强度,影响所有八度的幅度,
// 并间接影响沟壑的方向。
uniform float EROSION_STRENGTH = 0.22;

// 沟壑的幅度,作为 0 到 1 的权重值。
// 值为 0 时可以锐化山峰和山谷,但几乎不产生沟壑。
// 值为 1 时产生完整沟壑,但可能会使山峰和山谷变得圆润。
// 调整侵蚀沟壑权重的同时反向调整侵蚀缩放,可用来控制
// 山峰和山谷的锐度,同时保持沟壑幅度基本不变。
uniform float EROSION_GULLY_WEIGHT = 0.5;

// 侵蚀的整体细节。较低的值会将较高频率沟壑的效果限制在更陡的斜坡上。
uniform float EROSION_DETAIL = 1.5;

// 山脊和沟槽的独立圆角控制。
//  x:山脊的圆角。
//  y:沟槽(山谷)的圆角。
//  z:应用于初始高度函数的乘数。
//     例如,如果高度函数的噪声频率比最大的沟壑低 5 倍,
//     则取 0.2 的值可以补偿这一点。
//  w:应用于第一个后续沟壑八度之后的每个八度的乘数。
//     将其设置为与侵蚀分形频率(lacunarity)相同的值,
//     将使得所有八度的圆角效果保持一致。
uniform vec4 EROSION_ROUNDING = vec4(0.1, 0.0, 0.1, 2.0);

// 控制侵蚀效果离开山脊/沟槽多远才开始生效。
//  x:用于初始高度函数的起始阈值。
//  y:用于每个沟壑八度的起始阈值。
//  z:用于初始高度函数的 RidgeMap 专用起始阈值。
//  w:用于每个沟壑八度的 RidgeMap 专用起始阈值。
uniform vec4 EROSION_ONSET = vec4(1.25, 1.25, 2.8, 1.5);

// 控制初始高度函数的假定斜率。
// 实际上,假定斜率可能比使用输入斜率效果更好,
// 因为最终地形可能与输入地形有较大差异。
//  x:用于覆盖实际斜率的假定斜率值。
//  y:覆盖实际斜率的程度(0 到 1)。
uniform vec2 EROSION_ASSUMED_SLOPE = vec2(0.7, 1.0);

// 沟壑基于 Phacelle 噪声中类 Voronoi 单元格内的条纹。
// 单元格缩放参数控制单元格相对于整体侵蚀缩放的大小,
// 同时保持条纹宽度不受影响。
// 接近 1 的值通常会产生良好效果。较小的值产生更颗粒化的沟壑,
// 较大的值产生更长且连绵不断的沟壑,但过大的值会产生
// 不与斜坡对齐的弯曲沟壑。值的改变可能导致输出的突变,
// 尤其是在远离原点的地方,因此此参数不适合动画或被其他函数调制。
uniform float EROSION_CELL_SCALE = 0.7;
// Phacelle 噪声中应用的归一化程度,取值 0 到 1。
// 侵蚀滤镜依赖于 Phacelle 输出幅度的一定一致性。
// 然而,高值可能会产生环形结果,使得山脊和沟槽在某一点汇合,
// 产生不自然的外观。
uniform float EROSION_NORMALIZATION: hint_range(0.0, 1.0, 0.01) = 0.5;

// 控制侵蚀的八度,每个后续八度将更小的沟壑叠加到地形上。
uniform int EROSION_OCTAVES = 5;
// 分形频率(lacunarity)控制每个八度的频率(即水平缩放的反比)
// 相对于上一个八度的倍数。
uniform float EROSION_LACUNARITY = 2.0;
// 增益(gain)控制每个八度的幅度(垂直缩放)
// 相对于上一个八度的倍数。
uniform float EROSION_GAIN = 0.5;

// ------------------------------------------------------------------------
// 侵蚀函数本身不使用的其他地形参数。
// ------------------------------------------------------------------------

group_uniforms NotErosionParameters;

// 控制侵蚀效果是抬高还是降低地形。
//  x:取值范围 -1 到 1 的偏移值,其中 -1 仅降低,1 仅升高。
//     偏移量与侵蚀强度参数成正比,因此如果该参数在整个地形上
//     保持一致,则高度偏移的效果将使整个地形表面以相同量上下移动。
//  y:取值 0 到 1,表示偏移值被取反的侵蚀淡化目标值替代的程度。
//     这会产生仅在谷底抬高、仅在峰顶降低的效果,由于侵蚀滤镜
//     的工作方式,这实际上会很大程度上保留地形的极值点。
uniform vec2 TERRAIN_HEIGHT_OFFSET = vec2(-0.65, 0.0);

// 基础高度噪声参数。

// 地形噪声函数的水平缩放反比(频率)。
uniform float HEIGHT_FREQUENCY = 3.0;
// 地形噪声函数的垂直缩放(幅度)。
uniform float HEIGHT_AMP = 0.125;
// 控制噪声函数八度,每个后续八度将更小的凸起叠加到地形上。
uniform int HEIGHT_OCTAVES = 3;
// 分形频率(lacunarity)控制每个八度的频率(水平缩放反比)
// 相对于上一个八度的倍数。
uniform float HEIGHT_LACUNARITY = 2.0;
// 增益(gain)控制每个八度的幅度(垂直缩放)
// 相对于上一个八度的倍数。
uniform float HEIGHT_GAIN = 0.1;

// -----------------------------------------------------------------------------
// 演示示例
// -----------------------------------------------------------------------------

uniform vec2 pos_offset = vec2(0.0);
uniform bool enable = true;

// 返回梯度噪声(在 x 方向)及其导数(在 yz 方向)。
// 源代码 https://www.shadertoy.com/view/XdXBRH
vec3 noised(in vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);

    vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
    vec2 du = 30.0 * f * f * (f * (f - 2.0) + 1.0); 
    
    vec2 ga = hash(i + vec2(0.0, 0.0));
    vec2 gb = hash(i + vec2(1.0, 0.0));
    vec2 gc = hash(i + vec2(0.0, 1.0));
    vec2 gd = hash(i + vec2(1.0, 1.0));
    
    float va = dot(ga, f - vec2(0.0, 0.0));
    float vb = dot(gb, f - vec2(1.0, 0.0));
    float vc = dot(gc, f - vec2(0.0, 1.0));
    float vd = dot(gd, f - vec2(1.0, 1.0));

    return vec3(va + u.x * (vb - va) + u.y * (vc - va) + u.x * u.y * (va - vb - vc + vd),
        ga + u.x * (gb - ga) + u.y * (gc - ga) + u.x * u.y * (ga - gb - gc + gd) +
        du * (u.yx * (va - vb - vc + vd) + vec2(vb, vc) - va));
}


// 用于生成高度图。
vec3 FractalNoise(vec2 p, float freq, int octaves, float lacunarity, float gain) {
    vec3 n = vec3(0.0);
    float nf = freq;
    float na = 1.0;
    for (int i = 0; i < octaves; i++) {
        n += noised(p * nf) * na * vec3(1.0, nf, nf);
        na *= gain;
        nf *= lacunarity;
    }
    return n;
}


void fragment() {
	// ------------------------------------------------------------------------
	// 高度图实现。
	// ------------------------------------------------------------------------

	float heightFunctionScale = 1.0;
	vec2 pHeight = (UV + pos_offset) / heightFunctionScale;

	// 计算 FBM 地形高度和导数,并存储在 n 中。
	// 高度范围在 [-1, 1] 之间。
	vec3 n = FractalNoise(pHeight, HEIGHT_FREQUENCY, HEIGHT_OCTAVES, HEIGHT_LACUNARITY, HEIGHT_GAIN)
		* HEIGHT_AMP * vec3(heightFunctionScale, 1.0, 1.0);

	// 基于侵蚀前地形的高度定义侵蚀淡化目标值(fade target)。
	// 该目标值应尽量在谷底为 -1,峰顶为 1,但允许超调。
	float fadeTarget = clamp(n.x / (HEIGHT_AMP * 0.6), -1.0, 1.0);
	
	// 将地形高度从 [-1, 1] 范围映射到 [0, 1] 范围。
	n = n * 0.5 + vec3(0.5, 0, 0);

	// 侵蚀结果存储在 h 中(x:高度增量,yz:斜率增量,w:总幅度)。
	// 输出的 ridgeMap 在沟槽处为 -1,在山脊处为 1。
	// 输出的 debug 值可在侵蚀函数内部设置为各种值。
	float ridgeMap, debug;

	vec4 h = ErosionFilter(
		UV + pos_offset, n, fadeTarget,
		EROSION_STRENGTH, EROSION_GULLY_WEIGHT, EROSION_DETAIL,
		EROSION_ROUNDING, EROSION_ONSET, EROSION_ASSUMED_SLOPE,
		EROSION_SCALE, EROSION_OCTAVES, EROSION_LACUNARITY,
		EROSION_GAIN, EROSION_CELL_SCALE, EROSION_NORMALIZATION,
		ridgeMap, debug);
		
	if (!enable) {
		h = vec4(0.0, 0.0, 0.0, 1.0);
		ridgeMap = 1.0;
	}else{
		// 根据高度偏移参数,乘以总幅度来应用偏移。
		float offset = mix(TERRAIN_HEIGHT_OFFSET.x, -fadeTarget, TERRAIN_HEIGHT_OFFSET.y) * h.w;
		float eroded = n.x + h.x + offset;

		// 将四个浮点数打包
		vec3 packed = vec3(
			eroded,
			clamp01(h.x / h.w * 0.5 + 0.5), // 侵蚀增量,映射为 [0, 1] 值。
			clamp01(ridgeMap * 0.5 + 0.5)  // 脊线图,映射为 [0, 1] 值。
			//clamp01(debug * 0.5 + 0.5)      // 调试值。
		);
		COLOR.rgb = packed;
	}
}
Live Preview
Tags
noise, terrain
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Fish_233

Related shaders

guest

0 Comments
Oldest
Newest Most Voted