“自然界中没有真正的随机,只有我们尚未理解的规律。” —— Perlin Noise 的哲学


目录

  1. 什么是 Perlin Noise?
  2. 为什么需要 Perlin Noise?
  3. 核心原理
  4. 一维 Perlin Noise 详解
  5. 二维 Perlin Noise 详解
  6. 分形噪声(Fractal Noise / fBm)
  7. Simplex Noise
  8. 代码实现
  9. 常见应用场景
  10. 参数调节技巧
  11. 常见误区
  12. 参考资料

1. 什么是 Perlin Noise?

Perlin Noise(柏林噪声)是由 Ken Perlin 在 1983 年为电影《创:战纪》(Tron)开发的一种梯度噪声算法,并于 1985 年在 SIGGRAPH 论文 An Image Synthesizer 中正式发表。Ken Perlin 也因此获得了奥斯卡科学技术奖。

它是一种相干噪声(Coherent Noise),具有以下特点:

  • 相邻采样点之间平滑过渡,不会突变
  • 在统计上是各向同性的(没有明显方向偏好)
  • 输出值在一定范围内(通常 -11,或归一化到 01
  • 可重复:相同输入总是产生相同输出

2. 为什么需要 Perlin Noise?

纯随机的问题

纯随机(White Noise):9 4 1 7 3 8 2 5 6 ...
Perlin Noise:        3 4 5 6 5 4 3 2 3 ...

纯随机噪声(白噪声)每个像素完全独立,产生的是”电视雪花”效果,在自然界中几乎不存在

真实的自然现象(山脉、云朵、火焰、水面)都具有局部相似性:靠近的地方高度/颜色差异不大,但整体上又有宏观的起伏变化。Perlin Noise 正是为了模拟这种特性而生。

对比总结

特性 白噪声 Perlin Noise
相邻平滑
自然感
可控性
计算复杂度 O(1) O(2ⁿ),n 为维度

3. 核心原理

Perlin Noise 的核心思路分三步:

① 建立网格,为每个格点分配随机梯度向量
        ↓
② 计算采样点到周围格点的距离向量,与梯度向量做点积
        ↓
③ 用平滑插值函数,将周围格点的点积值融合成最终输出

关键概念:梯度向量(Gradient Vector)

在每个整数坐标(格点)上,预先分配一个随机的单位方向向量,这就是”梯度”。

与 Value Noise(直接在格点上存随机值)不同,Perlin Noise 存的是方向,而不是。这使得它的输出更加平滑、视觉上更自然。

关键概念:平滑插值函数

普通线性插值(lerp)会产生折线感。Perlin 使用了五次平滑曲线(Quintic Curve)

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

这个函数的特点:

  • $f(0) = 0$,$f(1) = 1$
  • $f’(0) = 0$,$f’(1) = 0$(端点处导数为 0,保证平滑衔接)
  • $f’‘(0) = 0$,$f’‘(1) = 0$(二阶导也为 0,更加平滑)

早期版本使用的是三次曲线 $3t^2 - 2t^3$(Smoothstep),改进版使用五次曲线(Smootherstep)。


4. 一维 Perlin Noise 详解

以一维为例,假设采样点 $x = 2.3$:

Step 1:找到所属格子

左格点:x0 = floor(2.3) = 2
右格点:x1 = x0 + 1 = 3
局部坐标:t = 2.3 - 2 = 0.3

Step 2:获取两端梯度

一维中,梯度只有两个方向:+1-1,通过置换表(Permutation Table)伪随机确定:

grad(2) = +1
grad(3) = -1

Step 3:计算点积

距离向量 × 梯度向量(一维中即乘法):

左端点贡献:dot(grad(x0), x - x0) = (+1) × (2.3 - 2) = +0.3
右端点贡献:dot(grad(x1), x - x1) = (-1) × (2.3 - 3) = +0.7

Step 4:平滑插值

u = fade(t) = fade(0.3) ≈ 0.163  (五次曲线)
result = lerp(u, 0.3, 0.7) = 0.3 + 0.163 × (0.7 - 0.3) ≈ 0.365

5. 二维 Perlin Noise 详解

二维时,采样点 $(x, y)$ 被四个格点围绕:

(x0,y1) -------- (x1,y1)
   |         .        |
   |      (x,y)       |
   |                  |
(x0,y0) -------- (x1,y0)

梯度向量(2D)

二维梯度通常从以下 4 个(或 8 个)向量中随机选取:

(1,0), (-1,0), (0,1), (0,-1)
(1,1), (-1,1), (1,-1), (-1,-1)  ← 归一化后使用

计算流程

① 计算采样点到 4 个角的距离向量
② 分别与各角的梯度做点积,得到 4 个值
③ 用 fade(tx) 和 fade(ty) 先沿 x 插值,再沿 y 插值
# 伪代码
a = dot(grad(x0, y0), dx,   dy  )
b = dot(grad(x1, y0), dx-1, dy  )
c = dot(grad(x0, y1), dx,   dy-1)
d = dot(grad(x1, y1), dx-1, dy-1)

u = fade(dx)
v = fade(dy)

ab = lerp(u, a, b)
cd = lerp(u, c, d)
result = lerp(v, ab, cd)

6. 分形噪声(Fractal Noise / fBm)

单层 Perlin Noise 产生的地形过于”平滑单调”,缺乏细节。自然界的地貌(山脉、云朵)是多尺度叠加的结果。

fBm(Fractional Brownian Motion)

通过叠加多个频率和振幅不同的 Noise 层(倍频程,Octaves):

\[\text{fBm}(x) = \sum_{i=0}^{n} \text{amplitude}_i \cdot \text{noise}(\text{frequency}_i \cdot x)\]

关键参数

参数 说明 典型值
Octaves(倍频程数) 叠加的层数,越多细节越丰富 4 ~ 8
Lacunarity(缺陷度) 每层频率的倍增系数 2.0
Persistence / Gain 每层振幅的衰减系数 0.5

示例(4 层叠加)

Layer 0: frequency=1,  amplitude=1.0   → 大尺度地形轮廓
Layer 1: frequency=2,  amplitude=0.5   → 中尺度丘陵
Layer 2: frequency=4,  amplitude=0.25  → 小尺度岩石
Layer 3: frequency=8,  amplitude=0.125 → 细节纹理
def fbm(x, y, octaves=6, lacunarity=2.0, gain=0.5):
    value = 0.0
    amplitude = 1.0
    frequency = 1.0
    for _ in range(octaves):
        value += amplitude * perlin(x * frequency, y * frequency)
        amplitude *= gain
        frequency *= lacunarity
    return value

7. Simplex Noise

Ken Perlin 于 2001 年提出了 Simplex Noise,作为 Perlin Noise 的改进版本。

改进点

特性 Perlin Noise Simplex Noise
插值方向数 2ⁿ 个角 n+1 个顶点
计算复杂度(高维) O(2ⁿ) O(n²)
方向性伪影 有(轴对齐) 较少
实现复杂度 较简单 较复杂
专利问题 3D+ 版本有专利(OpenSimplex 可替代)

在高维(3D、4D)场景中,Simplex Noise 的性能优势非常显著

如果需要无专利约束的实现,可以使用 OpenSimplex2SuperSimplex


8. 代码实现

Python 实现(简化版 2D Perlin Noise)

import math
import random

def fade(t):
    """五次平滑曲线"""
    return t * t * t * (t * (t * 6 - 15) + 10)

def lerp(t, a, b):
    """线性插值"""
    return a + t * (b - a)

def grad(hash_val, x, y):
    """根据 hash 值选择梯度方向并计算点积"""
    h = hash_val & 3
    if h == 0: return  x + y
    if h == 1: return -x + y
    if h == 2: return  x - y
    return              -x - y

class PerlinNoise2D:
    def __init__(self, seed=None):
        random.seed(seed)
        # 构建置换表
        self.perm = list(range(256))
        random.shuffle(self.perm)
        self.perm += self.perm  # 复制一份,方便索引

    def noise(self, x, y):
        # 格点坐标
        xi = int(math.floor(x)) & 255
        yi = int(math.floor(y)) & 255
        # 局部坐标
        xf = x - math.floor(x)
        yf = y - math.floor(y)
        # 平滑
        u = fade(xf)
        v = fade(yf)
        # 四个角的 hash
        p = self.perm
        aa = p[p[xi    ] + yi    ]
        ab = p[p[xi    ] + yi + 1]
        ba = p[p[xi + 1] + yi    ]
        bb = p[p[xi + 1] + yi + 1]
        # 插值
        x1 = lerp(u, grad(aa, xf,   yf  ), grad(ba, xf-1, yf  ))
        x2 = lerp(u, grad(ab, xf,   yf-1), grad(bb, xf-1, yf-1))
        return lerp(v, x1, x2)

# 使用示例
pn = PerlinNoise2D(seed=42)
for y in range(5):
    row = [pn.noise(x * 0.1, y * 0.1) for x in range(10)]
    print([f"{v:.2f}" for v in row])

GLSL Shader 实现(2D)

// 置换函数(近似)
float hash(vec2 p) {
    p = fract(p * vec2(127.1, 311.7));
    p += dot(p, p + 19.19);
    return fract(p.x * p.y);
}

// 梯度向量
vec2 gradient(vec2 p) {
    float h = hash(p);
    float angle = h * 6.2832; // 0 ~ 2π
    return vec2(cos(angle), sin(angle));
}

// 2D Perlin Noise
float perlin(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); // fade

    float a = dot(gradient(i + vec2(0,0)), f - vec2(0,0));
    float b = dot(gradient(i + vec2(1,0)), f - vec2(1,0));
    float c = dot(gradient(i + vec2(0,1)), f - vec2(0,1));
    float d = dot(gradient(i + vec2(1,1)), f - vec2(1,1));

    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

9. 常见应用场景

地形生成

高度图 = fBm(x, z)
纹理混合权重 = noise(x, z)  → 草地/泥土/岩石

云朵与天空

云密度 = smoothstep(0.4, 0.6, fBm(x, y, t))

水面与波浪

水面高度(x, z, t) = noise(x + t, z) + noise(x, z + t * 0.7) * 0.5
法线方向 = gradient of height field

程序化纹理

  • 大理石纹理:sin(x + noise(x, y) * 10)
  • 木纹纹理:sin(sqrt(x² + y²) + noise(x, y) * 5)
  • 火焰效果:动态 noise 配合颜色渐变

动画与运动

  • 相机抖动:用低频 noise 驱动相机偏移,模拟手持效果
  • NPC 行走路径:noise 生成自然的漫游方向
  • 粒子系统:用 noise 场控制粒子受力方向

10. 参数调节技巧

频率(Frequency / Scale)

频率低(Scale 大)→ 变化缓慢,大尺度特征(山脉、大陆)
频率高(Scale 小)→ 变化快速,细节特征(石头纹理、草地)

振幅(Amplitude)

控制 noise 值的强度范围,通常配合归一化:

# 归一化到 [0, 1]
normalized = (noise_value + 1) / 2

Octaves 的选择

2 ~ 3 层:平滑、简洁(适合云朵、水面)
4 ~ 6 层:自然地形(适合山脉、丘陵)
7 ~ 8 层:极丰富细节(适合岩石、土壤)

注意:层数过多在视觉上收益递减,但计算量线性增加。

域变形(Domain Warping)

一种高级技巧,用 noise 对坐标本身进行扭曲,产生极度自然的流动感:

def warped_noise(x, y):
    # 用 noise 偏移输入坐标
    dx = fbm(x + 1.7, y + 9.2)
    dy = fbm(x + 8.3, y + 2.8)
    return fbm(x + 4.0 * dx, y + 4.0 * dy)

这正是 Inigo Quilez 等人创作的许多 Shadertoy 作品的核心技巧。


11. 常见误区

误区 1:Perlin Noise 是随机的

Perlin Noise 不是随机的。相同的输入坐标永远返回相同的值。它是确定性的伪随机过程。真正的随机来自初始化时打乱的置换表(seed)。

误区 2:输出范围是 [-1, 1]

理论范围是 $[-\sqrt{n/4}, \sqrt{n/4}]$(n 为维度),实际实现中因梯度选取不同而略有差异。在实践中,不要假设输出严格限制在某个范围内,应对输出进行归一化或 clamp 处理。

误区 3:频率越高细节越好

高频 noise 容易产生走样(Aliasing),在动态场景中会出现闪烁。应根据采样率选择合适的最高频率(参考奈奎斯特定理)。

误区 4:用 random() 直接生成梯度

不少初学者在每次调用时重新生成随机梯度,导致输出不连贯或不可重复。正确做法是预先构建置换表,通过查表确定梯度。


12. 参考资料