13 常见 Noise 的结构与选择
前置知识:第 12 章的格点、hash 和插值,以及第 10 章的连续函数。
本章示例:Value、梯度与 Cellular Noise 对比,完整源码。
学完本章能做什么
你将能分辨几类常见 noise 的“随机数据存在哪里”,理解它们各自适合的视觉任务,并在引入现成 GLSL 实现时检查输入、输出、周期、导数和精度约定。
先看现象
示例使用同样的平面与尺度,切换三类图案:value noise 呈现柔和的色块;梯度噪声在格点附近回到中间值,起伏由方向向量决定;cellular noise 显示距离随机特征点的远近,具有明确的“细胞中心”和边界。再切换到阈值显示,可以看到相同阈值不保证相同面积:三种函数的数值分布不同。
1. 先按“存什么、怎么算”区分
| 家族 | 格点或单元里存什么 | 当前位置怎么算 | 常见视觉特点 |
|---|---|---|---|
| Value noise | 一个标量值 | 插值周围值 | 柔和斑块,较容易看到轴向结构 |
| Gradient noise | 一个方向向量 | 各角方向与位移点积,再插值 | 围绕格点起伏,适合云、地形等连续场 |
| Cellular / Worley | 单元内一个或多个特征点 | 到附近特征点的距离,如最近距离 F1 |
细胞、斑点、石纹、裂纹结构 |
这不是三种固定图片,而是三种构造标量场的方法。输入通常是二维或三维位置,输出是一个数。随后仍可以用该数做颜色、阈值、位移或遮罩。第 14 章会把多个尺度叠加;不要把“fBm”误认为第四种格点 hash,它是一种组合办法。
2. Value noise:复用第 12 章
示例的 valueNoise(p) 在四角读取 hash21,用三次平滑权重插值。输入 p 是二维实数坐标;由于它是四个 [0,1) 值的加权平均,输出也在 [0,1)。值与一阶导数在格线上连续。它实现简单,便于把频率、阈值和坐标变换讲清楚。
如果效果中方形方向感太明显,可以换用梯度或 simplex 家族,也可以旋转输入坐标、叠加尺度;后两种做法并不能自动消除所有规律。实际选择应以目标画面和性能测量为准。
3. 梯度噪声:格点存方向,而不是高度
在每个格点生成一个单位向量 g。对当前位置 p,计算它相对于该格点的位移 offset,再取点积:
// 片元阶段;cell 为整数格点,local 为当前单元内的 0~1 位置。
float angle = 6.28318530718 * hash21( cell );
vec2 gradient = vec2( cos( angle ), sin( angle ) );
float contribution = dot( gradient, local );
右下角不能继续用 local,而应使用 local - vec2(1.0, 0.0);其他两角也各自减去对应的角点偏移。四个贡献随后像 value noise 一样做二维插值。示例用五次权重:
// 片元阶段;local 在 [0,1] 内。
vec2 weight = local * local * local *
( local * ( local * 6.0 - 15.0 ) + 10.0 );
五次权重在两端的一阶和二阶导数都为 0。配合共享的格点梯度,本示例的梯度场跨单元边界具有连续的值和一阶导数,二阶导数也连续。我们称它为Perlin 风格的二维梯度噪声:原理与经典 Perlin noise 相近,但此处的 hash、梯度集合和归一化都由教程自己定义,不承诺与某个库的 perlin() 逐值一致。
原始点积可能为负;与 value noise 的 [0,1) 不同,它是以 0 附近为中心的有符号值。精确极值取决于梯度集合和插值细节,不能直接假设原始范围恰为 [-1,1]。示例为了显示,把它暂时映射为 clamp(0.5 + 1.1 * raw, 0.0, 1.0);这是显示映射,不是算法天然的返回范围。用于顶点高度时应明确选择原始值还是映射值。
4. Cellular / Worley:比较到特征点的距离
每个整数单元生成一个随机位置 site。对当前位置,检查附近单元,找到最近的那个点:
// 片元阶段;p 是连续的二维 noise 坐标。
vec2 cell = floor( p );
vec2 local = fract( p );
float nearest = 100.0;
for ( int y = -2; y <= 2; y++ ) {
for ( int x = -2; x <= 2; x++ ) {
vec2 neighbor = vec2( float( x ), float( y ) );
vec2 site = neighbor + featurePoint( cell + neighbor );
nearest = min( nearest, length( site - local ) );
}
}
featurePoint() 返回当前单元内 [0,1)² 的二维位置。示例用同一个 hash21 查询两次,第二次把格点编号加上 (17,53),以取得另一组伪随机值;这两个偏移只是去相关的经验常数。这里搜索 5 × 5 个单元,即 x、y 的编号相对当前单元均在 -2~2。当前单元总有一个点,距离最多为 √2;不在该搜索范围的单元,可能的最近距离至少为 2,因此不会赢过当前单元。使用 3 × 3 邻域通常也能产生可信画面,但在允许特征点落到单元任何位置的设定下,不宜声称它对每个位置都严格最近。
nearest 又叫 F1。它非负,在本设定中不超过 √2;距离场的数值连续,但当最近点的身份改变时,梯度会在细胞边界发生跳变。示例把 F1 映射成亮中心、暗边缘。若再追踪第二近距离 F2,F2 - F1 可突出细胞边界,但搜索与排序也更费计算。
5. 如何选择现成实现
现在常见的名称包括 classic / improved Perlin、Simplex、OpenSimplex2、psrdnoise,以及封装多种算法的 FastNoiseLite。它们的名字不等于统一的 GLSL 接口;CPU 版本不能直接贴进片元着色器。在 Three.js 项目中通常选择可信的 GLSL 移植,或预先生成纹理再由 shader 采样。
| 需求 | 优先查看 | 选型时确认 |
|---|---|---|
| 快速理解并可自己改写 | 本章 value / 梯度实现 | 坐标空间、尺度、输出映射 |
| 连续自然纹理、较少网格轴向痕迹 | Simplex / OpenSimplex2 等实现 | 具体维度、授权、GPU 语言版本、性能 |
| 周期平铺、旋转梯度或解析导数 | 支持这些能力的实现,如 psrdnoise |
周期参数单位、导数所在空间、返回范围 |
| 明显的细胞与裂纹 | Worley / cellular | F1、F2 的定义,邻域搜索范围 |
| 需要多算法快速试样 | FastNoiseLite 等工具 | 使用的是 CPU 库、生成纹理还是匹配的 GPU 实现 |
不同实现可能使用不同的 hash、梯度表、坐标旋转与归一化;都叫“simplex”也不意味着值完全一样。复制第三方函数前,读清源码版本、许可证和调用约定,并保留必要署名。不要为了追求一个“最流行”的名字,跳过画面效果、接口与性能的比较。
把现成 GLSL 函数接入 Three.js
先确认拿到的是着色器源码及它依赖的辅助函数,而不是只有 JavaScript / C++ 版本。把函数声明放在 ShaderMaterial.fragmentShader 的 main() 前,再用当前示例已经构造好的 p 调用它。假设选定的实现提供 float chosenNoise( vec2 p ),并且其文档明确说输出在 [-1,1],可按下面的方式替换本例的 level 计算:
// 片元阶段;p 是比例修正后的二维 noise 坐标。
// chosenNoise 的完整 GLSL 定义应放在 main() 之前;名称与范围以实际实现为准。
float raw = chosenNoise( p );
float level = clamp( raw * 0.5 + 0.5, 0.0, 1.0 );
若该实现已经返回 [0,1],就不要再做一次 * 0.5 + 0.5。如果文档只说“通常接近某范围”,先观察或测量实际值,再决定显示映射;clamp 会截掉超出范围的细节。需要导数或周期时,应调用实现专门提供的版本,并看清导数对应的输入坐标空间。当 p 乘以频率或按比例变换后,导数也必须通过链式法则换算,不能把原始导数直接当成世界空间法线。
6. 输入维度、平铺和动画
- **二维输入
n(vec2(x,y)):**适合平面图案和 UV 遮罩。加vec2(speed * time, 0)只是让固定的二维图案平移。 - **三维输入
n(vec3(x,y,time)):**沿第三维取切片,可以让局部形状随时间演变;要使用真正的三维实现,不能把time随意塞进二维 hash 的一个分量。 - **四维输入:**有些实现用第四维构造循环动画,但必须确认函数支持四维以及周期构造方法。
- **无缝平铺:**普通
fract(p)只重复输入坐标,若 noise 在单元边界两侧值或导数不一致,就会出现接缝。优先选择明确支持周期的实现,或用经过验证的周期坐标映射。
只改颜色时,二维平面仍可用 1 × 1 分段;若把 noise 用于顶点位移,必须有足够顶点采样细节,并重新考虑表面法线。第 15~16 章会分别处理真实位移后的法线和片元细节法线。
7. 精度与性能先看哪里
示例的浮点 hash 和三角函数并不保证所有 GPU、驱动与精度设置逐位相同。尤其是非常大的输入坐标,floor 前的浮点精度可能已经不足以区分相邻格点。若画面必须跨设备完全一致,可以预生成 noise 纹理,或者使用经验证的整数/纹理查表方案。cellular 的 5 × 5 邻域需要多次 hash 与距离计算,通常比单次 value noise 重;高分辨率全屏、移动设备和多层调用时应实测帧时间。这里先建立正确的算法直觉,第 17 章会系统处理跨设备表现和性能。
常见问题
- **三种模式阈值都是
0.5,亮区却不同:**显示范围相同不代表统计分布相同。 - **梯度噪声全黑或全白:**忘记把有符号原始值映射到显示范围,或把映射后的值又错误地减去
0.5。 - **细胞图案有意外直线:**检查是否只搜索当前单元,或特征点在相邻单元的坐标没有加上
neighbor。 - **纹理接缝:**普通 noise 并不自动周期化;
fract输入只会造成强行重复。 - **换了库画面完全变样:**检查函数版本、输入频率、返回范围、种子、维度和颜色映射,不要只比较函数名称。
本章小结
Value noise 插值随机标量,梯度噪声插值方向贡献,cellular noise 比较到特征点的距离。算法选择要同时考虑形状、输入维度、返回范围、连续性、周期能力和 GPU 成本。现成库应按具体实现的约定使用,而不是按名称猜测行为。