← Blog
Procedural GenerationCanvas APIMathTypeScript

Perlinノイズで自然な地形を生成する — ランダムなのに美しい理由

更新 2026年5月27日7 分で読める

このトピックを深掘りする

クリエイティブコーディング

ブラウザでの数学的シミュレーション・自然現象の可視化・生成アートの実装技法

全12本中 4 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

Math.random()ではダメな理由

Math.random() で地形の高さを決めると、隣接するセルの値が無関係になり、ギザギザの意味不明な地形になる。Perlinノイズは隣接値が滑らかに連続する乱数を生成する。

勾配ノイズの原理

2Dの場合、整数格子点にランダムな勾配ベクトルを割り当て、格子内の各点と格子点との距離ベクトルのドット積を補間する:

// 勾配ベクトル(8方向に限定)
const GRAD = [
  [1, 1], [-1, 1], [1, -1], [-1, -1],
  [1, 0], [-1, 0], [0, 1], [0, -1],
]

// ハッシュ関数(格子点→勾配インデックス)
const PERM = new Uint8Array(512)
function initPerm() {
  const p = Array.from({ length: 256 }, (_, i) => i)
  // Fisher-Yatesシャッフル
  for (let i = 255; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[p[i], p[j]] = [p[j], p[i]]
  }
  for (let i = 0; i < 512; i++) PERM[i] = p[i & 255]
}

function fade(t: number): number {
  return t * t * t * (t * (t * 6 - 15) + 10)
}

function lerp(a: number, b: number, t: number): number {
  return a + t * (b - a)
}

fade 関数は5次の滑らかなS字カーブ。Ken Perlinの改良版(2002年)で、1次導関数も2次導関数も境界で0になる。

2D Perlinノイズ

function perlin2d(x: number, y: number): number {
  const xi = Math.floor(x) & 255
  const yi = Math.floor(y) & 255
  const xf = x - Math.floor(x)
  const yf = y - Math.floor(y)

  const u = fade(xf)
  const v = fade(yf)

  const aa = PERM[PERM[xi] + yi]
  const ab = PERM[PERM[xi] + yi + 1]
  const ba = PERM[PERM[xi + 1] + yi]
  const bb = PERM[PERM[xi + 1] + yi + 1]

  function dot(hash: number, dx: number, dy: number): number {
    const g = GRAD[hash & 7]
    return g[0] * dx + g[1] * dy
  }

  const x1 = lerp(dot(aa, xf, yf), dot(ba, xf - 1, yf), u)
  const x2 = lerp(dot(ab, xf, yf - 1), dot(bb, xf - 1, yf - 1), u)

  return lerp(x1, x2, v)
}

返り値は -1〜1。これをそのまま高さマップに使う。

オクターブ合成

単一のPerlinノイズは大きなうねりしか表現できない。複数の周波数を重ね合わせることで、自然な地形のディテールが生まれる:

function fbm(
  x: number,
  y: number,
  octaves: number,
  lacunarity = 2.0,
  persistence = 0.5,
): number {
  let value = 0
  let amplitude = 1
  let frequency = 1
  let maxValue = 0

  for (let i = 0; i < octaves; i++) {
    value += perlin2d(x * frequency, y * frequency) * amplitude
    maxValue += amplitude
    amplitude *= persistence
    frequency *= lacunarity
  }

  return value / maxValue
}
  • lacunarity: 周波数の増加率(2.0で各オクターブが2倍の細かさ)
  • persistence: 振幅の減衰率(0.5で各オクターブが半分の影響力)

4-6オクターブで十分にリアルな地形になる。

3D地形メッシュの描画

PerlinLandscapeではCanvas APIの2Dコンテキストのみで擬似3Dメッシュを描画する。アイソメトリック投影を使う:

function renderTerrain(
  ctx: CanvasRenderingContext2D,
  heightMap: number[][],
  cols: number,
  rows: number,
) {
  const cellSize = 8
  const heightScale = 80

  ctx.strokeStyle = 'rgba(100, 200, 255, 0.6)'
  ctx.lineWidth = 0.5

  // 後ろから前に描画(Painter's Algorithm)
  for (let y = 0; y < rows - 1; y++) {
    for (let x = 0; x < cols - 1; x++) {
      const h = heightMap[y][x]

      // アイソメトリック変換
      const sx = (x - cols / 2) * cellSize + ctx.canvas.width / 2
      const sy = (y - rows / 2) * cellSize * 0.5 - h * heightScale + ctx.canvas.height / 2

      const h2 = heightMap[y][x + 1]
      const sx2 = (x + 1 - cols / 2) * cellSize + ctx.canvas.width / 2
      const sy2 = (y - rows / 2) * cellSize * 0.5 - h2 * heightScale + ctx.canvas.height / 2

      ctx.beginPath()
      ctx.moveTo(sx, sy)
      ctx.lineTo(sx2, sy2)
      ctx.stroke()
    }
  }
}

y方向のスケールを半分にすることで奥行き感を出す。WebGLを使わずともCanvas 2Dで十分に「地形らしく」見せられる。

PerlinNoiseコンポーネントとの違い

PerlinNoiseは2Dノイズをグレースケールの濃淡で表示するだけのデモ。PerlinLandscapeは同じノイズ関数を使いつつ、3D投影と高さマッピングを追加している。同じ数学関数をどう「見せるか」でまったく違う体験になる。

まとめ:Perlinノイズで使った技術と道具

Perlinノイズの応用範囲はゲーム開発全般に及ぶ。p5.jsnoise() 関数はPerlinノイズそのもので、プロトタイピングに最適だ。道具棚の書籍では、プロシージャル生成の基礎からレベルデザインへの応用まで体系的に学べる。