基于C++实现柏林噪声算法(Perlin Noise)
柏林噪声是一种具有连续性的随机函数,通过插值和哈希映射,确保相同输入产生一致输出且相邻点变化平滑。本文以C++为例,拆解其核心算法逻辑,从基础的一维线性插值拓展至经典的三维网格计算,并展示如何通过倍频叠加等波形调整技巧,在程序设计中高效生成逼真的自然地形或纹理。
概述
柏林噪声(Perlin noise)由 Ken Perlin 发明,主要特征是具有函数连续性。在电子游戏中常用于生成具备自然过渡的地形,或在艺术领域生成连续变化的图样。与存在时间种子状态的普通随机函数不同,柏林噪声更像是一个确定性 Hash 函数,具备两大特点:
- 连续输入得到平滑输出的结果(对邻近输入有加权采样效应)。
- 完全相同的输入必然返回完全相同的输出。
该算法非常适合需要“连续且可控的随机性”的场景。例如模拟自然地形时,避免地面出现前一步是高山、脚下是深谷、后一步又是高山的剧烈断层。
对比普通随机函数输出的离散跳变,柏林噪声输出的曲线则呈现出自然的连贯波动:
生成原理
要理解柏林噪声的平滑特性,核心在于插值和平滑函数的应用。对于有经验的基础开发者来说,直觉上平滑往往意味着插值运算。下面先从简单的一维情况推导其核心机制,再拓展至复杂的二维和三维场景。
第1步:理解平滑插值基础
在一维空间中,我们预先在坐标系中按固定单位距离散落一组随机数值。假设这些值为 rands[],对应的坐标点为 P1、P2... 。对于区间内的任意一点 P,其噪声值取决于它两侧最近的两个随机节点值(设为 Y1 和 Y2),进行线性插值计算:
- 确定 P 所在的网格区间,获取左右边界随机值 Y1 和 Y2。
- 计算归一化的位置比例,即 P 点在当前区间的相对位置。
- 将相对位置传入平滑函数 F(x),消除线性插值导致的生硬折角。
- 使用平滑后的比例 t 进行最终插值:
Noise(P) = Y1 + (Y2 - Y1) * F(t)。
这里的平滑函数通常采用五次多项式,如 F(x) = 6x^5 - 15x^4 + 10x^3。它在 0 到 1 区间内不仅保持了单调递增,更在端点处的导数为零,从而保证了斜率的完美衔接:
第2步:拓展至多维空间(二维与三维)
一维噪声可以无缝推广到二维平面。假设目标点 P 落在 a、b、c、d 四个随机点组成的二维网格内。本质上这是三次一维计算的组合:
- X轴方向插值:分别在 ab 边和 dc 边上,根据横坐标比例计算出对应点 X1 和 X2 的值。
- Y轴方向插值:在得到的 X1 和 X2 之间,根据纵坐标比例进行插值,得出最终的 P 点噪声值。
扩展到三维时,原理相同但维度增加。P 点位于一个由8个顶点(abcd 和 efg h等构成的小立方体)包围的空间内,需要沿着三个轴向分别进行插值操作,从而融合八个顶点的信息。
经典C++实现解析
实现柏林噪声的标准流程依赖于预定义的随机哈希表以及梯度向量,以下是经典的基于 C++ 的三维柏林噪声核心源码及其逻辑解读:
static int p[512] = {
151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180,
151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180
};
static float3 grads[12] = {
{1,1,0},
{-1,1,0},
{1,-1,0},
{-1,-1,0},
{1,0,1},
{-1,0,1},
{1,0,-1},
{-1,0,-1},
{0,1,1},
{0,-1,1},
{0,1,-1},
{0,-1,-1}
};
float grad(int hash, float x, float y, float z)
{
float3 v3 = float3(x,y,z);
hash = hash & 0xb;
return dot(grads[hash],v3);
}
int inc(int num) {
num++;
return num;
}
float fade(float t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
float perlin(float x,float y,float z)
{
int xi = (int)x & 255;
int yi = (int)y & 255;
int zi = (int)z & 255;
float xf = x - xi;
float yf = y - yi;
float zf = z - zi;
float u = fade(xf);
float v = fade(yf);
float w = fade(zf);
int aaa, aba, aab, abb, baa, bba, bab, bbb;
aaa = p[p[p[ xi ]+ yi ]+ zi ];
aba = p[p[p[ xi ]+inc(yi)]+ zi ];
aab = p[p[p[ xi ]+ yi ]+inc(zi)];
abb = p[p[p[ xi ]+inc(yi)]+inc(zi)];
baa = p[p[p[inc(xi)]+ yi ]+ zi ];
bba = p[p[p[inc(xi)]+inc(yi)]+ zi ];
bab = p[p[p[inc(xi)]+ yi ]+inc(zi)];
bbb = p[p[p[inc(xi)]+inc(yi)]+inc(zi)];
float x1, x2, y1, y2;
x1 = lerp( grad (aaa, xf , yf , zf),
grad (baa, xf-1, yf , zf),
u);
x2 = lerp( grad (aba, xf , yf-1, zf),
grad (bba, xf-1, yf-1, zf),
u);
y1 = lerp(x1, x2, v);
x1 = lerp( grad (aab, xf , yf , zf-1),
grad (bab, xf-1, yf , zf-1),
u);
x2 = lerp( grad (abb, xf , yf-1, zf-1),
grad (bbb, xf-1, yf-1, zf-1),
u);
y2 = lerp (x1, x2, v);
return lerp (y1, y2, w);
}
这段代码实现了典型的三维柏林噪声,其核心处理逻辑如下:
第1步:构建哈希表(p数组)
p[512] 包含两组 0~255 的数字序列(重复一遍是为了简化取模运算,直接做加法溢出即可)。这组散乱的数据充当了类似随机数发生器的角色。通过层层索引 pp[p[xi]+yi]+zi 这样的嵌套读取,我们将三维坐标转化为特定的网格顶点标识(如 aaa, bbb 等),实现坐标到随机顶点的伪随机映射。
第2步:梯度影响计算(grad函数)
在三维空间中,“梯度”不仅仅是简单的线性斜率,而是表示周围各点影响力的向量。代码预定义了 grads[12] 共12种基础梯度向量。通过 grad 函数,选取对应顶点的梯度向量与当前点到顶点的偏移量向量((xf, yf, zf))进行点积运算(内积/投影)。这一步决定了某个特定顶点在当前点上产生的扰动强度和方向。
第3步:多层级平滑插值(lerp调用)
最后利用 fade 函数算出每个维度的平滑系数 u, v, w。先沿第一个轴向(比如X轴)混合相对的两个梯度值;接着在第二个轴向上混合上一层的结果;最后在第三轴向上完成最终混合。这个过程就是三维插值的具象化体现,如图所示:
非典型简化实现(Shader风格)
除了上述标准的 C++ 结构体实现方式外,在图形着色器(GLSL/HLSL)中常能见到更加紧凑的非典型写法。这种写法省略了庞大的哈希表,直接通过数学公式快速提取高频随机数:
float rand(float2 p){
return frac(sin(dot(p ,float2(12.9898,78.233))) * 43758.5453);
}
float noise(float2 x)
{
float2 i = floor(x);
float2 f = frac(x);
float a = rand(i);
float b = rand(i + float2(1.0, 0.0));
float c = rand(i + float2(0.0, 1.0));
float d = rand(i + float2(1.0, 1.0));
float2 u = f * f * f * (f * (f * 6 - 15) + 10);
float x1 = lerp(a,b,u.x);
float x2 = lerp(c,d,u.x);
return lerp(x1,x2,u.y);
}
这种实现的思路依然符合柏林噪声的核心原则:获取四点 -> 平滑插值。差异仅在于:
1. 哈希函数的构造极为精简,利用正弦波的周期性配合无理数乘法制造看似随机的分布。
2. 由于没有复杂的梯度向量空间变换,它只计算标量影响,因此在某些对视觉细节要求不高的场景中更为轻量。
波形的调整与应用
柏林噪声本身输出的是一个拥有“波浪”特性的随机场。为了在实际应用中(如复杂地图生成)获得更丰富的效果,我们可以套用物理学中对波的调优方法。
第1步:幅值与相位的直接控制
如同调整正弦波 y = A*sin(w*x + φ) + b,我们可以对噪声的输出施加缩放和平移。调幅(改变 A)可以直接让地形变得更高耸陡峭;平移(改变 b)则可以整体抬高海拔线。例如,设置参数 A=0.5, w=2, φ=1, b=0.5 时的波形变化:
第2步:频率叠加(分形技术)
傅里叶变换提示我们,复杂的波可以由多个不同频率的简谐波叠加而成。这在图形学中被称为分形噪声(Fractal Noise)或八度(Octaves)叠加。通过不断将自变量放大(提高频率/倍频),并将多次采样的高频细碎噪声相加:
其作用十分关键:
细化细节:低频大波长负责搭建山脉的整体走势,高频小波长则叠加出岩石嶙峋的细节和微观起伏。
增强真实感:单纯的单次柏林噪声显得过于圆润,经过调频叠加处理后,能够生成极其逼真且连绵不绝的自然地貌。
以上就是基于C++实现柏林噪声算法(Perlin Noise)的详细内容,更多关于C++柏林噪声算法的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









