Skip to content

教程:simplex-noise 噪声在地形/资源生成中的实战

一个小故事:你想做“无限草地 + 零散树林”的地图。如果纯靠随机,草地会一块块“斑驳”,树也会挤成团;如果靠手搓函数,参数一多就难以控制。后来你用了噪声函数:只输入坐标,输出一个稳定的“看起来像自然纹理”的值。于是草地起伏自然、树林分布平滑,还能用同样的种子复现。

Simplex Noise 是一种快速、平滑、维度可扩展的噪声算法,适合用于地形高度、热度/湿度图、资源与生物分布、云层/火焰特效等。

1. 安装

bash
npm install simplex-noise

该库为 ESM 友好,ArenaPro 环境可直接导入使用。

2. 最小上手:2D 噪声高度图

ts
import { createNoise2D } from "simplex-noise";

// 可选:控制可复现的随机种子
// import seedrandom from "seedrandom";
// const rng = seedrandom("map-seed-42");
// const noise2D = createNoise2D(rng);

const noise2D = createNoise2D(); // 默认随机种子

// 取样(x,y)坐标,频率 scale 控制“花纹尺寸”
function heightAt(x: number, y: number, scale = 0.05) {
  const n = noise2D(x * scale, y * scale); // 区间 [-1,1]
  // 映射到 [0,1] 再放大到想要的高度
  return (n * 0.5 + 0.5) * 20; // 高度 0..20
}

// 示例:采样一个 128x128 高度图
const H: number[][] = [];
for (let j = 0; j < 128; j++) {
  const row: number[] = [];
  for (let i = 0; i < 128; i++) row.push(heightAt(i, j));
  H.push(row);
}

3. 八度叠加(Fractal Brownian Motion, fBM)

单层噪声像“柔和的起伏”,叠加多层(Octaves)能更丰富自然。

ts
import { createNoise2D } from "simplex-noise";
const noise2D = createNoise2D();

function fbm2D(
  x: number,
  y: number,
  octaves = 4,
  lacunarity = 2.0,
  gain = 0.5,
  baseScale = 0.01
) {
  let amp = 1.0; // 振幅(贡献权重)
  let freq = 1.0; // 频率(采样密度)
  let sum = 0.0;
  let norm = 0.0; // 归一化因子(振幅总和)
  for (let o = 0; o < octaves; o++) {
    const n = noise2D(x * baseScale * freq, y * baseScale * freq); // [-1,1]
    sum += n * amp;
    norm += amp;
    amp *= gain; // 越往高频振幅越小
    freq *= lacunarity; // 频率逐层增大
  }
  return sum / norm; // [-1,1]
}

// 使用:
function heightAt(x: number, y: number) {
  const n = fbm2D(x, y, 5, 2.1, 0.5, 0.02);
  return (n * 0.5 + 0.5) * 30; // 0..30
}

调参建议:

  • octaves ↑ → 细节更多;
  • lacunarity ≈ 2.0 常用;
  • gain ≈ 0.5 常用;
  • baseScale 控制整体纹理大小。

4. 山脊/峡谷:Ridged / Billow 噪声

  • Billow:先取绝对值再线性变换,得到柔软的“鼓包”。
  • Ridged:山脊效果,像“倒扣的 Billow”。
ts
function billow(n: number) {
  return 2 * Math.abs(n) - 1;
} // [-1,1]
function ridged(n: number) {
  return 1 - Math.abs(n);
} // [0,1]

function ridgedFbm2D(x: number, y: number) {
  const n = fbm2D(x, y, 5, 2.0, 0.5, 0.02); // 先 fBM
  const r = ridged(n); // 再塑形
  return r; // [0,1]
}

用途:

  • billow 做云层/沙丘;
  • ridged 做山脊/峡谷。

5. 域扭曲(Domain Warp)

把输入坐标再用一组噪声“扭曲”,能得到更复杂的纹理(如“漩涡、流动感”)。

ts
import { createNoise2D } from "simplex-noise";
const n1 = createNoise2D();
const n2 = createNoise2D();

