噪声算法基础(上):白噪声、值噪声、Perlin噪声的原理与实现

各位好,今天我们开始聊噪声算法。这是程序化地形生成里最核心的一块地基。我个人习惯把噪声比作「数字世界的泥土」——你捏地形、做纹理、生成植被分布,都离不开它。

这一节我们先搞定三种最基础的噪声:白噪声、值噪声、Perlin噪声。别小看它们,很多高级噪声(比如分形布朗运动、域扭曲)都是在这上面搭积木搭出来的。

1. 白噪声:最纯粹的随机

白噪声是什么?说白了就是每个像素点独立随机取值。没有任何规律,相邻像素之间毫无关联。

我在项目中第一次用白噪声做地形高度图,结果出来一个「刺猬山」——全是尖锐的毛刺,根本没法用。嗯,这就是白噪声的典型问题:太随机了,没有连续性。

核心特征:

  • 每个采样点完全独立
  • 频谱能量均匀分布(所以叫「白」噪声)
  • 没有空间相关性

实现起来极其简单:

// 白噪声 - 最简单的实现
float WhiteNoise(float2 uv)
{
    // 用小数部分打散整数坐标
    return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}

这段代码我用了好多年。核心思路是用正弦函数的小数部分来制造「伪随机」。你可能会问:为什么不用rand()?因为在GPU上,sin + frac 的组合比标准库函数快得多。

注意:白噪声的「伪随机」特性意味着——同样的输入永远得到同样的输出。这在程序化生成里是好事,因为我们需要可复现的结果。

2. 值噪声:让随机变得平滑

白噪声太「炸」了,怎么办?值噪声给出了一个很自然的思路:在网格顶点上放随机值,中间区域用插值过渡。

我曾经在做一个沙漠地形时,一开始用白噪声生成沙丘高度,结果沙丘边缘全是锯齿。换成值噪声后,问题立刻解决了——因为插值让相邻区域平滑过渡。

值噪声的实现分三步:

  1. 定义网格:把空间划分成整数格子
  2. 顶点赋值:每个格子顶点给一个随机值
  3. 插值:根据采样点在格子内的位置,对四个顶点做双线性插值
// 值噪声 - 双线性插值版本
float ValueNoise(float2 uv)
{
    // 1. 找到格子顶点
    float2 i = floor(uv);
    float2 f = frac(uv);
    
    // 2. 四个顶点的随机值
    float a = WhiteNoise(i);
    float b = WhiteNoise(i + float2(1, 0));
    float c = WhiteNoise(i + float2(0, 1));
    float d = WhiteNoise(i + float2(1, 1));
    
    // 3. 平滑插值(用f*f*(3-2f)做平滑步进)
    float2 u = f * f * (3.0 - 2.0 * f);
    
    // 4. 双线性插值
    return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y);
}

技巧:插值函数用 f*f*(3-2f) 而不是直接 f,是因为前者在端点处的导数为0,能让过渡更自然。这个函数也叫「平滑步进」(smoothstep)。

值噪声的优点是简单、计算快。缺点也很明显——有明显的网格痕迹,看起来像「格子布」。这是因为每个格子内的变化完全由四个顶点决定,格子边界处虽然值连续,但导数不连续。

3. Perlin噪声:Ken Perlin的杰作

1983年,Ken Perlin为了给电影《电子世界争霸战》做纹理,发明了Perlin噪声。这玩意儿后来拿了奥斯卡技术奖。你想想看,一个算法能拿奥斯卡,说明它有多重要。

Perlin噪声和值噪声最大的区别在于:它用梯度向量代替了随机标量。什么意思呢?每个顶点不再是一个随机数值,而是一个随机方向(梯度)。采样时,计算采样点到顶点的向量与梯度向量的点积。

这样做的好处是——噪声的导数连续,看起来更自然,没有网格痕迹。

// Perlin噪声 - 核心实现
float PerlinNoise(float2 uv)
{
    float2 i = floor(uv);
    float2 f = frac(uv);
    
    // 平滑插值权重
    float2 u = f * f * (3.0 - 2.0 * f);
    
    // 四个顶点的梯度向量(预定义的随机方向)
    float2 g00 = Gradients[i.x][i.y];
    float2 g10 = Gradients[i.x + 1][i.y];
    float2 g01 = Gradients[i.x][i.y + 1];
    float2 g11 = Gradients[i.x + 1][i.y + 1];
    
    // 计算点到顶点的向量
    float2 d00 = f;
    float2 d10 = f - float2(1, 0);
    float2 d01 = f - float2(0, 1);
    float2 d11 = f - float2(1, 1);
    
    // 点积 + 双线性插值
    float n00 = dot(g00, d00);
    float n10 = dot(g10, d10);
    float n01 = dot(g01, d01);
    float n11 = dot(g11, d11);
    
    return lerp(lerp(n00, n10, u.x), lerp(n01, n11, u.x), u.y);
}

关键区别总结:

特性 值噪声 Perlin噪声
顶点数据 随机标量 随机梯度向量
插值方式 直接插值 点积后插值
导数连续性 不连续 连续
网格痕迹 明显 几乎不可见
计算开销 中等

4. 三种噪声的对比与选择

在实际项目中怎么选?我个人的经验是这样的:

  • 白噪声:适合做粒子系统的初始位置、随机颜色偏移、或者需要「完全随机」的场景。别用它做地形高度图,除非你想要「刀山」。
  • 值噪声:适合做低频的大尺度地形起伏,比如平原上的缓坡。计算快,但别用在需要精细细节的地方。
  • Perlin噪声:通用性最强。地形、云朵、纹理、程序化材质,都能用。缺点是计算稍重,但现代GPU上完全不是问题。

避坑指南:我曾经在移动端项目里用Perlin噪声做地形,结果帧率掉了一半。后来发现是每帧都在CPU端计算噪声。解决方案:预计算成纹理贴图,GPU直接采样。性能瞬间提升10倍。

5. 知识体系结构图

下面这张图展示了三种噪声的核心逻辑和它们之间的关系:

噪声算法基础(上)知识体系 白噪声 独立随机采样 无空间相关性 频谱均匀分布 加插值 值噪声 网格顶点随机标量 双线性插值过渡 有网格痕迹 梯度向量 Perlin噪声 顶点梯度向量 点积后插值 导数连续 三种噪声对比 特性 白噪声 值噪声 Perlin噪声 连续性 值连续 导数连续 计算开销 极低 中等 适用场景 随机数 大尺度地形 通用

6. 写在最后

这三种噪声是程序化地形的「字母表」。白噪声是单个字母,值噪声是单词,Perlin噪声是流畅的句子。掌握了它们,你就能开始「写作」了。

下一节我们会深入Simplex噪声和分形噪声,到时候你会看到,今天学的这些基础是如何被组合成更强大的工具的。嗯,先消化好这些,动手写写代码,比什么都强。


公众号:蓝海资料掘金营,微信deep3321