Flame游戏开发——噪声合成、域变换与阈值/调色映射的工程化实践(2)

基于 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 增长整体偏亮/偏暗。

    glsl 复制代码
    sum  += 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。

    glsl 复制代码
    vec2 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 自适应的平滑阶梯,减少走样与闪烁。

    glsl 复制代码
    float 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)与坡度调制

  • 分段线性插值 :连贯过渡(避免断层):

    glsl 复制代码
    vec3 col = mix(cA, cB, s0);
    col = mix(col, cC, s1);
    col = mix(col, cD, s2);
    col = mix(col, cE, s3);
  • 坡度/光照 :用屏幕导数近似法线,做简易阴影/高光:

    glsl 复制代码
    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); // 坡越大越暗

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。

    glsl 复制代码
    float 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 地形片段的三要点。将上述要点模板化,可在不同风格地图间快速复用与迁移。


github.com/tao-999/xiu...

相关推荐
星斗大森林2 小时前
flame游戏开发——地图拖拽与轻点判定(3)
前端
samonyu2 小时前
fnm 简介及使用
前端·node.js
bug_kada2 小时前
玩转Flex布局:看完这篇你也是布局高手!
前端
前端小巷子2 小时前
JS打造“九宫格抽奖”
前端·javascript·面试
潘小安2 小时前
『译』资深前端开发者如何看待React架构
前端·react.js·面试
李昊哲小课3 小时前
HTML 完整教程与实践
前端·html
GISer_Jing3 小时前
React 18的createRoot与render全面对比
前端·react.js·前端框架
我叫汪枫3 小时前
React Hooks原理深度解析与高级应用模式
前端·react.js·前端框架
我叫汪枫3 小时前
深入探索React渲染原理与性能优化策略
前端·react.js·性能优化