每日编程挑战:Perlin噪声地形生成

今天实现了经典的Perlin噪声算法,用于生成自然的2D地形高度图。Perlin噪声是程序化内容生成中最基础也最重要的算法之一,广泛应用于游戏地形、纹理生成和视觉效果制作。

项目概述

  • 实现语言:C++17
  • 算法核心:2D Perlin Noise + Octave叠加
  • 输出格式:512×512 和 1024×1024 PNG灰度高度图
  • 开发时长:7分钟(一次性编译成功)

Perlin噪声算法原理

Perlin噪声由Ken Perlin在1983年发明,用于解决计算机图形学中自然纹理生成的问题。该算法的核心思想是在网格点上定义随机梯度向量,然后通过平滑插值计算任意点的噪声值。

算法流程

  1. 网格定位

    • 将输入坐标映射到整数网格
    • 计算点在网格单元内的相对位置
  2. 梯度向量

    • 为每个网格点分配随机梯度向量
    • 计算输入点到四个角点的距离向量
    • 计算距离向量与梯度向量的点积
  3. 平滑插值

    • 使用fade函数对相对坐标进行平滑处理
    • 进行双线性插值得到最终噪声值

核心代码实现

1. Perlin噪声基础函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PerlinNoise {
private:
std::vector<int> permutation; // 置换表

// Fade函数:6t^5 - 15t^4 + 10t^3
double fade(double t) const {
return t * t * t * (t * (t * 6 - 15) + 10);
}

// 线性插值
double lerp(double t, double a, double b) const {
return a + t * (b - a);
}

// 梯度函数(8个方向)
double grad(int hash, double x, double y) const {
int h = hash & 7;
double u = h < 4 ? x : y;
double v = h < 4 ? y : x;
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
}

2. 2D噪声函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
double noise(double x, double y) const {
// 找到单位网格
int X = static_cast<int>(std::floor(x)) & 255;
int Y = static_cast<int>(std::floor(y)) & 255;

// 计算相对位置
x -= std::floor(x);
y -= std::floor(y);

// 应用fade函数
double u = fade(x);
double v = fade(y);

// 哈希四个角点
int aa = permutation[permutation[X] + Y];
int ab = permutation[permutation[X] + Y + 1];
int ba = permutation[permutation[X + 1] + Y];
int bb = permutation[permutation[X + 1] + Y + 1];

// 双线性插值
return lerp(v,
lerp(u, grad(aa, x, y), grad(ba, x - 1, y)),
lerp(u, grad(ab, x, y - 1), grad(bb, x - 1, y - 1))
);
}

3. Octave叠加技术

通过叠加多个不同频率和振幅的噪声层,可以生成更丰富的细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
double octaveNoise(double x, double y, int octaves, double persistence) {
double total = 0.0;
double frequency = 1.0;
double amplitude = 1.0;
double maxValue = 0.0;

for (int i = 0; i < octaves; i++) {
total += noise(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence; // 振幅衰减
frequency *= 2.0; // 频率加倍
}

return total / maxValue; // 归一化到[-1, 1]
}

关键参数说明

scale(缩放因子)

  • 作用:控制噪声特征的大小
  • 当前值:10.0
  • 效果:值越大,地形特征越大,变化越平缓

octaves(叠加层数)

  • 作用:控制细节层次的数量
  • 当前值:6层
  • 效果:层数越多,细节越丰富,但计算量增加

persistence(持久度)

  • 作用:控制高频细节的衰减速度
  • 当前值:0.5
  • 效果
    • 值接近1:高频细节明显,地形粗糙
    • 值接近0:高频细节模糊,地形平滑

数学细节

Fade函数推导

Ken Perlin在2002年提出的改进fade函数:

f(t) = 6t⁵ - 15t⁴ + 10t³

这个函数的特点:

  • f(0) = 0f(1) = 1(边界值正确)
  • f’(0) = 0f’(1) = 0(一阶导数为零,保证C¹连续)
  • f’’(0) = 0f’’(1) = 0(二阶导数为零,保证C²连续)

这些性质确保了噪声在网格边界处的平滑过渡。

梯度向量选择

使用8个方向的单位梯度向量:

1
2
(1, 0), (-1, 0), (0, 1), (0, -1)
(1, 1), (-1, 1), (1, -1), (-1, -1)

通过哈希值选择梯度向量,保证随机性和可重复性。

可视化效果

生成的地形高度图使用灰度表示:

  • 黑色区域(0):低地、谷地
  • 灰色区域(中间值):平原、丘陵
  • 白色区域(255):高地、山峰

通过多层octave叠加:

  • 第1层:大尺度地形起伏(基础形状)
  • 第2-3层:中等尺度的山丘和谷地
  • 第4-6层:细节纹理和小尺度变化

性能分析

  • 时间复杂度:O(n²) 其中n为图像边长
  • 空间复杂度:O(1)(除了输出图像)
  • 计算量:每像素约 8×octaves 次噪声采样
  • 优化潜力
    • SIMD向量化
    • 多线程并行生成
    • GPU计算着色器加速

实际应用场景

1. 游戏地形生成

1
2
3
4
5
6
7
8
// Minecraft风格的无限地形
for (int x = 0; x < worldWidth; x++) {
for (int z = 0; z < worldDepth; z++) {
double height = perlin.octaveNoise(x * 0.01, z * 0.01, 6, 0.5);
int blockHeight = static_cast<int>((height + 1.0) * 64);
generateColumn(x, z, blockHeight);
}
}

2. 纹理生成

1
2
3
4
5
6
7
// 生成云层纹理
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
double cloud = perlin.octaveNoise(x * 0.02, y * 0.02, 4, 0.6);
pixels[y][x] = (cloud + 1.0) * 0.5; // 映射到[0, 1]
}
}

