Perlin噪声

引言

在计算机图形学中,Perlin噪声是一种非常重要的算法,它被广泛用于生成自然现象,如云朵、地形、火焰等。它的发明者Ken Perlin在1983年首次提出了这一算法,并在1985年因这项工作获得了奥斯卡科学技术奖。

原始实现获得奥斯卡科学技术奖

Perlin噪声之所以受欢迎,是因为它能生成看似随机但具有连续性和平滑性的噪声,这使得生成的图形更自然。

Perlin噪声的历史

Ken Perlin发明Perlin噪声的初衷是为了解决传统伪随机噪声在计算机图形学中产生的不自然感。传统噪声函数往往会在图像中产生尖锐的、不连续的过渡,导致图像看起来不够真实。为了解决这个问题,Perlin提出了一种可以生成“渐进式噪声”的方法,这种噪声在空间上是连续的,过渡平滑,可以很好地模拟自然现象。

Perlin噪声的最初版本是基于一维的,之后扩展到了二维和三维。在1985年,这一算法被应用于电影《创:战纪》的特效制作中,用于生成云朵、烟雾等特效。Ken Perlin因此获得了图形学界的广泛认可,并在1985年获得奥斯卡科学技术奖。

Perlin噪声的原理

Perlin噪声的核心思想是通过在空间中的各个点上生成一组伪随机梯度,并通过插值来平滑过渡。这种方法使得噪声在整个空间中呈现出连续变化的趋势,而不是像传统噪声那样在各个点之间出现剧烈的变化。

具体来说,Perlin噪声生成过程可以分为以下几个步骤:

  1. 网格划分:将整个空间划分成一个个小网格。在每个网格的顶点上分配一个随机的梯度向量。

  2. 计算相对位置:对于任意一个输入点,找到它所在的网格,并计算该点相对于网格顶点的偏移量。

  3. 梯度点积:使用前面生成的随机梯度向量与偏移量进行点积运算,生成每个顶点的贡献值。

  4. 平滑插值:使用插值函数对各个顶点的贡献值进行平滑插值,得到最终的噪声值。

    在 Perlin 噪声中,网格点(grid point)是噪声函数定义的固定点,这些点在一个规则的网格上排列。网格通常是一个二维或三维的结构,每个维度上等距分布。Perlin 噪声会为每个网格点分配一个随机的梯度向量,这些梯度向量用于计算噪声值在该点附近的变化。

    当输入位置位于两个网格点之间时,Perlin 噪声函数会使用这些梯度向量和插值函数来平滑地计算该位置的噪声值。这种通过网格点生成的平滑噪声广泛用于生成自然的纹理、地形等。

我们使用Python来可视化展示Perlin 噪声的生成过程:

首先我们进行网格划分,并为每个网格点分配随机梯度向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def perlin_noise_visualization(size=5, resolution=100):
# 步骤1: 创建网格
grid_points = np.linspace(0, size, resolution)
x, y = np.meshgrid(grid_points, grid_points)

