[Noise 入門 #40] 第4集完結 — 太陽系を錬成する(Procedural Solar System)

はじめに

Noise 入門シリーズ第40回、そして第4集「Three.js編」の完結編です。

これまで私たちは、ノイズという単なる「乱数の配列」から、地形、バイオーム、生きた雲海、荒れ狂う嵐、文明の灯火、揺らめくオーロラ、そして美しい環(リング)を錬成してきました。これにて、一つの「星」を構成するためのパーツはすべて揃いました。

今回は視点をさらに引き上げます。 これまで作ってきたすべてのシェーダーとオブジェクトを統合し、複数のプロシージャル惑星がそれぞれの軌道を描く「ミニ太陽系(Procedural Solar System)」を Three.js 上に展開します。ノイズが創り出した広大な宇宙を、自由に飛び回る準備をしましょう。

1. 太陽系の設計図 — オブジェクト指向で星を管理する

これまでの連載で、私たちは一つの「星」に対して、地形、海、大気、雲、そして環など、様々なプロシージャル要素を追加してきました。
しかし、それらを複数個同時に描画して「太陽系」を作ろうとした瞬間、エンジニアリング上の壁に直面します。

それは、コードの肥大化とUniform変数の管理破綻です。

メインのスクリプトにベタ書きのまま実装すると、星Aの uTime、星Bの uColor、星Cの uSeed……と、毎フレームの更新処理(requestAnimationFrame内)が瞬く間にスパゲッティ状態になります。これを防ぐため、星を生成・管理する処理をクラスとしてカプセル化(オブジェクト指向)します。

ProceduralPlanet クラスの構築

以下が、プロシージャルな惑星を管理するための基本設計図です。 ポイントは、Three.jsの THREE.Group を用いて、星の本体や雲、環などの複数のMeshをひとまとめ(コンテナ化)にして扱うことです。

import * as THREE from 'three';

export class ProceduralPlanet {
  constructor(radius, orbitRadius, orbitSpeed, type) {
    this.orbitRadius = orbitRadius;
    this.orbitSpeed = orbitSpeed;
    this.angle = Math.random() * Math.PI * 2; // 初期位置をランダムな角度に

    // 惑星を構成するすべての要素をまとめるコンテナ
    this.group = new THREE.Group();

    // type(地球型、ガス惑星、氷の星など)に応じて
    // シェーダーマテリアルやパラメータ(Seed値、Colorなど)を生成
    this.mesh = this.createPlanetMesh(radius, type);
    this.group.add(this.mesh);

    // 雲や大気のレイヤーを追加
    this.clouds = this.createCloudsMesh(radius * 1.02);
    this.group.add(this.clouds);

    // ... 必要に応じて環(Ring)や大気散乱(Atmosphere)のMeshも group に追加
  }

  // シーンへ追加するためのメソッド
  addToScene(scene) {
    scene.add(this.group);
  }

  // 毎フレーム呼ばれる更新処理
  update(deltaTime, time) {
    // 1. 公転(軌道計算)
    this.angle += this.orbitSpeed * deltaTime;
    this.group.position.x = Math.cos(this.angle) * this.orbitRadius;
    this.group.position.z = Math.sin(this.angle) * this.orbitRadius;

    // 2. 自転(グループ全体、または個別のMeshを回す)
    this.mesh.rotation.y += 0.1 * deltaTime;
    this.clouds.rotation.y += 0.15 * deltaTime; // 雲は少し速く回すなど

    // 3. シェーダーのUniform更新(時間経過によるノイズの変化)
    this.mesh.material.uniforms.uTime.value = time;
    this.clouds.material.uniforms.uTime.value = time;
  }

  // Mesh生成のダミーメソッド(これまでの連載のShaderMaterialを組み込む場所)
  createPlanetMesh(radius, type) {
    // ... ShaderMaterialの実装
  }
  createCloudsMesh(radius) {
    // ... ShaderMaterialの実装
  }
}

軌道計算とカプセル化の利点

このクラス設計には、描画において極めて重要な利点があります。

  • THREE.Group による座標の同期: this.group.position を動かすだけで、惑星本体、雲、大気、環など、属するすべてのレイヤーが破綻することなく一緒に移動してくれます。
  • Uniform変数の独立性: 各インスタンスが独自の Material を持つため、一つの星のパラメータ(色やノイズのSeed値など)を変えても他の星のシェーダーに影響を与えません。
  • 円軌道の数学: update メソッド内で行っているのは、三角関数を用いたシンプルな円軌道運動です。軌道半径を $R$、現在の角度を $\theta$ としたとき、惑星のXZ平面上の座標 $(x, z)$ は以下の式で計算されます。 $$x = R \cos(\theta)$$ $$z = R \sin(\theta)$$ これにフレーム間の差分時間(deltaTime)を掛けることで、PCのフレームレートに依存しない一定速度の滑らかな公転を実現しています。

この設計基盤があれば、あとはパラメータ(半径、公転軌道、自転速度、生態系のタイプなど)を変えて new ProceduralPlanet(…) を実行するだけで、無限のバリエーションを持つ星々を宇宙空間へ配置していくことができます。

2. 太陽の錬成 — 宇宙を照らす「光源」

太陽系の中心には、すべてを統べる圧倒的なエネルギーの源、「恒星(太陽)」が必要です。 ただ明るいだけの球体を置くのではなく、ノイズの力で「生きたプラズマ」を錬成し、さらにThree.jsのシステムとして周囲の星々を物理的に照らし出しましょう。

プロシージャルな恒星の表面(Shader)

第3集の [Noise 入門 #21] で、私たちは4D Simplex Noise、FBM、そしてDomain Warpingを掛け合わせて「燃え盛るプロシージャルな炎」を作りました。

あの時は2D平面(Plane)に描画していましたが、今回はそのアルゴリズムをそのまま SphereGeometry(球体)の表面にマッピングします。

時間(uTime)とともに4Dノイズの第4次元を進めることで、ドロドロと脈打つプラズマの対流や、プロミネンスのような黒点のうねりを極めてリアルに表現できます。太陽自身は光を放っているため、Three.jsの標準的なライティング(陰影)の影響を受けないよう、光の計算を行わない純粋な ShaderMaterial として実装します。

PointLight による「真の光源」の配置

見た目が太陽になっても、Three.jsの空間内では自ら光を放って他のオブジェクトを照らすことはできません。そこで、太陽のMeshと全く同じ座標に THREE.PointLight(点光源)を配置します。

import * as THREE from 'three';

// 1. 太陽のメッシュ(プラズマ表面のShader)を錬成
const sunGeometry = new THREE.SphereGeometry(20, 64, 64);
const sunMaterial = new THREE.ShaderMaterial({
  vertexShader: sunVertexShader,
  fragmentShader: sunFragmentShader,
  uniforms: {
    uTime: { value: 0.0 }
  }
});
const sunMesh = new THREE.Mesh(sunGeometry, sunMaterial);
scene.add(sunMesh);

// 2. 太陽の中心に「全方位を照らす光」を配置
// PointLight(色, 強度, 距離, 減衰率)
const sunLight = new THREE.PointLight(0xffeedd, 3.0, 2000, 1.5);
sunMesh.add(sunLight); // 太陽のメッシュの子要素にすることで座標を同期

光が織りなす「ターミネーター(昼夜の境界線)」

この PointLight が宇宙空間の(座標 $(0,0,0)$)に配置されることで、これまでの連載で積み上げてきた各惑星のシェーダーが完全に機能し始めます。

  • 光と影のコントラスト: 光源の方向ベクトル(Light Direction)と、#27で再計算した惑星表面の法線(Normal)の内積を取ることで、物理的に正しいハイライトと深い影が落ちます。
  • 大気散乱の覚醒: #33で実装したフレネル効果による大気(Atmosphere)が、太陽の光を受ける側でのみ青白く輝き、リアリティが爆発します。
  • 文明の灯火の浮かび上がり: そして何より美しいのが、#37で作った「Procedural Night Lights」です。光が当たらない夜側に差し掛かった瞬間、Voronoi Noiseで構築された都市の明かりがジワジワと浮かび上がり、昼夜の境界線(ターミネーター)のドラマチックな景色を描き出します。

太陽という一つの強烈な光源を置くことで、ノイズで生成した惑星たちが「宇宙の法則」の中に組み込まれ、初めて一つのシステムとして息づくのです。

3. 多様性のパレット — 異なる環境の星々を配置する

太陽を中心に、いよいよ個性豊かな星々を配置していきます。 プロシージャル生成の最大の魅力は、数式を少し調整するだけで無限のバリエーションを生み出せることです。同じノイズ関数を使っていても、パラメータ一つで灼熱の地獄から極寒の氷の星まで、全く異なる環境(バイオーム)を錬成できます。

パラメータが描く「4つの星の世界」

先ほど作成した ProceduralPlanet クラスのインスタンスを生成する際に、引数(タイプや色、ノイズの周波数など)を渡して、太陽系に多様な星々を配置しましょう。

  • 🔥 第1惑星(灼熱の岩石星) 太陽に最も近い軌道を回る、海を持たない過酷な星です。 [Noise 入門 #13] で学んだ Voronoi Noise の「細胞の境界線(F2 - F1)」を活用し、地表を覆う巨大なひび割れを生成します。その溝の底に赤く発光するマグマ(emissive な表現)を流し込み、太陽の強烈な光に焼かれる荒涼とした岩肌を表現します。大気は薄く、雲は存在しません。

  • 🌍 第2惑星(地球型惑星) 太陽から適度な距離(ハビタブルゾーン)にある、生命の息吹を感じさせる星。 [Noise 入門 #33] から [#38] までの技術の結晶です。FBMノイズを用いた海と森のバイオーム分割、青く輝く大気散乱(Fresnel)、4Dノイズで流れる雲海、局所的なCurl Noiseによる台風、極地の夜空を彩るオーロラ、そして夜の闇に浮かび上がるVoronoiの都市網。私たちがこれまで錬成してきたすべての技術が、この一つの星に集約されています。

  • 🪐 第3惑星(巨大ガス惑星) 巨大な質量を持ち、分厚い大気と美しい環(リング)を纏う星。 岩石の地表を持たず、全体が流体のようにうねるガスで構成されています。ここでは [Noise 入門 #06] で学んだ Domain Warping を応用します。ノイズの座標系をY軸方向(緯度方向)に強く引き伸ばす(Stretching)ことで、木星に見られるような特徴的な横縞模様を作り出します。さらに [#39] で実装した塵と氷のリングを赤道に配置し、圧倒的な存在感を放ちます。

  • ❄️ 第4惑星(極寒の氷星) 太陽から遠く離れた、凍てつく暗い星。 [Noise 入門 #10] で地形生成に用いた Ridge Noise(尾根ノイズ)のパラメータを極端に尖らせ、地表全体を鋭く険しい氷の山脈で覆い尽くします。海は完全に凍結し、大気散乱の色(Scattering Color)を青から白〜薄いシアンに変更することで、冷え切った空気の層を表現します。

実装のイメージ

これらの星をシーンに配置するコードは、以下のように非常にシンプルになります。

// 太陽系を構成する惑星たちを配列で管理
const planets = [];

// new ProceduralPlanet(半径, 軌道半径, 公転速度, 'タイプ')
planets.push(new ProceduralPlanet(2, 30, 0.5, 'LAVA'));    // 第1惑星
planets.push(new ProceduralPlanet(3, 50, 0.3, 'EARTH'));   // 第2惑星
planets.push(new ProceduralPlanet(8, 90, 0.1, 'GAS'));     // 第3惑星
planets.push(new ProceduralPlanet(2.5, 140, 0.05, 'ICE')); // 第4惑星

// シーンに追加
planets.forEach(p => p.addToScene(scene));

この配列(planets)を requestAnimationFrame のループ内で回し、毎フレーム p.update(deltaTime, time) を呼び出すだけで、太陽系全体が静かに動き始めます。

4. 宇宙の時計を動かす — 軌道計算と時間の同期

星々を空間に配置しただけでは、まだ美しい「ジオラマ」に過ぎません。 この宇宙に生命を吹き込むための最後のピース、それが「時間(Time)」です。Three.js の requestAnimationFrame を用いて、すべての天体の座標とシェーダーの時間を同期させ、システム全体を静かに動かしましょう。

アニメーションループの構築

Three.js には時間を管理するための THREE.Clock という便利なクラスが用意されています。これを使って「前フレームからの経過時間(deltaTime)」と「起動からの総経過時間(elapsedTime)」を取得し、すべてのオブジェクトの更新処理に流し込みます。

import * as THREE from 'three';

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);

  const elapsedTime = clock.getElapsedTime();
  const deltaTime = clock.getDelta();

  // 1. 太陽のプラズマ(4Dノイズ)の時間を進める
  sunMaterial.uniforms.uTime.value = elapsedTime;

  // 2. すべての惑星の軌道計算、自転、シェーダーの更新
  planets.forEach(planet => {
    planet.update(deltaTime, elapsedTime);
  });

  // 3. 背景の星屑(GPGPU)の更新
  if (universeParticles) {
    universeParticles.update(deltaTime, elapsedTime);
  }

  // 描画
  renderer.render(scene, camera);
}

// 宇宙の時計を動かし始める
animate();

第1セクションで作成した ProceduralPlanet クラスの update メソッド内で、サイン波(sin)とコサイン波(cos)を用いた円軌道の計算が毎フレーム実行されます。これにより、太陽に近い第1惑星は猛スピードで駆け抜け、遠く離れた第4惑星はゆったりと外周を回る、物理法則に基づいた美しい公転運動が始まります。

さらに、各星のマテリアルに渡された elapsedTime によって、地表を流れる雲海、海面の波立ち、極地のオーロラの揺らめきといった「ミクロなノイズの動き」も同時に同期して描画されます。

100万の星屑が舞う「究極の背景」

そして、このミニ太陽系の背景を単なる「黒色」で終わらせてはいけません。 [Noise 入門 #30] で錬成した、GPGPUを用いた「100万のパーティクルによる星雲」をシーンの背景(天球)として配置しましょう。

Curl Noise(回転ノイズ)によってうねる巨大な星間ガスや、Ping-Pongバッファによって毎フレーム位置を更新し続ける無数の星屑たち。これらを太陽系の遥か後方に配置することで、空間の奥行きと圧倒的なスケール感が生まれます。

手前では各惑星が自転しながら太陽を回り、奥では100万の星々が銀河のうねりを描く。 数学とGPUが織りなす、完全なる「プロシージャル・ユニバース」の完成です。

5. 宇宙を駆ける — FlyControlsの導入

太陽系が完成し、星々がそれぞれの軌道を描き始めました。 しかし、この広大な宇宙をただ外から眺めているだけでは、もったいないですよね。

これまでの連載では、一つの星の地形や雲を観察するために OrbitControls(対象物を中心にカメラを回すコントロール)を使用してきました。しかし、太陽系という圧倒的なスケールの空間では、カメラの自由度が決定的に足りません。遠く離れた第4惑星へ近づこうにも、太陽を中心にしか回れないからです。

そこで、読者自身が宇宙船のパイロットとなり、広大な空間を自由に飛び回れるようにしましょう。Three.js に用意されている FlyControls を導入します。

FlyControls のセットアップ

FlyControls は、キーボードとマウス入力を用いて、空間内を全方位へ自由に推進・旋回できる、まさに「飛行(Fly)」のためのコントローラーです。

import { FlyControls } from 'three/addons/controls/FlyControls.js';

// カメラの初期位置を太陽系の少し外側に設定
camera.position.set(0, 50, 150);

// FlyControls の初期化
const controls = new FlyControls(camera, renderer.domElement);

// 前進・後退のスピード(宇宙スケールなので速めに設定)
controls.movementSpeed = 50;

// マウスによる旋回スピード
controls.rollSpeed = Math.PI / 10;

// 自動前進の有効/無効(Wキーを押さなくても進むかどうか)
controls.autoForward = false;

// マウスドラッグ時のみ旋回するよう設定(ブラウザ操作との競合を防ぐ)
controls.dragToLook = true;

セットアップはこれだけです。 あとは、先ほど作成した animate ループの中で、コントローラーにも deltaTime を渡して更新処理を行います。

function animate() {
  requestAnimationFrame(animate);
  const deltaTime = clock.getDelta();

  // ... 惑星やGPGPUの更新処理 ...

  // カメラコントロールの更新
  controls.update(deltaTime);

  renderer.render(scene, camera);
}

圧倒的な没入体験

これで、ブラウザ上に構築されたプロシージャルな宇宙を、自らの意志で探索できるようになりました。

  • W / S キーで前進・後退。
  • A / D キーで左右へのスライド。
  • R / F キーで上昇・下降。
  • マウスドラッグ で視点の旋回。

巨大な太陽(プラズマ球)の熱を感じるほどギリギリまで接近し、そこから一気に加速して宇宙空間へ。遠くを回る巨大ガス惑星の美しい環(リング)をすり抜け、青く輝く地球型惑星の雲海へとダイブする……。

ノイズという単なる「乱数の配列」から、数学とGPUの力によって錬成された、無限のバリエーションを持つ星々。その一つ一つに独自の生態系(バイオーム)があり、大気が散乱し、オーロラが揺らめき、文明の灯火が瞬いています。

あなたが今飛び回っているこの宇宙は、テクスチャ画像を一枚も使わず、すべてリアルタイムの計算(プロシージャル)によって描画されているのです。

第4集の終わりに:ノイズが創った「世界」

第31回から始まった「Three.js編」では、ノイズを単なる2Dのテクスチャから、プレイヤーが入り込み、触れることのできる3Dの「環境」へと進化させました。

数学の数式(アルゴリズム)とGPUの圧倒的な演算力(Shader)、そしてそれらを統合するエンジニアリング(Three.js)。これら3つの力が綺麗に噛み合った時、ブラウザという窓の向こう側に、広大な宇宙そのものが生まれることを私たちは体験しました。

しかし、ノイズの旅路はまだ終わりません。

次回予告:第5集「Procedural World 編」開幕

次回からは、いよいよ次なる次元となる第5集「Procedural World編」が開幕します。 神の視点で宇宙を創り上げた私たちは、舞台を再び「大地」へと戻し、今度はその地上を自らの足で歩くための世界を錬成します。

  • 無限に広がる Minecraft のようなボクセル地形の生成
  • シームレス(継ぎ目なし)なオープンワールドのチャンク管理
  • 風の流れ(風向ノイズ)と植生のプロシージャル配置
  • 3Dノイズが穿つ複雑な洞窟ネットワーク

単なるグラフィック(VFX)表現を超え、ついに「ゲームエンジン開発者」の領域へと足を踏み入れます。

ノイズの真の力は、ここからです。 第5集でお会いしましょう。お楽しみに!