2D 柏林噪声的原理
0. 简介
柏林噪声(Perlin Noise) 是一种广泛应用于计算机图形学的伪随机函数,常用于生成自然风格的纹理、地形、云彩等,例如 Minecraft 的世界生成就采用了柏林噪声。
1. 核心思路
柏林噪声的生成流程如下:
- 将平面划分为整数网格单元
- 在每个格点处分配一个伪随机梯度向量
- 输入点根据与四个格点的相对位置计算点积
- 平滑插值
- 使用平滑插值函数对四个点积结果插值
- 最终返回平滑过渡的噪声值
2. 生成过程
2.1. 划分网格
通常情况下,我们会将整个图划分为边长为 1 的网格,如下图所示:

假设我们要计算某个二维位置点 $(x, y)$ 的噪声值,首先我们要找到它所在的整数网格单元。(一般情况下,我们定义网格单元的长度为 1 )即:
该点 $(x, y)$ 会处于四个整数格点 $(x_0,y_0)$、$(x_1,y_0)$、$(x_0,y_1)$、$(x_1,y_1)$ 形成的一个单位正方形内部。
2.2. 分配伪随机梯度向量
在每个格点上,我们需要分配一个伪随机梯度向量。通常情况下,这些向量是从一个预定义的方向集合中随机选择的,例如以下的八个方向:
1 | |
每个格点 $(x_0, y_0)$、$(x_1, y_0)$、$(x_0, y_1)$、$(x_1, y_1)$ 都会被分配一个梯度向量 $g_{00}$、$g_{10}$、$g_{01}$、$g_{11}$,如下图所示:(注:为方便展示,后续图只会展示一个格子)

2.3. 计算点积
接下来我们计算输入点 $(x, y)$ 与各格点之间的相对向量,并将其与该格点的梯度向量做点积(dot product)。这个点积的结果表示:
“该点在这个方向上与梯度方向的匹配程度”。
设:
我们计算:
点积得到的是该点在当前格点梯度方向上的“投影值”。如下图所示:
2.4. 平滑插值
由于我们只得到了离散的四个角落的点积值,我们需要对这些值进行插值,才能获得输入点 $(x, y)$ 的最终噪声值。
但如果直接使用线性插值,会在格点边界出现不连续。因此,Perlin 引入了一个平滑函数 fade(t) 来进行插值平滑过渡:
1 | |
该函数具有良好的光滑性,在 $t=0$ 和 $t=1$ 处一阶导数和二阶导数均为 0。
设:
2.5. 线性插值
为了求得输入点 $(x, y)$ 的噪声值,我们需要先求出 $(x ,y_{0})$ 和 $(x, y_{1})$ 两个点的插值。
插值函数 lerp(x0, x1, t) 定义为:
1 | |
因此,要求出$(x ,y_{0})$ 和 $(x, y_{1})$ 两个点的插值 $ix_0$ 和 $ix_1$,我们要使用以下公式:
此时我们得到了两个插值结果 $ix_0$ 和 $ix_1$,如图所示
然后在 y 方向进行插值:

最终得到该点 $(x, y)$ 的噪声值 value,它将在空间中连续且平滑变化。
通过Python实现的柏林噪声图如下

3. 步骤的必要性
3.1. 划分网格过大/过小会发生什么
如果网格划分过大,那么整个噪声图会变得非常平滑,缺乏细节,相邻像素之间变化非常缓慢,看上去像是一大片模糊的区域,如图所示(width=height=scale=500):

如果网格划分过小,那么噪声图会变得非常嘈杂,类似白噪声或像素化图像,不再平滑,且插值区间变窄,容易看出格子痕迹,如图所示(width=height=500,scale=10):

3.2. 梯度向量的必要性
如果梯度向量不随机分配,而是固定方向(如全部为 $(1, 0)$),则噪声图将失去随机性,变得非常规律和单调,如图所示:

3.3. 平滑插值的必要性
如果不使用平滑插值,即:
1 | |
则每个格子内点的噪声与格点的左下角的点有强关联,噪声图会出现明显的网格边缘,如图所示:
