2D 柏林噪声的原理

0. 简介

柏林噪声(Perlin Noise) 是一种广泛应用于计算机图形学的伪随机函数,常用于生成自然风格的纹理、地形、云彩等,例如 Minecraft 的世界生成就采用了柏林噪声。

1. 核心思路

柏林噪声的生成流程如下:

  1. 将平面划分为整数网格单元
  2. 在每个格点处分配一个伪随机梯度向量
  3. 输入点根据与四个格点的相对位置计算点积
  4. 平滑插值
  5. 使用平滑插值函数对四个点积结果插值
  6. 最终返回平滑过渡的噪声值

2. 生成过程

2.1. 划分网格

通常情况下,我们会将整个图划分为边长为 1 的网格,如下图所示:

假设我们要计算某个二维位置点 $(x, y)$ 的噪声值,首先我们要找到它所在的整数网格单元。(一般情况下,我们定义网格单元的长度为 1 )即:

$x_0 = floor(x)$
$y_0 = floor(y)$
$x_1 = x_0 + 1$
$y_1 = y_0 + 1$

该点 $(x, y)$ 会处于四个整数格点 $(x_0,y_0)$、$(x_1,y_0)$、$(x_0,y_1)$、$(x_1,y_1)$ 形成的一个单位正方形内部。

2.2. 分配伪随机梯度向量

在每个格点上,我们需要分配一个伪随机梯度向量。通常情况下,这些向量是从一个预定义的方向集合中随机选择的,例如以下的八个方向:

1
2
[(1, 0), (0, 1), (-1, 0), (0, -1), 
(1, 1), (-1, 1), (-1, -1), (1, -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)。这个点积的结果表示:

“该点在这个方向上与梯度方向的匹配程度”。

设:

$\vec{d}_{00} = (x - x_0, y - y_0)$
$\vec{d}_{10} = (x - x_1, y - y_0)$
$\vec{d}_{01} = (x - x_0, y - y_1)$
$\vec{d}_{11} = (x - x_1, y - y_1)$

我们计算:

$n_{00} = \vec{g}_{00} \cdot \vec{d}_{00}$
$n_{10} = \vec{g}_{10} \cdot \vec{d}_{10}$
$n_{01} = \vec{g}_{01} \cdot \vec{d}_{01}$
$n_{11} = \vec{g}_{11} \cdot \vec{d}_{11}$

点积得到的是该点在当前格点梯度方向上的“投影值”。如下图所示:

2.4. 平滑插值

由于我们只得到了离散的四个角落的点积值,我们需要对这些值进行插值,才能获得输入点 $(x, y)$ 的最终噪声值。

但如果直接使用线性插值,会在格点边界出现不连续。因此,Perlin 引入了一个平滑函数 fade(t) 来进行插值平滑过渡:

1
2
def fade(t):
return 6 * t**5 - 15 * t**4 + 10 * t**3

该函数具有良好的光滑性,在 $t=0$ 和 $t=1$ 处一阶导数和二阶导数均为 0。

设:

$u = fade(x - x_0)$
$v = fade(y - y_0)$

2.5. 线性插值

为了求得输入点 $(x, y)$ 的噪声值,我们需要先求出 $(x ,y_{0})$ 和 $(x, y_{1})$ 两个点的插值。

插值函数 lerp(x0, x1, t) 定义为:

1
2
def lerp(x0, x1, t):
return x0 + t * (x1 - x0)

因此,要求出$(x ,y_{0})$ 和 $(x, y_{1})$ 两个点的插值 $ix_0$ 和 $ix_1$,我们要使用以下公式:

$ix_0 = lerp(n_{00}, n_{10}, u)$
$ix_1 = lerp(n_{01}, n_{11}, u)$

此时我们得到了两个插值结果 $ix_0$ 和 $ix_1$,如图所示

然后在 y 方向进行插值:

$value = lerp(ix_0, ix_1, v)$
如下图所示:

最终得到该点 $(x, y)$ 的噪声值 value,它将在空间中连续且平滑变化。

通过Python实现的柏林噪声图如下

3. 步骤的必要性

3.1. 划分网格过大/过小会发生什么

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

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

3.2. 梯度向量的必要性

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

3.3. 平滑插值的必要性

如果不使用平滑插值,即:

1
2
def fade(t):
return t

则每个格子内点的噪声与格点的左下角的点有强关联,噪声图会出现明显的网格边缘,如图所示:


2D 柏林噪声的原理
https://nacldragon.top/2025/Perlin-Noise/
作者
NaCl
发布于
2025年8月1日
许可协议