[Next.js #43] Procedural Aurora — 極地の夜空に揺らめくプロシージャルなオーロラを実装

はじめに

前回の記事 [Next.js + Three.js #41] では、惑星の夜の領域に「街の灯り(Night Lights)」を灯し、プロシージャルな自然の中に文明の息吹を吹き込みました。

しかし、惑星の北極や南極といった極地に目を向けると、まだ夜空は暗く寂しいままです。

今回は、宇宙空間から降り注ぐ粒子と磁場が織りなす極地の魔法、「オーロラ(Aurora)」 を GLSL で錬成します。

オーロラ生成の数学的な理論やアルゴリズムについては、今朝公開した理論編の記事 [Noise 入門 #38] Procedural Aurora で詳しく解説しています。

本記事では、それを Next.js + Three.js の環境下で実際に動かし、息を呑むような「美しさ」を引き出すための実装とパラメータ調整にフォーカスします。

スクリーンショット:

動画(Youtube):

動画(PC):

1. 実装の設計「第3の球体」

これまでの実装で、私たちの惑星はすでに2つのレイヤー(球体)を持っています。

  1. Terrain Sphere(地形): 海、森、雪、そして街の灯りを描画するベースの球体。
  2. Cloud Sphere(雲): 地形の少し外側(スケール 1.075 倍)を覆い、影を落とす半透明の球体。

今回オーロラを追加するにあたり、既存の地形や雲の Shader にコードを混ぜ込むことも可能ですが、ここでは「第3の球体(Aurora Sphere)」を新しく追加するアプローチを取ります。

なぜ独立した球体にするのでしょうか?それは、「光の重ね合わせ(Additive Blending)」を極めてクリーンに行うためです。

オーロラは物体ではなく「発光するガス」です。Three.js 側で専用の Mesh と Material を用意し、blending: THREE.AdditiveBlending と depthWrite: false を設定することで、背景の地形や雲の色を暗く塗りつぶすことなく、純粋に「光の足し算」だけを行ってくれるようになります。これにより、雲の奥で大気と溶け合うような、透き通る光のベールを表現できるのです。

2. GLSLシェーダーの実装(オーロラ専用)

それでは、Next.js のコンポーネント内にオーロラ専用のシェーダーを定義しましょう。 Vertex Shader(頂点シェーダー)は座標を渡すだけのシンプルなもので構いません。魔法のすべては Fragment Shader(フラグメントシェーダー)の中で起こります。

// オーロラ用の Vertex Shader
const auroraVertexShader = `
  varying vec3 vWorldPosition;
  void main() {
    vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

// オーロラ用の Fragment Shader
const auroraFragmentShader = `
  // ※ここに Simplex 4D Noise (snoise) と FBM (fbm4d) の関数を定義します(省略)

  varying vec3 vWorldPosition;

  uniform float uTime;
  uniform vec3 uAuroraBottomColor;
  uniform vec3 uAuroraTopColor;
  uniform float uAuroraIntensity;
  uniform float uAuroraScale;
  uniform float uAuroraSpeed;
  uniform vec3 uLightDir;

  void main() {
    vec3 pos = vWorldPosition;
    float time = uTime * uAuroraSpeed;

    // 1. 空間全体をゆっくり歪ませる(Domain Warping)
    // Y成分に 0.0 を指定し、縦方向の歪みを防ぐ
    vec3 warpOffset = vec3(
        fbm4d(vec4(pos + time, 0.0)),
        0.0,
        fbm4d(vec4(pos - time, 0.0))
    );

    // 2. 歪んだ座標を引き伸ばす(Stretching)
    vec3 auroraPos = pos + warpOffset * 2.0;
    auroraPos.y *= 0.05; // Y軸のスケールを極端に小さくし、縦スジを作る

    // 3. オーロラのベース密度を計算(マイナス値をカット)
    float auroraDensity = max(0.0, fbm4d(vec4(auroraPos * uAuroraScale, time)));

    // 4. マスク処理
    // 極地マスク:北極(1.0)と南極(-1.0)の両方で白くなるよう abs() を使用
    float polarMask = smoothstep(0.7, 0.95, abs(normalize(pos).y));

    // 昼夜マスク:オーロラは夜の領域(daylightがマイナス)でだけ光らせる
    float daylight = dot(normalize(pos), normalize(uLightDir));
    float nightMask = smoothstep(0.1, -0.2, daylight);

    // すべてのマスクを掛け合わせて最終的な強度を決定
    float finalAurora = auroraDensity * polarMask * nightMask;

    // 5. 高さ(ノイズの強さ)に応じたカラーグラデーション
    vec3 auroraColor = mix(uAuroraBottomColor, uAuroraTopColor, auroraDensity);

    // 加算合成用の出力(RGBに強度を掛け合わせる)
    gl_FragColor = vec4(auroraColor * finalAurora * uAuroraIntensity, finalAurora);
  }
`;

難解な数式よりも「感覚的なリンク」を大切にする

Shader のコードを見ると、normalize や dot、mix、そして4次元のノイズ関数など、数学の呪文が並んでいて圧倒されるかもしれません。

しかし、今の時点でコードの数式を隅から隅まで完璧に理解する必要は全くありません。

「auroraPos.y *= 0.05 と掛け算の数値を小さくしたら、ノイズが縦にビローンと伸びてオーロラっぽくなったな」 「warpOffset を足したら、風に揺らめくような動きが出たな」

このように、『数式・パラメータの変更』と『画面上の視覚的な変化』の感覚的なリンクを掴むこと。これこそが、プロシージャル生成において最も重要で、かつクリエイティビティの源泉となる部分です。実際に、パラメータを触って「美しい」と思えるポイントを見つけ出せれば、それはもう立派な Shader アートなのです。

3. Three.js への組み込みとGUI設定

ここで重要なのは、オーロラ専用の球体(Aurora Sphere)のサイズです。 地形(スケール 1.0)や、雲(スケール 1.075)よりもさらに外側の大気圏上層を漂わせるため、スケールを 1.15 など少し大きめに設定します。

// 共通の球体ジオメトリ(地形や雲と同じものを使用)
const geometry = new THREE.SphereGeometry(2, 192, 192);

// オーロラマテリアルの生成
const auroraMaterial = new THREE.ShaderMaterial({
  vertexShader: auroraVertexShader,
  fragmentShader: auroraFragmentShader,
  uniforms: uniforms, // 先ほど定義したパラメータ群を渡す
  transparent: true,
  depthWrite: false, // 奥のオブジェクト(雲や地形)を描画し続けるために必須
  blending: THREE.AdditiveBlending, // 加算合成:光の足し算を行う
});

// オーロラメッシュの生成と配置
const auroraMesh = new THREE.Mesh(geometry, auroraMaterial);
auroraMesh.scale.setScalar(1.15); // 雲(1.075)のさらに外側に配置
scene.add(auroraMesh);

GUI(lil-gui / Leva)への登録

パラメータをリアルタイムに調整できるよう、GUIに組み込みます。Next.js環境であれば Leva を使うことも多いですが、今回は lil-gui の例で記述します。

const folderAurora = gui.addFolder("Procedural Aurora");
folderAurora.add(params, "auroraIntensity", 0.0, 5.0, 0.1).name("Intensity").onChange(v => uniforms.uAuroraIntensity.value = v);
folderAurora.add(params, "auroraScale", 0.5, 30.0, 0.1).name("Scale").onChange(v => uniforms.uAuroraScale.value = v);
folderAurora.add(params, "auroraSpeed", 0.0, 1.0, 0.01).name("Speed").onChange(v => uniforms.uAuroraSpeed.value = v);
folderAurora.addColor(params, "auroraBottomColor").name("Bottom Color (Green)").onChange(v => uniforms.uAuroraBottomColor.value.set(v));
folderAurora.addColor(params, "auroraTopColor").name("Top Color (Purple)").onChange(v => uniforms.uAuroraTopColor.value.set(v));

4. パラメータ調整の妙:自然な神秘さを引き出す

オーロラが画面に表示されたら、パラメータを触ってみましょう。実は、プロシージャル生成において「コードを書く時間」と同じくらい「パラメータを調整する時間」は重要です。

ここでは、極めて自然で美しいオーロラを引き出すための、おすすめのパラメータ設定(と、その理由)を紹介します。

  • Intensity(明るさ): 1.3 付近 光の現象を作ると、つい数値を高くしてギラギラと発光させたくなりますが、ここはグッと堪えて 1.3 程度に抑えます。これにより、オーロラが自己主張しすぎず、背景の雲のディテールや夜の闇と「溶け合う」ような奥ゆかしさが生まれます。
  • Scale(ノイズの細かさ): 1.1 付近 ノイズのスケールは、数値が小さいほど模様が「大きく」なります。Scaleを 1.1 という小さめの値に設定することで、細かいチカチカとしたノイズではなく、惑星全体を包み込むような「巨大でゆったりとした光のうねり(リボン)」を表現できます。
  • Speed(アニメーション速度): 0.03 付近 オーロラは風で激しく揺れる布ではなく、大気圏上層でゆっくりと形を変えるプラズマです。スピードを極限まで落とすことで、より本物らしい雄大さと神秘性が際立ちます。

💡 調整のヒント:
「Intensity 1.3 / Scale 1.1」という設定は、オーロラが雲の奥で大気と溶け合うような、極めて自然なバランスを生み出します。数式の全てを理解していなくても、GUIのスライダーを動かして「あ、今すごく綺麗だ」というポイントを見つけ出す。あなたのその直感こそが、最高のアートディレクションになります。

まとめ:生命と大気の境界線

今回の実装により、惑星の夜の領域は劇的な進化を遂げました。

陸地には「文明の灯火」が静かに瞬き、北極と南極の上空には「オーロラ」という大気と宇宙の境界線を示すベールが揺らめいています。地形(ソリッド)、雲(透過)、そしてオーロラ(加算発光)という3つのレイヤーが重なり合うことで、単なるWebGLの球体が、圧倒的なスケール感を持つ「生きた惑星」へと変貌しました。