function domainWarp2D(
  x: number,
  y: number,
  scale = 0.02,
  warpAmp = 15,
  warpScale = 0.05
) {
  const wx = n1(x * warpScale, y * warpScale) * warpAmp;
  const wy = n2(x * warpScale, y * warpScale) * warpAmp;
  const v = n1((x + wx) * scale, (y + wy) * scale); // 被扭曲后的主噪声
  return v; // [-1,1]
}

用途:水流/云层/魔法能量的“流动感”。

6. 让噪声“可平铺”(Tileable Noise)

生成可拼接的平铺纹理,用于无缝地表或天空盒。

思路:把 2D 平面映射到环面(torus)上,对角度使用三角函数构造循环输入。

ts
import { createNoise2D } from "simplex-noise";
const noise2D = createNoise2D();

function tileable2D(u: number, v: number, size = 256, scale = 1.0) {
  // u,v ∈ [0,size) → 映射到 [0, 2π)
  const x = (u / size) * Math.PI * 2;
  const y = (v / size) * Math.PI * 2;
  const nx = Math.cos(x) * scale,
    ny = Math.sin(x) * scale;
  const nz = Math.cos(y) * scale,
    nw = Math.sin(y) * scale;
  // 用 4D 噪声将两个圆周维度“闭合”
  // 如果只引入 2D,可把库换成支持 4D 的版本,或用两次 2D 近似(质量略差)
  // 这里用两次 2D 近似法:
  const a = noise2D(nx + 5.32, ny + 1.73);
  const b = noise2D(nz - 2.11, nw + 9.77);
  return (a + b) * 0.5; // 近似平铺
}

更严格的做法是使用支持 4D 的噪声库,或者把 simplex-noise 源扩展到 4D(成本较高)。

7. 三维噪声:体素/洞穴

体素或体积效果可用 3D 噪声。

ts
import { createNoise3D } from "simplex-noise";
const noise3D = createNoise3D();

function caveDensity(x: number, y: number, z: number, scale = 0.05) {
  // 正值填充、负值空洞,根据阈值生成体素
  return noise3D(x * scale, y * scale, z * scale); // [-1,1]
}

// 例:生成 64^3 的密度场(注意体量)
const SIZE = 64;
const field = new Float32Array(SIZE * SIZE * SIZE);
let p = 0;
for (let k = 0; k < SIZE; k++)
  for (let j = 0; j < SIZE; j++)
    for (let i = 0; i < SIZE; i++) field[p++] = caveDensity(i, j, k);

8. 与游戏要素结合:几个套路

注:本示例以地图尺寸 XYZ = 256 × 64 × 256 进行测试(X/Z 平面 256、Y 高度 64)。请根据你的实际世界尺寸调整参数与边界判断。

  • 地形高度:fBM → 缩放映射 → 平滑坡度限制。
  • 生物群落:用两张噪声作为“热度/湿度图”,判定生物群落类型(草地/沙地/雪地)。
  • 资源点:噪声阈值选点,配合蓝噪声/Poisson Disk 进一步均匀化。
  • 天气与云:域扭曲 + 多层叠加,缓慢变化(时间作为第三维)。
  • 地下结构:3D 噪声密度场 + Marching Cubes 网格化(离线或低频生成)。

8.1 用 voxel.setVoxel 生成最小 2D 高度图地形

思路:用 2D 噪声得到 (x,z) 的地面高度 h,h 以下填充石头/泥土/草,以上填空气。

ts
import { createNoise2D } from "simplex-noise";
const noise2D = createNoise2D(); // 可传入种子以保证可复现

function heightAt(x: number, z: number, scale = 0.05, amp = 32, base = 40) {
  const n = noise2D(x * scale, z * scale); // [-1,1]
  return Math.floor(base + n * amp); // 例如 8..72
}

export function generateChunk2D(
  chunkOriginX: number,
  chunkOriginZ: number,
  CHUNK = 32,
  MAX_Y = 64
) {
  for (let dz = 0; dz < CHUNK; dz++) {
    for (let dx = 0; dx < CHUNK; dx++) {
      const x = chunkOriginX + dx;
      const z = chunkOriginZ + dz;
      const h = heightAt(x, z, 0.05, 32, 40);

      for (let y = 0; y < MAX_Y; y++) {
        if (y < h - 4) voxels.setVoxel(x, y, z, "stone");
        else if (y < h - 1) voxels.setVoxel(x, y, z, "dirt");
        else if (y === h) voxels.setVoxel(x, y, z, "grass");
        else voxels.setVoxel(x, y, z, "air");
      }
    }
  }
}