# 步骤2:定义网格点
x0 = (x // 1).astype(int)
y0 = (y // 1).astype(int)

# 步骤3: 计算单位方格内各点的相对位置
x_rel = x - x0
y_rel = y - y0

# 步骤4:为每个网格点分配随机梯度向量
angles = np.random.rand(resolution + 1, resolution + 1) * 2 * np.pi # 每个网格点生成一个0到2π之间的随机角度
gradients_x = np.cos(angles) # 计算该角度的x分量。
gradients_y = np.sin(angles) # 计算该角度的y分量。

# 可视化网格和梯度向量
fig, ax = plt.subplots(figsize=(8, 8))
ax.quiver(x0, y0, gradients_x[:-1, :-1], gradients_y[:-1, :-1], color='red')
ax.set_title("Grid Points and Gradient Vectors")
plt.show()
GridPointsandGradientVectors

然后计算梯度向量与距离向量的点积。这个点积就是表示输入点相对于网格点的影响程度。

数学上,点积的原理是将两个向量的关系投影到梯度向量的方向上。具体来说:

  • 梯度向量代表了噪声在该网格点的变化方向。
  • 距离向量表示输入点与网格点的相对位置。
  • 当两向量平行时,点积最大,表明输入点位于梯度变化的主方向上;当两向量垂直时,点积为零,表明输入点对噪声的影响最小。这个点积的结果用于加权插值,形成平滑的噪声值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 步骤5:计算梯度向量与距离向量的点积
def dot_grid_gradient(ix, iy, x, y): # dx 和 dy 分别表示输入点 (x, y) 与网格点 (ix, iy) 的水平和垂直距离。
dx = x - ix
dy = y - iy
gradient = np.array([gradients_x[iy, ix], gradients_y[iy, ix]]) # 获取梯度向量
return dx * gradient[0] + dy * gradient[1] # 计算距离向量与梯度向量的点积。

n00 = dot_grid_gradient(x0, y0, x, y)
n10 = dot_grid_gradient(x0 + 1, y0, x, y)
n01 = dot_grid_gradient(x0, y0 + 1, x, y)
n11 = dot_grid_gradient(x0 + 1, y0 + 1, x, y)
# n00、n10、n01 和 n11 代表的是在四个相邻的网格点处,梯度向量与输入点的距离向量的点积结果
plt.figure(figsize=(8, 8))
plt.subplot(2, 2, 1)
plt.title("n00: (0,0) Grid Point Contribution")
plt.imshow(n00, cmap=cm.viridis, origin='upper')
plt.colorbar()

plt.subplot(2, 2, 2)
plt.title("n10: (1,0) Grid Point Contribution")
plt.imshow(n10, cmap=cm.viridis, origin='upper')
plt.colorbar()

plt.subplot(2, 2, 3)
plt.title("n01: (0,1) Grid Point Contribution")
plt.imshow(n01, cmap=cm.viridis, origin='upper')
plt.colorbar()

plt.subplot(2, 2, 4)
plt.title("n11: (1,1) Grid Point Contribution")
plt.imshow(n11, cmap=cm.viridis, origin='upper')
plt.colorbar()

plt.suptitle("Perlin Noise Without Interpolation")
plt.show()

随后我们直接绘制n00n10n01n11 的图像的结果:

PerlinNoiseWithoutInterpolation

由于未进行插值处理,图像中的噪声看起来是块状的,并且在不同区域之间存在明显的断层和不连续的变化。这是因为在每个网格点之间的值没有进行平滑过渡,同时也直接反映了噪声在这些点上的随机性。


最后进行插值操作:

在Perlin噪声的生成中,fade(t) 函数用于平滑插值。具体来说,它对 t 值进行处理,使得噪声在网格点之间的过渡更加平滑。

数学上,fade(t) 是一个三次方平滑函数,其公式为:

\[f(t) = t^3 \times (t \times (6t - 15) + 10)\]

这个函数被应用于插值计算中,通过对两个噪声值之间的插值来平滑噪声。

首先,计算出 xy 的相对位置 (uv) 然后使用 fade(u)fade(v) 来插值生成平滑的噪声值。

u = fade(x_rel) v = fade(y_rel)

在水平方向的线性插值中,nx0 和 nx1 分别对n00和n10、n01和n11插值:

\[ nx0=(1−u)⋅n00+u⋅n10\]

\[nx1=(1−u)⋅n01+u⋅n11 \]

其中,u 是相对位置 x_rel 经过 fade 函数平滑后的结果。这两个公式分别对 n00n10n01n11 进行线性插值,从而在水平方向上计算出两个新的插值结果 nx0nx1

在垂直方向进行线性插值,计算最终的噪声值 perlin_value :

\[ value=(1−v)⋅nx0+v⋅nx1\]

其中,v 是相对位置 y_rel 经过 fade 函数平滑后的结果。这个公式在垂直方向上对 nx0nx1 进行线性插值,从而计算出最终的 Perlin 噪声值 perlin_value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 步骤6 插值
def fade(t): # fade函数将线性插值转换为更平滑的插值,通过使用三次函数来平滑过渡。
return t * t * t * (t * (t * 6 - 15) + 10)

u = fade(x_rel)
v = fade(y_rel)
# 水平插值

# 首先在水平方向进行线性插值:nx0 和 nx1 分别对n00和n10、n01和n11插值。
# 公式为:nx0 = (1 - u) * n00 + u * n10,nx1 = (1 - u) * n01 + u * n11,其中u是x_rel经过fade后的结果。
nx0 = (1 - u) * n00 + u * n10
nx1 = (1 - u) * n01 + u * n11
# 垂直插值 得到最终的噪声值perlin_value
perlin_value = (1 - v) * nx0 + v * nx1

# 步骤7: 展示
plt.figure(figsize=(8, 8))
plt.imshow(perlin_value, cmap=cm.viridis, origin='upper')
plt.colorbar()
plt.title("Perlin Noise Visualization")
plt.show()

噪声值如何在二维平面上形成平滑的过渡和复杂的纹理:

PerlinNoiseVisualization

数学背景

fade 函数的数学推导

fade 函数是Perlin噪声中用于平滑插值的关键部分。它通过一个三次多项式对插值点进行平滑处理,以避免简单线性插值带来的突变。函数的公式为:

$ f(t) = t^3 (t (6t - 15) + 10) $

这个公式的推导可以从以下几个方面理解:

  1. 立方插值: 函数 $t^3 $用于生成一个立方曲线,这种曲线在插值的起始和结束处表现得比较平滑。立方插值相比线性插值能更好地避免在插值范围内的突变。

  2. 二次多项式调整\(( t \times (6t - 15) + 10 )\) 部分是一个二次多项式,用于调整插值曲线的形状,使得过渡更加平滑。具体来说,这个多项式在 ( t = 0 ) 和 ( t = 1 ) 处具有平滑的导数,避免了尖锐的转折。

  3. 平滑过渡: 通过结合立方插值和二次多项式,fade 函数在 ( t ) 的变化范围 [0, 1] 内提供了一个平滑的过渡。这样可以确保在网格点之间的过渡是连续且自然的,避免了简单线性插值可能带来的硬边缘和突兀变化。

梯度计算的原理

在Perlin噪声中,梯度计算是生成噪声的关键步骤。梯度向量用于描述噪声在每个网格点的变化方向。这些梯度向量的计算和使用原理如下:

  1. 随机梯度向量: 每个网格点上分配一个随机梯度向量。这个梯度向量的方向决定了噪声在该点的变化方向。梯度向量的选择通常是随机的,以确保噪声的随机性和多样性。

  2. 点积计算: 对于输入点来说,梯度向量与距离向量的点积计算用于确定该点相对于网格点的噪声值。这种计算方式将输入点的位置投影到梯度向量的方向上,从而生成噪声值。

  3. 影响程度: 点积的结果表示输入点对噪声值的影响程度。当距离向量与梯度向量平行时,点积最大,表明该点对噪声的贡献最大;当两者垂直时,点积为零,说明该点对噪声的影响最小。

这种计算方法使得噪声在网格点之间过渡平滑,并能够生成自然的纹理和图案。

参考文章:

了解柏林噪声