噪声算法基础(上):白噪声、值噪声、Perlin噪声的原理与实现
各位好,今天我们开始聊噪声算法。这是程序化地形生成里最核心的一块地基。我个人习惯把噪声比作「数字世界的泥土」——你捏地形、做纹理、生成植被分布,都离不开它。
这一节我们先搞定三种最基础的噪声:白噪声、值噪声、Perlin噪声。别小看它们,很多高级噪声(比如分形布朗运动、域扭曲)都是在这上面搭积木搭出来的。
1. 白噪声:最纯粹的随机
白噪声是什么?说白了就是每个像素点独立随机取值。没有任何规律,相邻像素之间毫无关联。
我在项目中第一次用白噪声做地形高度图,结果出来一个「刺猬山」——全是尖锐的毛刺,根本没法用。嗯,这就是白噪声的典型问题:太随机了,没有连续性。
核心特征:
- 每个采样点完全独立
- 频谱能量均匀分布(所以叫「白」噪声)
- 没有空间相关性
实现起来极其简单:
// 白噪声 - 最简单的实现
float WhiteNoise(float2 uv)
{
// 用小数部分打散整数坐标
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}
这段代码我用了好多年。核心思路是用正弦函数的小数部分来制造「伪随机」。你可能会问:为什么不用rand()?因为在GPU上,sin + frac 的组合比标准库函数快得多。
注意:白噪声的「伪随机」特性意味着——同样的输入永远得到同样的输出。这在程序化生成里是好事,因为我们需要可复现的结果。
2. 值噪声:让随机变得平滑
白噪声太「炸」了,怎么办?值噪声给出了一个很自然的思路:在网格顶点上放随机值,中间区域用插值过渡。
我曾经在做一个沙漠地形时,一开始用白噪声生成沙丘高度,结果沙丘边缘全是锯齿。换成值噪声后,问题立刻解决了——因为插值让相邻区域平滑过渡。
值噪声的实现分三步:
- 定义网格:把空间划分成整数格子
- 顶点赋值:每个格子顶点给一个随机值
- 插值:根据采样点在格子内的位置,对四个顶点做双线性插值
// 值噪声 - 双线性插值版本
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. 知识体系结构图
下面这张图展示了三种噪声的核心逻辑和它们之间的关系:
6. 写在最后
这三种噪声是程序化地形的「字母表」。白噪声是单个字母,值噪声是单词,Perlin噪声是流畅的句子。掌握了它们,你就能开始「写作」了。
下一节我们会深入Simplex噪声和分形噪声,到时候你会看到,今天学的这些基础是如何被组合成更强大的工具的。嗯,先消化好这些,动手写写代码,比什么都强。
公众号:蓝海资料掘金营,微信deep3321