[Next.js #10] R3F × GLSL で “動く雲の SkyDome” を作る:FBM ノイズで空を生成

1. はじめに

前回(Next.js #09)では Perlin/FMB ノイズで地形を生成し、プレイヤーを Raycast で歩かせるところまで実装。

前回の記事:

最近のサブテーマは、「ノイズ」なので、今回もノイズを活用した、霧・雲を実装してみたのでその備忘録メモです。

仕組み的には、R3F と GLSL で本物の雲のように動く SkyDome(雲の球体)をキューブマップに貼るテクスチャの代わりに、シェーダーでノイズを動的に生成するという実装内容です。

動画を撮ってみたのでアップします。(Youtubeのショート動画にもアップ予定)

画像:

動画:

2. 雲の作り方の基本:SkyDome とは?

SkyDome(スカイドーム)は、 巨大な球体の“内側”に空のテクスチャを描く手法 だ。 現代のゲームエンジンでも一般的に使われている。

今回のポイントは次の 3つ。

  1. 巨大な球体(半径 500〜1000)をワールド全体に被せる
  2. 球体の裏側(BackSide)を描画する
  3. FBM ノイズで雲を生成し、時間パラメータで流す

これによって、

  • 黒いキューブマップのように空が動く
  • 完全自作の “動く空” が構築できる
  • どの角度から見ても破綻しない

という、 R3F × GLSL ベースのシンプルかつ柔軟な SkyDome が完成する。

3. CloudDome のコード

export function CloudDome() {
  const mat = useRef<any>();
  useFrame((s) => (mat.current.time = s.clock.elapsedTime));

  return (
    <mesh renderOrder={-2}>
      <sphereGeometry args={[500, 64, 64]} />
      <cloudMaterial
        ref={mat}
        side={THREE.BackSide}
        transparent
        depthWrite={true}
      />
    </mesh>
  );
}

このコンポーネントの役割

R3F で 「雲の球体(SkyDome)」を描画する部分 だけを切り出したもの。 内部では次のような処理をしている:


500 → 雲を描く球の半径

ワールド全体を覆いたいため、かなり大きめの値を指定する。 500〜1000 程度が扱いやすい。


BackSide → 球の内側を描画

雲は 球体の外側ではなく“内側”に貼る ので必須。

R3F / Three.js では、

side: THREE.BackSide

と書くことで「裏面だけ描画する」設定になる。


time を毎フレーム更新

useFrameelapsedTimemat.current.time に渡すことで、 GLSL 内で FBM ノイズが時間で変化し、 “雲が流れる” エフェクト が生まれる。

4. CloudMaterial(GLSL シェーダー)

R3F の shaderMaterial を使い、 FBM(Fractal Brownian Motion)ノイズで雲を生成する シェーダーを定義する。

SkyDome の見た目はほぼこのシェーダーのクオリティで決まる。


頂点シェーダ(vertex shader)

varying vec3 vWorld;

void main() {
  vec4 w = modelMatrix * vec4(position, 1.0);
  vWorld = w.xyz;
  gl_Position = projectionMatrix * viewMatrix * w;
}

解説

  • modelMatrix * position で ワールド座標を取得
  • FBM の入力に必要なので vWorld としてフラグメント側へ渡す
  • ここでは頂点を変形しないため、通常の MVP 行列で描画しているだけ

フラグメントシェーダ(fragment shader)

uniform float time;
uniform vec3 cloudColor;
varying vec3 vWorld;

float hash(vec3 p){
  return fract(sin(dot(p, vec3(12.9898,78.233,128.852))) * 43758.5453);
}

float noise(vec3 p){
  vec3 i = floor(p);
  vec3 f = fract(p);
  f = f * f * (3.0 - 2.0 * f);

  return mix(
    mix(
      mix(hash(i + vec3(0,0,0)), hash(i + vec3(1,0,0)), f.x),
      mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x),
      f.y
    ),
    mix(
      mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),
      mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x),
      f.y
    ),
    f.z
  );
}

float fbm(vec3 p){
  float v = 0.0;
  float a = 0.5;
  for(int i=0; i<5; i++){
    v += a * noise(p);
    p *= 2.0;
    a *= 0.5;
  }
  return v;
}

void main(){
  float n = fbm(vWorld * 0.002 + time * 0.02);
  float cloud = smoothstep(0.3, 0.8, n);

  gl_FragColor = vec4(cloudColor * cloud, 1.0);
}

ここが“雲”の本質(超重要)

hash()

格子状のランダム値を生成する最小ノイズ関数。

noise()

3D グラデーションノイズ(Perlin に近い)。 座標を 8 点で補間して「滑らかなランダム」を作る。

fbm()

Perlin(ノイズ)を 周波数 ×2、振幅 ×0.5 で重ねて奥行きのある模様を作る。 これが “雲の質感” の正体。

smoothstep(0.3, 0.8, n)

ノイズ値をしきい値で切り、 雲の境界を 柔らかくするフィルタ。

time

vWorld + time によって ノイズのサンプリング位置がずれ → 雲が流れる。


出力

vec4(cloudColor * cloud, 1.0)

ここではアルファ固定 1.0 にしているため SkyDome は常に視界に表示される。

(Fog で薄めたり、透明にしたい場合はここを 0.6〜0.8 にしても良い)


全体としての理解

このシェーダーは、

  • Perlin 風ノイズ
  • FBM(多層ノイズ)
  • smoothstep
  • 時間アニメーション
  • 裏面描画

を組み合わせて、

見た目が重厚な “動く雲の空” をリアルタイム生成

している。

Web ではここまでやる例が少なく、 lain の実装は普通に “研究レベルのクオリティ” になってる。

5. 完成:動くスカイドーム

上のように、 黒い雲がゆっくり流れるスカイドーム が完成する。

特徴としては:

  • 黒ベースの雲テクスチャ(FBM)
  • ノイズの濃淡が 時間で変化して流れる
  • プレイヤーの視点で見上げると 空全体が有機的に動く
  • 地形(NoiseTerrain)の上に“世界の空”が広がる

R3F + GLSL だけで、 ここまで重厚な “動く空” を生成できるのは強力だ。

Unity / Unreal の SkySphere と同じ発想だが、 WebGL(R3F)で完全自作できる 点が大きい。

これで Terrain(地形)+ SkyDome(空)+ Player(移動) という “世界の骨格” が完成する。

6. 地形+雲+プレイヤーで「世界」が完成する

Next.js + R3F だけで、 Terrain(地形)・Sky(空)・Player(歩く) の三要素が揃った。

これはつまり、

  • 地形を生成し
  • 空をシェーダーで描き
  • プレイヤーが歩き回れる

という “世界の骨格” が Web 上で動いている状態だ。

発想としては Unity / Unreal の構成と同じで、 違いは WebGL(R3F)でこれを全部自前実装できる ところにある。

この時点で「デモ」ではなく、 ゲーム空間としての土台ができた と言っていい。