噪声地形生成(Perlin Noise)

由 Rust + WebAssembly 用分形噪声实时生成无限大的地图

🖱️ 拖拽 平移地图(地图无限延伸) |  把「八度」调到 1,再逐级增加,看细节如何一层层叠加上去

📖 原理说明

游戏里无限延伸的地图(如《我的世界》)通常不是手工绘制的,而是由「噪声函数」实时计算出来的:给定任意坐标,都能立即算出那里的海拔,而且相邻位置的数值平滑过渡。这个示例用 Rust 手写 Perlin 噪声,把多层不同尺度的噪声叠加(分形布朗运动),再按海拔上色、加上山体阴影,得到一张看起来很自然的地图。

🧮算法原理

height = Σi pi · noise(2i · x)fBm:频率每层翻倍,振幅乘以粗糙度 p
Perlin 梯度噪声
把平面划分成单位网格,每个格点随机分配一个梯度方向(由按种子打乱的排列表决定)。对任意一点,计算它到四个角格点的偏移与各自梯度的点积,再平滑插值。因此噪声在整数格点上恰好为 0,在格点之间连续起伏,而且同一个坐标永远得到同一个值。
平滑曲线 fade
插值权重不是直接用线性的 t,而是 6t⁵ − 15t⁴ + 10t³。它在 0 和 1 处的一阶、二阶导数都为 0,相邻网格拼接处不会出现折痕,这是 Ken Perlin 在 2002 年改进版中的关键修改。
分形布朗运动(fBm)
单层噪声只有一种尺度的起伏,看起来像光滑的丘陵。把频率翻倍、振幅减半(粗糙度 0.5)的噪声一层层叠加:第 1 层决定大陆轮廓,后面各层依次加上山脉、丘陵、岩石等越来越细的细节。每一层叫一个「八度」。
着色与山体阴影
海拔低于海平面的画成水,越深颜色越暗;陆地按相对高度依次过渡为沙滩、草地、森林、岩石和雪顶。山体阴影用相邻像素的高度差估算坡度:朝向左上方光源的坡面变亮,背光的坡面变暗,平面地图因此有了立体感。

🔄Rust 与 JavaScript 的分工

  1. JS滑块改变或拖拽平移时,JS 用 requestAnimationFrame 合并请求,每帧最多重新生成一次。
  2. Rust调用 terrain_render(宽, 高, 种子, 缩放, 八度, 粗糙度, 偏移x, 偏移y, 海平面, 阴影)。Rust 为 256,000 个像素逐一计算 fBm 高度,再上色和计算阴影,返回 RGBA 数组。
  3. JSJS 用 putImageData() 把图像画到画布上。

地图没有边界:拖拽只是改变采样坐标的偏移量,任何位置的地形都是当场算出来的,不需要预先生成或存储。换一个种子,就是一个全新的世界。

⚡性能要点

  • 计算量 = 像素数 × 八度数。参考数据(一台 Linux 测试机,Chromium 152,640×400):1 个八度约 12ms,6 个八度约 35~47ms,8 个八度约 69ms。
  • 6 个八度时每帧要算约 150 万次噪声,拖拽时大约 20~30 fps。游戏引擎通常把地形切成区块,只计算新露出来的部分,或者交给 GPU 的着色器并行计算。
  • 每个八度的采样坐标都加了一个不同的偏移,避免所有层的「格点为零」恰好对齐在原点附近,形成可见的规则图案。

源码crates/graphics/terrain/src/lib.rswww/terrain/index.js