[Next.js #34] Procedural Biome Planet — Three.js × GLSLでノイズ惑星に海・森・雪・大気を与える

はじめに

今回は、今朝公開した [Noise 入門 #33] Procedural Biome と大気散乱 の内容をもとに、Next.js プロジェクト内で動かす実験デモとして、ノイズ惑星に海・砂浜・森・雪・岩肌・大気を与える実装をまとめます。

理論面では Height / Slope / Fresnel を使って惑星らしい見た目を作る話でしたが、今回の Next.js 側ではそれをそのまま記事で終わらせず、実際に触って調整できるデモとして public 配下に落とし込みました。

位置づけとしては、こんな流れです。

  • 理論面: Noise 入門 #33 の Procedural Biome と大気散乱
  • 実装面: Next.js プロジェクト内での Three.js + GLSL デモ化
  • 今回の着地: GUI で色・地形・大気・地形の流れまで調整できるノイズ惑星

なお、今回も実行環境は Next.js プロジェクト内ですが、構成としては React コンポーネント主体ではなく、public 配下で動かすほぼバニラ Three.js + GLSL です。 そのため、Next.js のページ設計そのものよりも、Next.js を足場にした shader 実験の記録に近い内容になります。

動画(Youtube):

動画(PC):

元になった記事

  • [Noise 入門 #33] Procedural Biome と大気散乱 — ノイズ惑星に海と森、そして「青い空」を纏わせる
  • [Next.js #33] Interactive Noise Sphere — Three.js × GLSLで球体ノイズにクリック波紋を加える

今回作ったもの

今回のデモでは、次の要素を組み合わせました。

  • SphereGeometry を使った球体メッシュ
  • FBM ベースの 3D ノイズによる地形生成
  • 高さ(Height)による海・砂浜・森・雪の塗り分け
  • 傾斜(Slope)による岩肌ブレンド
  • ランバート拡散反射による陰影
  • Fresnel による大気表現
  • lil-gui による各種パラメータ調整
  • 地形ノイズの流れ方向 / 速度の GUI 制御

見た目としては、無機質なノイズ球から、海や森を持つ惑星へ進化した球体という構成です。 単にノイズで凹凸を作るだけではなく、色のルール・傾斜のルール・視線依存の大気を重ねることで、見た目の説得力を上げています。

前回との違い

[Next.js #33] では、球体ノイズ地形に対して クリック波紋 を加えるインタラクティブデモを作りました。 あちらは「球体に触れる」「リングが広がる」というイベント駆動の表現が主役でした。

一方、今回はインタラクションの中心がクリックではなく、見た目そのものの構造化 にあります。

  • 前回: 球体ノイズ + クリック波紋 + 反応
  • 今回: 球体ノイズ + バイオーム + 岩肌 + 大気 + GUI 調整

つまり今回は、 “触る惑星” から “見た目を設計する惑星” へ進んだ回 と言えます。

実装の流れ

1. 球体ノイズ地形をベースにする

まずは SphereGeometry を用意し、Vertex Shader 側で 3D ノイズを使って球体を変形します。

float noiseVal = fbm(pos * 1.2);
vec3 displacedPos = position + normal * noiseVal * uDisplacement;

ここで重要なのは、平面ノイズではなく 球体の 3D 座標ベース でノイズを評価していることです。 これにより、UV の継ぎ目に依存しない、シームレスな球体地形を作れます。

また、uDisplacement を uniform として切り出しているので、GUI から隆起量を直接調整できます。

2. Height ベースでバイオームを塗り分ける

Fragment Shader では、まず vHeight を使ってベースカラーを決めています。

float waterMask = smoothstep(-0.25, -0.02, vHeight);
vec3 color = mix(uWaterDeep, uWaterShallow, waterMask);
color = mix(color, uSand,   smoothstep(-0.02, 0.06, vHeight));
color = mix(color, uForest, smoothstep(0.08, 0.32, vHeight));
color = mix(color, uSnow,   smoothstep(0.42, 0.62, vHeight));

流れとしては、

  • まず深海色で初期化
  • 高さに応じて浅瀬を混ぜる
  • さらに砂浜を上塗りする
  • その上に森をのせる
  • 最後に高所へ雪をのせる

という順です。

この「下から上に色を積み上げる」構造にすると、境界が比較的わかりやすく、しかも smoothstep によって自然な遷移が作れます。

3. 傾斜で岩肌を出す

高さだけで色を決めると、急斜面にも森や雪がべったり張りついてしまいます。 そこで、法線と上方向ベクトルの内積を使って Slope を計算し、崖っぽい場所に岩肌を出しています。

vec3 upVector = normalize(vPosition);
float slope = max(dot(normal, upVector), 0.0);
float flatMask = smoothstep(0.4, 0.6, slope);
color = mix(uRock, color, flatMask);

平坦な面ほど元のバイオーム色を残し、 急斜面ほど岩肌に寄せることで、地形全体がかなり引き締まります。

この段階で、 ただ色分けしただけの球体 から 地形のルールを持った惑星 に一歩進みます。

4. ライティングで立体感を加える

次に、簡易的な拡散反射で地形に陰影をつけます。

vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float diff = max(dot(normal, lightDir), 0.0);
vec3 lighting = vec3(0.18) + diff * vec3(0.95);
color *= lighting;

ここでは複雑な PBR をやっているわけではなく、まずは 見た目が破綻しにくい最低限の陰影 を与えることを優先しています。

暗部を完全に潰さずに少し持ち上げているので、 海・森・砂・岩の違いが見えやすくなります。

さらに今回は夜側の落ち込みも少し補正しています。

float night = smoothstep(0.25, 0.0, diff);
color = mix(color, color * vec3(0.2, 0.25, 0.35), night * 0.6);

この処理を入れることで、ただ暗くなるだけでなく、 夜側が少し青黒く沈む惑星らしい見え方 になります。

5. Fresnel で大気を重ねる

最後に、大気の薄い縁取りを加えています。

vec3 cameraPos = vec3(0.0, 0.0, 8.0);
vec3 viewDir = normalize(cameraPos - vPosition);
float fresnel = max(1.0 - dot(normal, viewDir), 0.0);
float atmosphereIntensity = pow(fresnel, uAtmospherePower);

color = mix(color, color + uAtmosphereColor * 0.35, atmosphereIntensity);

ここでは、視線と法線の関係から フチに行くほど強くなる値 を作り、 そこへ青系の色を重ねています。

初期段階では単純加算にしていましたが、青が強く出すぎたため、最終的には mix ベースにして少し抑えました。 この調整で、惑星本体の色を殺さずに大気だけを残す バランスになっています。

GUI で触れるようにした項目

今回のデモでは、lil-gui で以下を調整できるようにしました。

Terrain Generation

  • Displacement
  • Terrain Speed
  • Flow X
  • Flow Y
  • Flow Z

Biome Colors

  • Water Deep
  • Water Shallow
  • Sand
  • Forest
  • Snow
  • Rock

Atmosphere

  • Color
  • Power (Thinness)

Render Settings

  • wireframe
  • Rotation Speed

今回特に面白かったのは、地形ノイズの流れ方向を vec3 で持たせたことです。

vec3 pos = position + uTerrainFlowDir * uTime * uTerrainSpeed;

これにより、

  • 静的な惑星
  • 少しだけうごめく惑星
  • X 方向へ流れる地形
  • 斜め方向に流れる地形

といった差を、同じ shader のまま比較できるようになりました。

もっとも、これは「惑星の自転」そのものではなく、 ノイズ入力空間のドリフト に近い表現です。 そのため、常用するというよりは、実験用パラメータとして公開しておく くらいがちょうどよいと感じています。

実装中に調整した点

今回、見た目をかなり触り直しました。

1. 青が強すぎた

最初は海色と大気色が前に出すぎて、 ほぼ「青いノイズ球」みたいな見え方になっていました。

そこで、

  • 水色の彩度を少し落とす
  • 大気色を少し鈍くする
  • 大気の合成を加算から mix に寄せる

といった方向で調整しています。

2. 雪が出なかった

最初の雪帯は上側に寄りすぎていて、ほとんど見えていませんでした。

color = mix(color, uSnow, smoothstep(0.42, 0.62, vHeight));

のように閾値を少し下げたことで、 高所に雪が見えるようになり、地形の読みやすさが上がりました。

3. Flow Direction の uniform が噛み合っていなかった

途中で uTerrainFlowDir を触れるようにした際、 JavaScript 側では Vector3、shader 側では float になっていて、完全に不整合でした。

最終的には、

  • JS 側: THREE.Vector3
  • GLSL 側: uniform vec3
  • GUI 側: Flow X / Y / Z

で統一し直しています。

このあたりは、GUI を増やす時ほど uniform の型をちゃんと揃える必要がある と再確認した部分でした。

今回の構成

今回も Next.js のページコンポーネントではなく、 public/ProceduralBiomeAndAtmosphericScattering/ 配下に

  • index.html
  • app.js
  • style.css

を置く、ほぼバニラ構成です。

このやり方だと、

  • React 側の都合に引っ張られにくい
  • shader 実験をそのまま置ける
  • 記事とデモをすばやく接続しやすい

という利点があります。

個人的には、ノイズ記事を書いた直後にその内容を public デモへ落とし込む という流れと相性が良いです。

まとめ

今回は、[Noise 入門 #33] Procedural Biome と大気散乱 の内容を、Next.js プロジェクト内の実デモとして接続しました。

実装した要素は次の通りです。

  • FBM による球体地形
  • Height による海・砂浜・森・雪のバイオーム分岐
  • Slope による岩肌ブレンド
  • ライティングによる陰影
  • Fresnel による大気表現
  • GUI による地形・色・大気・流れ方向の調整

前回の [Next.js #33] が「球体ノイズへ触れる回」だったとすれば、 今回は 「球体ノイズを惑星として設計する回」 でした。

単にノイズを表示するだけでなく、

  • どう色を割り当てるか
  • どこを岩にするか
  • どこに大気を乗せるか
  • どこまで GUI で触らせるか

まで含めて考えることで、 Three.js + GLSL の球体ノイズ表現が一段階広がった感触があります。

次はこの惑星に対して、

  • 雲レイヤーを追加する
  • 夜側の表現をもう少し強化する
  • 月や背景星を追加する
  • 複数惑星へ拡張する

といった方向へ進めても面白そうです。