3. 动态效果

1
2
3
4
5
6
7
8
// 时间维度的动态噪声
double time = currentTime * 0.1;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
double wave = perlin.noise(x * 0.05, y * 0.05 + time);
animatePixel(x, y, wave);
}
}

扩展方向

1. 3D Perlin噪声

扩展到三维空间,用于体积云、3D地形等:

1
double noise(double x, double y, double z);

2. Simplex噪声

Ken Perlin在2001年提出的改进算法,在高维空间性能更好。

3. 地形着色

根据高度映射不同颜色:

1
2
3
4
5
6
7
Color getTerrainColor(double height) {
if (height < 0.3) return BLUE; // 水
if (height < 0.4) return YELLOW; // 沙滩
if (height < 0.7) return GREEN; // 草地
if (height < 0.9) return BROWN; // 山地
return WHITE; // 雪山
}

4. 导出3D网格

将高度图转换为OBJ模型:

1
2
3
4
5
6
7
8
9
10
void exportToOBJ(const std::vector<std::vector<double>>& heightMap) {
// 生成顶点
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
double h = heightMap[y][x];
vertices.push_back({x, h * maxHeight, y});
}
}
// 生成三角形面...
}

技术收获

通过实现Perlin噪声算法,深入理解了:

  1. 程序化生成的核心思想和实现方法
  2. 噪声函数在图形学中的重要应用
  3. 频率域叠加(octave)产生复杂细节的原理
  4. 平滑插值函数的设计和数学性质
  5. 哈希置换表在随机数生成中的应用

开发经验

这次实现的亮点:

  • 一次性编译成功:代码结构清晰,逻辑正确
  • 参数化设计:易于调整效果
  • 高效实现:使用STB库简化PNG输出
  • 可扩展性:为后续3D扩展留出接口

后续计划

  1. 实现3D Perlin噪声:支持体积生成
  2. 添加实时预览:使用OpenGL渲染
  3. 参数UI界面:实时调整查看效果
  4. 性能优化:GPU计算着色器加速
  5. 导出多种格式:OBJ、PLY、STL等

项目源码已托管至GitHubgithub-daily-coding-practice/2026/02/02-16-perlin-noise-terrain

图床链接perlin-terrain-2026-02-16/terrain_512.png

备注:本文为2026年2月16日”每日编程挑战”系列的第7篇文章。系列旨在通过每日小项目巩固计算机图形学和程序化生成基础知识。今天的项目实现了经典的Perlin噪声算法,为后续地形生成和纹理合成打下基础。