14 多层 Noise 与常见效果
前置知识:第 12 章的 value noise、第 13 章的输出范围与算法选择,以及第 11 章的反向坐标查询。
本章示例:多层 Noise 对比、云、石纹、地形与水纹;完整源码、效果源码。
学完本章能做什么
你将能把单层连续 noise 叠加成不同尺度的细节,区分 fBm、湍流和脊线组合;用二维 noise 改变另一个 noise 的查询位置;再把同一个标量场映射成云层、石纹、地形配色和流动水纹。
先看现象
第一个示例使用相同的底层 valueNoise(),可切换单层、fBm、湍流、脊线与域扭曲。增大层数后,大片起伏中出现细节;提高高频层的权重后,画面更碎。第二个示例让同一套函数产生四种画面。改变“域扭曲强度”时,形状弯曲;改变“层数”时,细节数量变化。
这里的 noise 只在片元阶段决定颜色。平面仍是 PlaneGeometry(3, 2, 1, 1),没有顶点位移,也没有被重新定义的法线。真实起伏表面的法线放在第 15~16 章讲。
1. 为什么叠加多个尺度
单层 value noise 的每个单元大小相近,图案容易显得软而规则。把同一个函数放到不同尺度的坐标上采样:
// 片元阶段;p 是按目标平面比例修正后的二维 noise 坐标。
float large = valueNoise( p );
float medium = valueNoise( p * 2.0 );
float small = valueNoise( p * 4.0 );
float combined = ( large + 0.5 * medium + 0.25 * small ) / 1.75;
p * 2.0 使单位长度内的变化次数加倍;并不是把颜色值乘 2。分母 1.75 = 1 + 0.5 + 0.25,让不同层数的输出仍落在底层 noise 的 0~1 范围内。大尺度负责形状,小尺度负责边缘和质感。多层不等于“更随机”:每层仍是确定性函数。
2. fBm:频率、振幅和归一化
常见的分形布朗运动(fBm)写成有限层求和:
fBm(p) = Σ [aᵢ × noise(fᵢ × p)] / Σ aᵢ
fᵢ = lacunarityⁱ,aᵢ = gainⁱ,i = 0 … octaves-1
octaves 是层数;lacunarity 是相邻层频率的倍率,常见起点为 2.0;gain 是相邻层振幅的倍率,常见起点为 0.5。它们是参数选择,并非固定规律。示例先用第 12 章的 valueNoise,因此每层值在 [0,1)。gain > 0 且除以正的振幅总和后,fBm 仍在 [0,1);但统计分布会随层数和参数改变,不能认为阈值 0.5 总会覆盖半幅画面。
// 片元阶段;p 的单位为 noise 格点。最大编译层数为 6。
float fbm( vec2 p ) {
float sum = 0.0;
float weight = 0.0;
float amplitude = 1.0;
for ( int i = 0; i < 6; i++ ) {
if ( i < uOctaves ) {
sum += amplitude * valueNoise( p );
weight += amplitude;
p *= uLacunarity;
amplitude *= uGain;
}
}
return sum / weight; // 控件保证 uOctaves 至少为 1。
}
提高 uOctaves 增加最高频率;提高 uLacunarity 拉开相邻层尺度;提高 uGain 让细节层更显眼。若增加层数时忘记更新分母,整体会变亮,阈值区域也会改变;这时你看到的不只是“更多细节”。对比示例的连续颜色模式在映射到颜色前用 smoothstep(0.38, 0.72, level) 拉开视觉对比;阈值模式直接比较原始 level,并没有改变 fBm 函数的返回值。
本章示例输入坐标先用目标平面宽高比 1.5 修正,再乘 uScale。uScale 表示每单位平面高度的基础格数。画布窗口比例不参与这个计算。示例的浮点 hash、精度局限与第 12 章相同;在很大的坐标上不能保证相邻格点仍被准确区分,跨设备也不保证逐位一致。
3. 湍流与脊线:先改变每层的值,再累加
假设每层 n = valueNoise(p) 在 [0,1)。先居中并折叠:
// 片元阶段;应用于每一层的样本,不是在最终 fBm 后只做一次。
float turbulenceSample = abs( 2.0 * n - 1.0 );
float ridgeSample = 1.0 - abs( 2.0 * n - 1.0 );
湍流在原始值接近 0 或 1 的地方高,在 0.5 处低;脊线正好相反,沿原始值约为 0.5 的等值线变亮。再把每层变换后的样本按振幅累加并归一化,就得到多尺度湍流与脊线。若只在最终 fBm 上做 abs(2.0 * fbm(p) - 1.0),所有层先互相抵消,再折叠;图案与“每层先折叠”的结果不同。
这两个变换把每层结果留在 [0,1],但在 n = 0.5 处有尖点:值连续,导数通常不连续。这对视觉纹理常常可接受;将来若从高度场求表面法线,尖点附近的法线可能出现突然变化,需按目标效果处理。
4. 域扭曲:改变查询位置
第 11 章改变纹理的 sampleUv,从原图的另一处取色。域扭曲做同样的事,只是被查询的对象换成了 noise 函数。先生成二维偏移场 q(p),再计算 fbm(p + strength × q(p)):
// 片元阶段;p 与 q 的位移都用 noise 格点单位。
vec2 q = vec2(
valueNoise( p * 0.7 + vec2( 13.1, 7.7 ) ),
valueNoise( p * 0.7 + vec2( -5.3, 11.9 ) )
) - 0.5;
float warped = fbm( p + uWarp * q );
两个固定偏移让 x、y 分量采到不同区域;它们只是去相关的经验数值,没有神秘的物理意义。减去 0.5 把偏移从 [0,1)² 移到 [-0.5,0.5)²,避免整体总往右上方偏。uWarp = 0 时恢复原始 fBm;增大强度会弯曲等值线,太大则可能把局部细节挤在一起。
输入变换与输出加法不同。 fbm(p) + valueNoise(p) 改变的是高度;fbm(p + q) 改变的是“在哪里查询高度”。多个扭曲阶段还能继续嵌套,但每一层调用都会增加片元计算量。示例只用两次单层 value noise 生成 q,再计算一次 fBm,便于观察成本。
5. 用同一标量场做常见效果
第二个示例把上述标量场 h = fbm(warpedP) 当作控制信号,而不是直接当 RGB 颜色。四个模式主要改变的是后续映射:
| 效果 | 从标量场到画面 | 关键可调量 |
|---|---|---|
| 云层 | 用 smoothstep 把 h 变成云遮罩,再 mix 天空与云色 |
阈值区间、基础尺度、层数 |
| 石纹 | 用扭曲后的坐标驱动正弦条纹 | 条纹频率、域扭曲强度 |
| 地形配色 | 按 h 的不同高度段混合海、沙、植被和岩石色 |
色带阈值、层数 |
| 流动水纹 | 平移 noise 查询位置,再用正弦和高亮遮罩画水纹 | 平移速度、波纹频率 |
云层:阈值决定覆盖面积
// 片元阶段;h 是 [0,1) 的归一化 fBm 值。
float cloud = smoothstep( 0.43, 0.62, h );
vec3 color = mix( skyColor, cloudColor, cloud );
两个阈值之间是羽化区。把 0.43 和 0.62 都提高,云通常会缩小;把两者拉开,边缘更软。颜色在线性 RGB 中混合,示例最后用 #include <colorspace_fragment> 输出。单层 noise 也能产生云块,多层主要增加尺度层次。
石纹:连续场弯曲周期条纹
// 片元阶段;warpedP 是扭曲后的二维 noise 坐标。
float bands = 0.5 + 0.5 * sin( 6.28318530718 *
( warpedP.x * 1.6 + 0.7 * h ) );
vec3 color = mix( darkStone, lightStone, bands );
若只用 sin(6.28 * p.x * 1.6),结果是笔直的竖条;warpedP 和 h 使条纹弯曲。bands 在 [0,1] 内,可再次用 smoothstep 提高黑白纹的对比。条纹太细时会出现像素锯齿,增加网格分段并不能解决片元采样不足。
地形:高度色带不是几何高度
将 h 的低值映射成海,高值逐渐过渡到沙、绿地与岩石。smoothstep 使边界可控,但这些颜色没有移动顶点;从侧面看平面仍是平面。若以后把 h 用作顶点位移,需要足够分段,并依据新高度场重算法线,这正是第 15 章的主题。
水纹:移动二维查询坐标
示例用 flowP = p + velocity * uTime 查询固定二维场,让纹理随时间移动;正弦条纹与 fBm 一起决定亮暗。它是平移的二维图案,不是三维 noise 随时间演化,也不代表水面几何波浪。第 10 章讲过真实顶点波浪;第 16 章会讲片元中根据细节高度重新定义用于光照的法线。
6. 细节、采样与成本
示例固定最多六层。第 i 层大约有 uScale × uLacunarityⁱ 个基础单元跨过一单位平面高度。若一个单元在屏幕上只有一两个像素,高频层可能闪烁或形成错误花纹;继续加层数不会真正增加可见细节。改善方法包括减少层数或频率、使用导数感知的过滤、预计算纹理与适当 mipmap。普通程序化 noise 不会因为 Three.js 自动获得纹理 mipmap。
计算量大致随层数增长。域扭曲示例额外采样两次底层 noise;效果示例中如果又调用第二个 fBm,成本会进一步增加。gain 小时,高层对画面的贡献也小,可以先删掉看不见的层。这里在片元中求颜色,网格仍只有 1 × 1 分段;如果把同一函数搬到顶点阶段,增加网格分段才会增加采样点,同时法线需要跟着更新。
常见问题
- **层数一增加画面就变亮:**忘记除以振幅总和,或者把归一化与颜色增益混在一起。
- **提高
lacunarity后出现闪烁或摩尔纹:**高频细节超过屏幕采样能力,减少层数、频率或做过滤。 - **湍流和脊线看起来一样:**检查
1.0 - abs(...)是否只用于脊线;确认变换发生在每层样本上。 - **域扭曲整体往一个方向漂:**偏移场没有居中,或两个分量使用了完全相同的采样坐标。
- **改变画布宽高后噪声被拉长:**平面宽高比与画布比例混用;本章按平面
3:2修正二维坐标。 - **水纹停住或跳帧:**检查
uTime是否每帧更新;大幅改变时间值会直接跳到固定场的另一处。 - **期待地形受光照起伏:**本章只按高度值涂颜色。真实位移、几何法线与细节法线将在后续章节单独处理。
本章小结
fBm 用频率递增、振幅递减的多层 noise 构造细节;湍流与脊线在每层采样后折叠数值;域扭曲先改变查询坐标。云、石纹、地形色带和水纹是在这些标量场上使用不同的颜色、阈值与动画映射。参数越多越要留意输出范围、屏幕采样和片元成本。