8.2 fBM 山丘 + 水面

思路:多八度叠加更自然,低处填水形成湖泊/海面。

ts
import { createNoise2D } from "simplex-noise";
const noise2D = createNoise2D();

function fbm2D(
  x: number,
  z: number,
  octaves = 5,
  lac = 2.0,
  gain = 0.5,
  scale = 0.02
) {
  let amp = 1,
    freq = 1,
    sum = 0,
    norm = 0;
  for (let o = 0; o < octaves; o++) {
    sum += noise2D(x * scale * freq, z * scale * freq) * amp;
    norm += amp;
    amp *= gain;
    freq *= lac;
  }
  return sum / norm; // [-1,1]
}

function heightAt(x: number, z: number) {
  const n = fbm2D(x, z, 5, 2.1, 0.5, 0.02);
  return Math.floor(40 + n * 32);
}

export function generateChunkTerrain(
  chunkX: number,
  chunkZ: number,
  CHUNK = 32,
  MAX_Y = 64,
  WATER = 42
) {
  for (let dz = 0; dz < CHUNK; dz++) {
    for (let dx = 0; dx < CHUNK; dx++) {
      const x = chunkX + dx,
        z = chunkZ + dz;
      const h = heightAt(x, z);

      for (let y = 0; y < MAX_Y; y++) {
        let id: string = "air";
        if (y < h - 6) id = "stone";
        else if (y < h - 1) id = "dirt";
        else if (y === h) id = y < WATER ? "sand" : "grass";
        else if (y <= WATER) id = "water";

        voxels.setVoxel(x, y, z, id);
      }
    }
  }
}

8.3 加入 3D 噪声洞穴

思路:用 3D 噪声定义密度,阈值以下改为空气,雕刻出洞穴。

ts
import { createNoise2D, createNoise3D } from "simplex-noise";
const n2 = createNoise2D();
const n3 = createNoise3D();

function heightAt(x: number, z: number) {
  let amp = 1,
    freq = 1,
    sum = 0,
    norm = 0;
  for (let o = 0; o < 5; o++) {
    sum += n2(x * 0.02 * freq, z * 0.02 * freq) * amp;
    norm += amp;
    amp *= 0.5;
    freq *= 2.0;
  }
  return Math.floor(40 + (sum / norm) * 32);
}

function caveDensity(x: number, y: number, z: number) {
  const base = n3(x * 0.05, y * 0.05, z * 0.05);
  const detail = n3(x * 0.12, y * 0.12, z * 0.12) * 0.5;
  return base * 0.8 + detail - 0.2; // <0 则挖空
}

export function generateChunkWithCaves(
  chunkX: number,
  chunkZ: number,
  CHUNK = 32,
  MAX_Y = 64,
  WATER = 42
) {
  for (let dz = 0; dz < CHUNK; dz++) {
    for (let dx = 0; dx < CHUNK; dx++) {
      const x = chunkX + dx,
        z = chunkZ + dz;
      const h = heightAt(x, z);

      for (let y = 0; y < MAX_Y; y++) {
        let id: string = "air";
        if (y < h - 6) id = "stone";
        else if (y < h - 1) id = "dirt";
        else if (y === h) id = y < WATER ? "sand" : "grass";
        else if (y <= WATER) id = "water";

        // 在地表以下雕刻洞穴
        if (id !== "air" && y < h - 1) {
          const dens = caveDensity(x, y, z);
          if (dens < 0) id = "air";
        }
        voxels.setVoxel(x, y, z, id);
      }
    }
  }
}

9. 性能与实践建议

  • 频繁采样要小心:把 scale、octaves 参数固定后,批量采样更高效;
  • 控制分辨率:渲染纹理/高度图先低分辨率,再做插值/细化,平衡效果与成本;
  • 复现性:配合 seedrandom 保证种子一致;
  • 与 gl-matrix 配合:把噪声值映射到 mat4/quat 的变换上,做风摆/波浪/浮动物体。

10. 参考资料

神岛实验室