[Next.js #09] Perlin/FBM ノイズで地形を作り、プレイヤーを Raycast で追従

はじめに

今朝書いたノイズの記事を踏まえて、Next.js + React + R3F でノイズから地形を生成し、過去のthree.jsでも実装したレイキャストで地面に張り付く処理を実装してみました。

動画(PC):

前回の記事:

この記事の目的

今回のテーマは -「ノイズを使った地形生成」と「Raycaster による地面追従」-。

本来これは React や Next.js の領域ではないが、 R3F(React Three Fiber) を使うと -Three.js のゲームエンジン的な処理を React 環境の中で実装できる-。

この記事では、以下の2つを実装する:

  1. -FBM(フラクタルノイズ)で自然な地形を生成する-
  2. -Raycaster によって“地形に貼り付いて歩くプレイヤー”を作る-

Unity や Unreal Engine が内部でやっている地形衝突の仕組みを、 Three.js と R3F のみで実現してみた。

1. FBM ノイズとは何か

3D やゲーム開発で自然な地形を作る場合、 「Perlin(または Simplex)ノイズ」だけでは不十分 です。

理由は、単一のノイズは “もこもこした雲模様” にしかならず、 山・谷・尾根のような “地形らしい構造” が生まれにくいからです。

そこで使われるのが FBM(Fractal Brownian Motion)。


FBM = ノイズを “重ねる” ことで自然さを作る手法

FBM は、ノイズを複数回「周波数×振幅(オクターブ)」を変えて重ねることで、 下記のような効果を生みます。

  • 低周波 → 大きくゆっくりした地形のうねり(山/谷の骨格)
  • 高周波 → 細かい起伏や自然な揺らぎ(岩や斜面の細部)

これによって、現実の山に近い “階層構造” が生まれる。


FBMの式(簡易版)

value = Σ ( noise(freq * x, freq * y) * amp )
freq *= 2
amp  *= 0.5
  • freq:周波数(倍にして細かい模様を追加)
  • amp:振幅(半分にして影響度を下げる)

結果として、 低周波の“ゆっくりした変化”と 高周波の“細かいノイズ”が混ざった自然なパターンになる。


今回の実装では Simplex Noise を採用

Perlin ノイズは古典的だが、 simplex-noise は高速で破綻が少なく、ゲーム向き。

2. FBM で PlaneGeometry を「山」に変換

Perlin/FBM ノイズをそのまま描画しても模様にしかならない。 地形として使うには、PlaneGeometry の頂点(position)を直接操作する。

Three.js の PlaneGeometry は、頂点の x / y を格子状に持っているので、 その 各頂点の Z 値を FBM ノイズで更新するだけで山になる。


NoiseTerrain.tsx のポイント(概要)

const geo = new THREE.PlaneGeometry(width, height, segments, segments);
const noise2D = createNoise2D();
const pos = geo.attributes.position;

for (let i = 0; i < pos.count; i++) {
  const x = pos.getX(i);
  const y = pos.getY(i);

  // FBM(5オクターブ)
  const elevation = fbm2(noise2D, x * 0.02, y * 0.02, 5);

  // 山の鋭さを調整する shaping 関数
  const shaped = Math.pow(Math.max(elevation, 0), 1.5);

  // Z の高さを決定
  pos.setZ(i, shaped * 10);
}

pos.needsUpdate = true;
geo.computeVertexNormals();

ポイント解説

頂点の Z を “FBMノイズ” で決定する

FBM は自然な起伏を作れるため、 1回の Perlin とは違い 大地のような凹凸 が生まれる。

pow() で山の形を鋭くする(shaping function)

Math.pow(value, 1.5)
  • 1.0 → 柔らかい丘
  • 1.5 → 山頂が尖る
  • 2.0+ → 火山のような形になる

数学的 shaping は地形生成ではよく使われる。

computeVertexNormals() が絶対に必要

頂点の高さをいじると 法線情報が壊れるため、 陰影がグチャグチャになる。

computeVertexNormals() を呼ぶことで、 三角形の角度から法線を再計算し、自然な影になる。


結果として得られるもの

  • ノイズ模様 → “地形” に変わる
  • 人工的なグリッド感が消える
  • “AI生成のような自然な山” が生まれる
  • FBM と shaping の組み合わせだけで十分ゲーム向きの地形になる

3. Raycast で地面の高さを正確に取る

生成したノイズ地形の上をキャラクターが歩くためには、 「いま立っている位置の地面の高さ(Y座標)」を毎フレーム取得する 必要がある。

Three.js の Raycaster は、そのための標準ツール。

仕組みはシンプルで、

  • origin(レイの発射位置)
  • direction(レイの向き)

を指定すると、 シーン内のオブジェクトと 最初に衝突した点(hit) を返してくれる。


実際の Raycast 処理

raycaster.set(
  new THREE.Vector3(player.x, player.y + 5, player.z), // プレイヤーの真上から
  new THREE.Vector3(0, -1, 0)                           // 下方向へ
);

const hit = raycaster.intersectObject(terrainRef.current);

ポイント解説

1. 「上 → 下」へレイを落とす

地形は上下に凹凸があるため、 プレイヤーの“上から”レイを落とすと安定する。

player.y + 5

でレイの起点を少し上に持ち上げているのはそのため。


2. intersectObject の先頭(hit[0])が最も近い地面

intersectObject() は衝突した三角形を 距離の近い順に並べて返してくれる。

const ground = hit[0];

とすれば “足元の地面” を取得できる。


3. 高さの本体は hit[0].point.y

Raycaster が返す値の中で、 point.y が地面の高さそのもの。

const groundY = hit[0].point.y;

これを使えば、キャラを自然に地形に貼り付けられる。


Raycast は Unity の「RaycastHit」とほぼ同じ

Unity の Physics.Raycast() と Three.js の Raycaster は同じ概念。

  • origin
  • direction
  • hit
  • hit.point.y

この4つが揃えば、 どんな地形上でもキャラを歩かせられる。

Web でも Unity と同じ手法が使える、という点が今回の面白い部分。

4. プレイヤーの位置に高さを適用する

Raycast で地形の高さ(hit[0].point.y)が取得できたら、 その値をプレイヤーの Y 座標に反映させる。

ただし注意点として、 高さを“一気に”変えるとキャラがガクつく。

そこで、Three.js の MathUtils.lerp() を使った “遅れて追従する” 補間 (LERP) が重要になる。


高さ追従の実装(ベストプラクティス)

if (hit.length > 0) {
  const targetY = hit[0].point.y + 0.2;
  ref.current.position.y = THREE.MathUtils.lerp(
    ref.current.position.y,
    targetY,
    delta * 10
  );
}

解説:なぜこの処理が必要なのか

1. 高さを瞬間移動させるとガタつく

地形は FBM で作った細かい凹凸なので、 1フレームごとの差が大きいと 上下に跳ねるような挙動 になる。

Unity でも Terrain で同じ問題が起こる。


2. LERP(補間)で“少し遅れて”追従すると自然になる

lerp(currentY, targetY, delta * 10)

これは、

  • 現在位置 → 目標位置
  • を “10×delta の速度” で近づける処理。

つまり:

  • 地面が急に上がっても、キャラは滑らかに追従
  • 小さな凹凸は吸収され、自然な歩き方に見える

というメリットがある。


3. 結果、Unity の CharacterController 的な動きになる

Unity の CharacterController は内部で「接地補間」をしており、 プレイヤーが地形に貼り付くように作られている。

Three.js + R3F でも Raycast × LERP を組み合わせることで同等の挙動が実現できる。


今回のポイントまとめ

  • Raycast の高さをそのまま使うと不安定
  • LERP でワンクッション置くのが正解
  • ゲームエンジンが裏でやっている処理を自前で実装できた
  • FBM のような細かい地形でも破綻しない

5. WASD と atan2 によるキャラ回転

プレイヤーがどの方向に移動しているかを知るには、 WASD の入力から “移動ベクトル(dir)” を作り、 そのベクトルを 角度(Y軸の回転)に変換 する必要がある。

R3F では useKeyboardControls() で入力を取得し、 dir を算出したあとに atan2 を使えば 「移動方向 → 角度」に一発で変換できる。

キャラ回転のコード(最小構成)

const targetY = Math.atan2(dir.x, dir.z);
ref.current.rotation.y = THREE.MathUtils.lerp(currentY, targetY, delta * 8);

ポイント解説

1. atan2 は「ベクトル → 角度」変換の最短ルート

三角関数を使うと方向角度を出せるが、 一般的にゲーム開発で使われるのは atan2。

Math.atan2(x成分, z成分)

これにより:

  • 斜め
  • カーブした方向

すべての向きが 一つの数学関数で取得できる。


2. lerp で回転を“滑らかに”補間する

THREE.MathUtils.lerp(currentY, targetY, delta * 8)

これは Unity の “Slerp に近い効果” を生む。

  • カクッと即座に向きを変えず
  • ヌルッと自然にキャラクターが向きを変える

TPS視点のゲームでは 必須の処理。


3. React の state ではなく、毎フレーム ref を触るのが正しい

React は UI フレームワークなので、

  • useState
  • useEffect
  • props

は 3Dの位置や回転の更新には向かない。

R3F では、 Three.js のゲームループ(useFrame)が実体であり、 位置・回転は useFrame 内で ref に直接書き込むのが正解。

これにより:

  • 無駄な再レンダリングを防ぎ
  • 物理挙動が安定し
  • Three.js 本来の動作を損なわない

というメリットがある。


結果として得られるもの

  • WASDで移動
  • 移動方向に自動で身体が向く
  • アニメーション(歩く/Idle)とも整合性が取れる
  • Unity の TPS Controller と似た挙動になる

6. 今回の学び

React(R3F)で “ゲームエンジン的な処理” を書くのは難しい

React は UI フレームワークであり、 本来は「毎フレーム更新される世界」を想定していない。

R3F は Three.js を React に載せるための仕組みだが、 キャラ制御・地形追従・Raycast などの リアルタイム挙動はその外側で直接 Three.js を扱う必要がある。

この構造を理解できたのが今回の大きな収穫。


Three.js の仕組みを“実践で”理解できる

  • 頂点変形
  • 法線再計算
  • Raycast
  • transform の更新
  • 毎フレームループ(useFrame)

これらを自分の手で書いたことで、 Three.js を“表面だけでなく中身から理解した”状態になった。


FBM ノイズは「自然地形」の基礎アルゴリズム

1回の Perlin/Simplex ノイズでは模様しか作れないが、 FBM の多層構造(オクターブ) を使うことで ゲーム向けの山・谷・尾根が生成できる。

Unity の Terrain も内部では同じ仕組みを使っている。


Raycast × LERP による地面追従は非常に強力

  • Raycaster で地形の高さを取り
  • LERP で少し遅れて追従させる

この2つを組み合わせることで、 Unity の CharacterController.isGrounded とほぼ同等の挙動を Web 上で再現できた。


Unity や Unreal の裏側を自分で実装して理解できた

今回の流れは、 “ゲームエンジンが内部で隠している苦労” を まるごと手作業で体験したのと同じ。

  • ノイズ地形
  • 衝突判定
  • キャラの追従と回転
  • 移動ベクトル
  • フレーム更新

こうした仕組みを React × Three.js でゼロから構築した 経験は、 確実に大きな財産になる。