基于 FBM 的地形片段着色器:噪声合成、域变换与阈值/调色映射的工程化实践
------以 fbm_terrain.frag
为蓝本的架构化技术解析

摘要
本文系统梳理基于 FBM(Fractal Brownian Motion) 的地形片段着色器实现路径:从基础噪声到多八度合成、从域变换(Domain Warping)到阈值/调色映射(Palette/Threshold Mapping),并给出抗走样策略、性能预算与可复现实验。目标是在移动端/桌面端 GPU 上以常数级每像素成本 获得平滑、层次丰富且参数可调的地形底图,可作为 2D/2.5D 游戏与可视化项目的可复用模块。
关键词:FBM、Perlin/Value Noise、Domain Warping、fwidth 抗走样、阈值分类、地形调色
1. 引言
在以程序化方式生成地形时,连续噪声场 是核心。单一频段噪声往往"纹样过宽或过细",真实地貌需要跨尺度的层次 。FBM 通过叠加多八度噪声(低频提供骨架,高频负责纹理)达到"既有宏观轮廓、又有细节起伏"的目标。渲染端若希望"一帧内实时生成",最务实的落地是把 FBM 与着色器调色/阈值直接绑定在片段着色器中。
2. 模型与公式
2.1 FBM 定义
设基础噪声函数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n ( p ) ∈ [ − 1 , 1 ] n(\mathbf{p})\in[-1,1] </math>n(p)∈[−1,1],则 FBM:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> F B M ( p ) = ∑ i = 0 N − 1 g i ⋅ n ( f ⋅ λ i p ) \mathrm{FBM}(\mathbf{p})=\sum_{i=0}^{N-1} g^i \cdot n\!\left(f\cdot \lambda^i \mathbf{p}\right) </math>FBM(p)=i=0∑N−1gi⋅n(f⋅λip)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N:八度数(octaves)
- <math xmlns="http://www.w3.org/1998/Math/MathML"> g g </math>g:增益(gain/persistence),控制幅值递减
- <math xmlns="http://www.w3.org/1998/Math/MathML"> λ \lambda </math>λ:lacunarity,控制频率递增
- <math xmlns="http://www.w3.org/1998/Math/MathML"> f f </math>f:基频(base frequency)
为避免整体增益随 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 增大而飘移,常将权重归一化:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> w ^ i = g i ∑ k = 0 N − 1 g k , F B M ^ ( p ) = ∑ i = 0 N − 1 w ^ i ⋅ n ( f ⋅ λ i p ) \hat{w}i=\frac{g^i}{\sum{k=0}^{N-1} g^k},\quad \widehat{\mathrm{FBM}}(\mathbf{p})=\sum_{i=0}^{N-1}\hat{w}_i\cdot n\!\left(f\cdot\lambda^i \mathbf{p}\right) </math>w^i=∑k=0N−1gkgi,FBM (p)=i=0∑N−1w^i⋅n(f⋅λip)
2.2 基础噪声选型
- Perlin/Simplex:梯度噪声,连续且可微,频谱友好;
- Value Noise:插值值噪声,成本更低但需插值抗格点印记;
- 改良变体 :如将
hash
替代纹理采样,减少依赖。
3. 实现结构(GLSL 片段示例)
代码段为可移植骨架 ,结构与职责与
fbm_terrain.frag
同类:可直接嵌入项目并用你的uniform/#define
替换参数。
glsl
// ====== Uniforms(按项目实际替换)======
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_worldOffset; // 世界坐标偏移(相机/逻辑原点)
uniform float u_baseFreq; // f
uniform float u_lacunarity; // λ
uniform float u_gain; // g
uniform int u_octaves; // N
uniform vec4 u_thresholds; // 阈值分段 t0<t1<t2<t3
uniform vec3 u_colorA, u_colorB, u_colorC, u_colorD, u_colorE;
// ====== hash / noise(示例替换为你的实现)======
float hash(vec2 p){ // 低相关性哈希
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 78.233);
return fract(p.x * p.y);
}
float valueNoise(vec2 p){
vec2 ip = floor(p);
vec2 fp = fract(p);
float v00 = hash(ip + vec2(0.0, 0.0));
float v10 = hash(ip + vec2(1.0, 0.0));
float v01 = hash(ip + vec2(0.0, 1.0));
float v11 = hash(ip + vec2(1.0, 1.0));
vec2 u = fp*fp*(3.0-2.0*fp); // smoothstep
return mix(mix(v00, v10, u.x), mix(v01, v11, u.x), u.y) * 2.0 - 1.0;
}
// ====== FBM(多八度叠加,可选归一化)======
float fbm(vec2 p, float baseFreq, float lac, float gain, int octaves){
float amp = 1.0, freq = baseFreq, sum = 0.0, norm = 0.0;
for(int i=0; i<16; ++i){ // 上限保护
if(i>=octaves) break;
sum += amp * valueNoise(p * freq);
norm += amp;
amp *= gain;
freq *= lac;
}
return sum / max(norm, 1e-5); // 归一化
}
// ====== Domain Warping(可选,提高丰富度)======
vec2 warp(vec2 p, float baseFreq){
float w1 = fbm(p + vec2(0.0, 5.2), baseFreq*0.5, 2.0, 0.5, 4);
float w2 = fbm(p + vec2(3.7, 0.0), baseFreq*0.5, 2.0, 0.5, 4);
return p + vec2(w1, w2) * 2.0; // 扰动强度按需调节
}
// ====== 阈值 + 调色(平滑过渡 & fwidth 抗走样)======
vec3 palette(float h){
// h∈[-1,1] → [0,1]
float t = 0.5 * (h + 1.0);
// 分段阈值
float t0 = u_thresholds.x;
float t1 = u_thresholds.y;
float t2 = u_thresholds.z;
float t3 = u_thresholds.w;
// 基于 fwidth 的平滑阶梯,减少走样
float w = fwidth(t) * 1.5; // 带宽,可调
float s0 = smoothstep(t0-w, t0+w, t);
float s1 = smoothstep(t1-w, t1+w, t);
float s2 = smoothstep(t2-w, t2+w, t);
float s3 = smoothstep(t3-w, t3+w, t);
// 五段:海、沙、草、林、岩(示例)
vec3 cA = u_colorA; // 海
vec3 cB = u_colorB; // 沙
vec3 cC = u_colorC; // 草
vec3 cD = u_colorD; // 林
vec3 cE = u_colorE; // 岩
// 分段线性插值(mix/mad 形式),sX 由0→1推动段间过渡
vec3 c = mix(cA, cB, s0);
c = mix(c, cC, s1);
c = mix(c, cD, s2);
c = mix(c, cE, s3);
return c;
}
void main(){
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
// 保持等比 & 世界坐标映射
vec2 p = (uv - 0.5) * vec2(u_resolution.x/u_resolution.y, 1.0);
p += u_worldOffset; // 世界偏移(相机/逻辑坐标)
// 可选域变换
vec2 q = warp(p, u_baseFreq);
// 计算 FBM 高度场
float h = fbm(q, u_baseFreq, u_lacunarity, u_gain, u_octaves);
// 调色(阈值平滑)
vec3 col = palette(h);
// (可选)根据坡度调暗/高光:近似梯度
vec2 g = vec2(dFdx(h), dFdy(h));
float slope = clamp(length(g) * 2.0, 0.0, 1.0);
col *= mix(1.05, 0.85, slope); // 坡越大越暗
gl_FragColor = vec4(col, 1.0);
}
4. 关键工程要点
面向片段着色器(GLSL / WGSL)在移动端与桌面 GPU 的可维护实现清单。以下要点均可直接落入
fbm_terrain.frag
同类工程。
4.1 坐标与尺度(World → Shader)
- 等比映射 :
vec2 p = (uv - 0.5) * vec2(res.x/res.y, 1.0);
,避免拉伸。 - 世界位移 :
p += u_worldOffset;
与相机/逻辑原点对齐,支持大地图滑动。 - 旋转/扰动 :对
p
先做 2×2 旋转或微扰,弱化轴向伪影与周期纹样。 - 尺度分层 :
u_baseFreq
决定大陆尺度;u_lacunarity≈2.0
决定频谱增长;u_gain∈[0.45,0.65]
控制细节能量衰减。
4.2 FBM 合成(稳定与可控)
-
幅值归一化 :防止随八度数 <math xmlns="http://www.w3.org/1998/Math/MathML"> N N </math>N 增长整体偏亮/偏暗。
glslsum += amp * noise(p * freq); norm += amp; // ... return sum / max(norm, 1e-5);
-
上限保护 :循环
for (i=0; i<MIN(N,16))
,硬件常量限制下避免未定义行为。 -
可编译展开 :若
N
固定,宏展开能减少分支开销:glsl#if OCTAVES > 0 sum += amp*noise(p*freq); freq*=lac; amp*=gain; #endif /* 继续展开... */
4.3 域变换(Domain Warping)
-
目标:打破同构纹样,提高层次感。
-
实践 :用低频 FBM 生成位移向量,强度建议 1.5--3.0。
glslvec2 q = p; q += vec2( fbm(p + vec2(0.0,5.2), f*0.5,2.0,0.5,4), fbm(p + vec2(3.7,0.0), f*0.5,2.0,0.5,4) ) * 2.0;
-
节流:远景或低端机关闭 warp 或减少其八度数。
4.4 阈值映射(连续 → 语义带)
-
软阈值 :基于
fwidth
自适应的平滑阶梯,减少走样与闪烁。glslfloat t = 0.5*(h+1.0); // [-1,1]→[0,1] float w = fwidth(t) * 1.5; // 带宽 float s0 = smoothstep(t0-w, t0+w, t); // 海→沙 float s1 = smoothstep(t1-w, t1+w, t); // 沙→草 float s2 = smoothstep(t2-w, t2+w, t); // 草→林 float s3 = smoothstep(t3-w, t3+w, t); // 林→岩
-
分段掩码 :如需明确"所属带",用
step
/smoothstep
组合出五段 one-hot 或 soft-mask。
4.5 调色(Palette)与坡度调制
-
分段线性插值 :连贯过渡(避免断层):
glslvec3 col = mix(cA, cB, s0); col = mix(col, cC, s1); col = mix(col, cD, s2); col = mix(col, cE, s3);
-
坡度/光照 :用屏幕导数近似法线,做简易阴影/高光:
glslvec2 g = vec2(dFdx(h), dFdy(h)); float slope = clamp(length(g)*2.0, 0.0, 1.0); col *= mix(1.05, 0.85, slope); // 坡越大越暗
4.6 抗走样(AA)与数值健壮性
- AA :所有边界过渡用
smoothstep(ti-w, ti+w, t)
;减少"高对比细纹"锯齿。 - 精度 :移动端优先
mediump
颜色、highp
坐标;避免NaN/Inf
(除法加 <math xmlns="http://www.w3.org/1998/Math/MathML"> ε \varepsilon </math>ε,clamp
输出)。 - 分支控制 :用向量化运算、
mix/step
替代if
减少 warp/divergence。
4.7 LOD 与性能预算
-
LOD :远处降低
u_octaves
或提高u_gain
;远处关闭 warp。glslfloat lod = clamp(log2(length(dFdx(p))+length(dFdy(p)))+1.0, 0.0, 1.0); int oct = int(mix(float(u_octaves), 3.0, lod)); // 距离越远八度越少
-
常量折叠 :阈值/颜色表做
uniform
,运行时设置,避免重编译。 -
成本估算 :主 FBM N≈6 + warp 2×4 ⇒ ~14 次基础噪声采样 + 若干
mix/smoothstep
;1080p@60fps 需配合 LOD。
4.8 可复用接口与调参
- 统一参数 :
u_baseFreq,u_lacunarity,u_gain,u_octaves,u_thresholds,u_worldOffset
。 - 在线扫参:在 UI 里动态调阈值与增益,统计各带面积占比,选"既好看又可玩"的点。
- 可测试性:提供"固定 seed 截图"与"随机 seed 连续序列",便于回归比对。
小结:稳(归一化+AA) 、准(阈值/调色有语义) 、快(LOD/少分支) 是 FBM 地形片段的三要点。将上述要点模板化,可在不同风格地图间快速复用与迁移。