基于C++实现柏林噪声算法(Perlin Noise)

周予安
周予安
发布时间:2026-10-13| 2人浏览过
原创

柏林噪声是一种具有连续性的随机函数,通过插值和哈希映射,确保相同输入产生一致输出且相邻点变化平滑。本文以C++为例,拆解其核心算法逻辑,从基础的一维线性插值拓展至经典的三维网格计算,并展示如何通过倍频叠加等波形调整技巧,在程序设计中高效生成逼真的自然地形或纹理。

概述

柏林噪声(Perlin noise)由 Ken Perlin 发明,主要特征是具有函数连续性。在电子游戏中常用于生成具备自然过渡的地形,或在艺术领域生成连续变化的图样。与存在时间种子状态的普通随机函数不同,柏林噪声更像是一个确定性 Hash 函数,具备两大特点:

  • 连续输入得到平滑输出的结果(对邻近输入有加权采样效应)。
  • 完全相同的输入必然返回完全相同的输出。

该算法非常适合需要“连续且可控的随机性”的场景。例如模拟自然地形时,避免地面出现前一步是高山、脚下是深谷、后一步又是高山的剧烈断层。

对比普通随机函数输出的离散跳变,柏林噪声输出的曲线则呈现出自然的连贯波动:

生成原理

要理解柏林噪声的平滑特性,核心在于插值和平滑函数的应用。对于有经验的基础开发者来说,直觉上平滑往往意味着插值运算。下面先从简单的一维情况推导其核心机制,再拓展至复杂的二维和三维场景。

第1步:理解平滑插值基础

在一维空间中,我们预先在坐标系中按固定单位距离散落一组随机数值。假设这些值为 rands[],对应的坐标点为 P1、P2... 。对于区间内的任意一点 P,其噪声值取决于它两侧最近的两个随机节点值(设为 Y1 和 Y2),进行线性插值计算:

  1. 确定 P 所在的网格区间,获取左右边界随机值 Y1 和 Y2。
  2. 计算归一化的位置比例,即 P 点在当前区间的相对位置。
  3. 将相对位置传入平滑函数 F(x),消除线性插值导致的生硬折角。
  4. 使用平滑后的比例 t 进行最终插值:Noise(P) = Y1 + (Y2 - Y1) * F(t)。

这里的平滑函数通常采用五次多项式,如 F(x) = 6x^5 - 15x^4 + 10x^3。它在 0 到 1 区间内不仅保持了单调递增,更在端点处的导数为零,从而保证了斜率的完美衔接:

第2步:拓展至多维空间(二维与三维)

一维噪声可以无缝推广到二维平面。假设目标点 P 落在 a、b、c、d 四个随机点组成的二维网格内。本质上这是三次一维计算的组合:

  1. X轴方向插值:分别在 ab 边和 dc 边上,根据横坐标比例计算出对应点 X1 和 X2 的值。
  2. 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++柏林噪声算法的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。