[Noise 入門 #38] Procedural Aurora — 磁力線とノイズが織りなす極地の光のカーテン

はじめに

前回の記事では、夜の領域に「文明の灯火」を灯し、知的生命体の存在をプロシージャルに描き出しました。 しかし、惑星の極地に目を向けると、まだ夜空は寂しいままです。

今回は、宇宙空間から降り注ぐ粒子と惑星の磁場が衝突して生まれる極地の魔法、「オーロラ(Aurora)」 を Shader(GLSL)で錬成します。ノイズの座標系を一方向に強く引き伸ばす「Stretching」というアプローチと、これまで学んだ Domain Warping を組み合わせ、あの神秘的に揺らめく光のカーテンを実装しましょう。

前回の記事:

1. オーロラの正体は「引き伸ばされたノイズ」

これまで雲や地形を作ってきた FBM(Fractal Brownian Motion)や Simplex Noise は、原則としてX, Y, Z の全方向に均等なスケールを持つ等方性(Isotropic)のノイズでした。そのまま球体や空間に適用すると、モクモクとした「雲」や、ゴツゴツとした「岩」になります。

しかし、極地の空を彩るオーロラの視覚的な特徴は、「縦方向に長く伸びた光のカーテン」です。これを数学的に表現するには、ノイズ関数に入力する座標(UVや3D座標)のスケールを意図的に操作し、空間に異方性(Anisotropy:方向によって性質が異なる状態)を持たせる必要があります。

直感に反する空間の魔法:「小さく掛ける」と「長く伸びる」

ここで、Shader初学者が必ず一度はつまずく「座標サンプリングの錯覚」について触れておきましょう。

特定の方向にノイズを「長く引き伸ばしたい」とき、私たちは直感的にその軸に大きな数値を掛けたくなります。しかし、Shaderの世界では逆です。

具体的には、極地付近の座標ベクトル $\vec{p} = (x, y, z)$ に対して、Y軸方向(縦方向)のスケール係数 $S_y$ を極端に小さく乗算します。

$$\vec{p}_{stretch} = (x \cdot S_x, y \cdot S_y, z \cdot S_z)$$

例えば、水平方向(X, Z)のスケールを 5.0 とし、垂直方向(Y)のスケールを 0.1 に設定した場合のGLSLコードを見てみましょう。

// 入力された3D座標(position)を歪ませる
vec3 p = position;
vec3 stretchedPos = vec3(p.x * 5.0, p.y * 0.1, p.z * 5.0);

// 引き伸ばされた座標でノイズを取得
float noiseVal = snoise(stretchedPos);

なぜ 0.1 を掛けると、縦に伸びるのでしょうか? それは、私たちがノイズという「無限に広がる地図」の上を歩きながら値を取得(サンプリング)しているからです。

  • p.x * 5.0 の意味: X軸方向へ進むとき、通常の5倍のスピードでノイズの地図を駆け抜けます。結果として、短い距離の中にたくさんのノイズの起伏が詰め込まれ、細かく圧縮されて見えます。
  • p.y * 0.1 の意味: Y軸方向へ進むとき、通常の10分の1のスピードでゆっくりとノイズの地図を進みます。結果として、同じノイズの値が画面上で長く留まることになり、視覚的には「ビローンと縦に引き伸ばされた」ように見えるのです。

FBMで「光の筋」を重ね合わせる

この「Stretching(引き伸ばし)」の技術を、これまでに学んだ FBM のループ(オクターブの加算)に組み込みます。

単一のノイズではただのぼやけたグラデーションにしかなりませんが、周波数を上げながら引き伸ばされたノイズを重ねていくことで、オーロラ特有の「無数の細かい光のシャワー」が生まれます。

float auroraFBM(vec3 p) {
    float value = 0.0;
    float amplitude = 0.5;

    // ベースとなる空間のスケール
    vec3 pos = p;

    for (int i = 0; i < 5; i++) {
        // Y軸のみスケールを抑え、縦長の特徴を維持する
        vec3 samplePos = vec3(pos.x, pos.y * 0.1, pos.z);

        value += amplitude * snoise(samplePos);

        // 次のオクターブへ(周波数を上げ、振幅を下げる)
        pos *= 2.0;
        amplitude *= 0.5;
    }
    return value;
}

この処理により、数学的なランダムの塊だったノイズが、空から降り注ぐ「縦方向の構造」を獲得しました。これがオーロラ錬成の第一歩です。

2. 磁力線に沿った Domain Warping の適用

通常の Domain Warping は、空間の X, Y, Z すべての軸を等しくグニャグニャと歪ませます。しかし、今回生成した「縦スジのノイズ」に対して全方位の歪みを適用してしまうと、せっかく作った真っ直ぐな縦のラインが千切れ、ただの「歪んだ雲(モクモク)」に戻ってしまいます。

オーロラは、宇宙空間から降り注ぐ太陽風の粒子が、惑星の磁力線に沿って落下・衝突することで発光します。つまり、粒子は「縦方向(Y軸)には真っ直ぐ落ちる」のに対し、その集団としてのカーテンの形状は「水平方向(XZ平面)の磁場や大気の流れによってうねる」という物理的な性質を持っています。

この現象を GLSL 上でフェイクするためには、「Y軸(縦)の歪みを殺し、XZ平面(水平)だけを歪ませる」という指向性を持たせた Domain Warping が必要になります。

コードで見る「水平方向への歪み」

実際の GLSL コードで、この「選択的な空間のねじれ」がどのように実装されているか見てみましょう。

// 時間経過によるアニメーション(ゆっくりとした揺らめき)
float time = uTime * 0.1;

// 1. まず空間全体をゆっくり歪ませる(ベースのうねり)
// ※ここが重要! Y成分には 0.0 を指定し、縦方向の歪みを無効化する
vec3 warpOffset = vec3(
    fbm(position + time),
    0.0,
    fbm(position - time)
);

// 2. 歪んだ座標に、セクション1の Stretching(引き伸ばし)を適用する
// オフセットを足して空間を水平にうねらせた後...
vec3 auroraPos = position + warpOffset * 2.0;

// Y軸のスケールを極端に小さくし、縦に長く引き伸ばす
auroraPos.y *= 0.05;

// 3. 最終的なオーロラの密度(明るさ)を計算
// 歪み・引き伸ばしが完了した座標を、高周波のFBMに突っ込む
float auroraDensity = fbm(auroraPos * 10.0);

3つのステップが織りなす「魔法のレシピ」

この短いコードの中で起きている数学的な変換を、視覚的にイメージしてみましょう。

  1. ベースのうねりを作る (warpOffset): 空から吊るされた巨大な見えないカーテンの「裾(すそ)」を想像してください。fbm(position + time) と fbm(position - time) を使って、X方向とZ方向から別々の風を当てます。Y軸の歪みを 0.0 にすることで、カーテンの布地そのものが上下に引き裂かれるのを防ぎます。
  2. 空間の圧縮と伸張 (auroraPos.y *= 0.05): 風で水平にゆらゆらと波打っている空間に対して、セクション1で学んだ Stretching を適用します。これにより、「波打つ形」を保ったまま、テクスチャだけが上から下へツーッと長く伸びた状態になります。
  3. ディテールの抽出 (auroraDensity): 最後に、その歪んで伸びた座標系を使って、細かい FBM ノイズをサンプリングします。これにより、オーロラ特有の「無数の細い光の繊維が集まって、大きなうねりを形成している」という、極めてフラクタル的で美しいディテールが完成します。

「時間 (time)」による揺らめきの演出

warpOffset の計算内で、X軸側には + time、Z軸側には - time を加算している点にも注目してください。 同じ time を足してしまうと、空間全体が斜め一方向にスライドするだけの単調な動きになってしまいます。XとZで時間の進む方向(あるいはサンプリングするノイズの位相)をずらすことで、オーロラが「同じ場所で蠢(うごめ)きながら形を変え続ける」という、幽玄で捉えどころのない動きを生み出すことができるのです。

3. 極地マスク(Polar Mask)の生成

オーロラは赤道付近には発生しません。私たちが生成したプロシージャルな光のカーテンを、惑星の北極・南極付近にのみ限定して表示させるために、球体の緯度(Y座標)を利用した「マスク(Mask)」を作成します。

マスクとは、Photoshopなどでもおなじみの「白(1.0)で見せて、黒(0.0)で隠す」という画像処理の基本概念です。これをGLSLの数学関数を使って動的に生成します。

球体のローカル座標のY成分を $y$ 、半径を $R$ としたとき、極地付近で $1.0$、赤道付近で $0.0$ になるようなスムーズなマスク関数 $M(y)$ を定義します。

$$M(y) = \text{smoothstep}(T_{min}, T_{max}, \frac{|y|}{R})$$

これをGLSLで実装すると、驚くほどシンプルに1行で書くことができます。

// Y座標の絶対値を取り、極地(1.0に近づく)ほど白くなるマスク
float polarMask = smoothstep(0.7, 0.95, abs(normalize(vPosition).y));

// オーロラの強さにマスクを掛ける
float finalAurora = auroraDensity * polarMask;

3つの数学関数が織りなす「エレガントな絞り込み」

このたった1行のコードの中には、Shaderを書く上で絶対に覚えておきたい3つのテクニックが詰まっています。内側から順番に解き明かしていきましょう。

  • ① 緯度の取得:normalize(vPosition).y 頂点のローカル座標(中心が 0.0, 0.0, 0.0 の座標系)を normalize() で正規化すると、ベクトルの長さが強制的に 1.0(半径1の単位球)になります。 この状態のY成分(.y)を取り出すと、「北極が 1.0、赤道が 0.0、南極が -1.0」 という、非常に扱いやすい「緯度データ」をそのまま取得できます。
  • ② 南北の対称化(ミラーリング):abs(…) 北極(1.0 付近)だけにオーロラを出すなら話は簡単ですが、南極(-1.0 付近)にも同じように発生させたいですよね。 ここで if 文を使って分岐させるのはGPUの計算上ご法度です。代わりに絶対値を返す abs() 関数を使います。これにより、南極側のマイナスの値が反転し、「赤道が 0.0、北極も南極も 1.0」 という対称的なデータへと折り畳まれます。
  • ③ 自然なフェードアウト:smoothstep(0.7, 0.95, …) 単純に「緯度が0.7以上ならオーロラを表示」としてしまうと、オーロラの境界線がハサミで切ったようにパキッと分かれてしまい、極めて不自然になります(CGっぽさが悪目立ちします)。 smoothstep(0.7, 0.95, 値) を通すことで、「緯度 0.7(中緯度)から徐々にオーロラがフェードインし始め、緯度 0.95(極地中心)で完全にクッキリと表示される」という、自然界特有の滑らかな減衰(グラデーション)を作り出すことができます。

マスクの掛け算が「存在」を決定する

最後に、セクション2で計算したオーロラの密度(auroraDensity)に対して、この polarMask を掛け算します。

極地では 1.0 を掛けるのでオーロラがそのまま残り、赤道付近では 0.0 を掛けるのでオーロラが完全に消滅します。 この「マスク値の乗算」は、Procedural World(プロシージャルな世界生成)において、地形のバイオーム(砂漠、森、雪原)を塗り分ける際にも頻繁に登場する超重要テクニックです。

4. 光の重ね合わせとカラーグレーディング

現実のオーロラは「物体」ではなく、大気中のガス(酸素や窒素)がプラズマと衝突して発光する「光」そのものです。そのため、通常の3Dモデルのように光の反射(ランバート反射など)を計算するのではなく、自ら発光しているように見せる処理が必要になります。

この発光を表現するためには、レンダリング時に背景(星空や宇宙空間)の色に対して、オーロラの色を「足し算」する加算合成(Additive Blending)という手法を用います。

Three.js 側での設定 vs Shader 側での加算

実装方法は大きく分けて2つあります。状況に合わせて使い分けましょう。

  • Three.js のマテリアル設定で行う場合: オーロラ専用の球体(Mesh)を用意している場合、Three.js の ShaderMaterial のプロパティで blending: THREE.AdditiveBlending と depthWrite: false を設定します。これにより、GPU側で自動的に背景色とオーロラの色が加算され、美しく発光して透けるようになります。
  • Shader(GLSL)内で完結させる場合: 1つの Shader 内で「夜空」と「オーロラ」を両方描画している場合は、最終的な出力の手前で vec3 finalColor = skyColor + auroraColor; のように、シンプルに色ベクトル同士を足し合わせます。

物理現象を mix 関数で模倣する

オーロラの色は均一ではありません。一般的に、高度が低い部分(酸素との衝突)は鮮やかなエメラルドグリーンに、高度が高い部分(窒素との衝突)は赤や神秘的な紫に発光します。

この物理的なグラデーションを GLSL で再現するために、mix 関数を使用します。

// 定数として2つの極端な色を定義
vec3 colorBottom = vec3(0.0, 1.0, 0.5); // 鮮やかなエメラルドグリーン
vec3 colorTop = vec3(0.5, 0.0, 0.8);    // 神秘的な紫

// ノイズの密度(または高さ)に応じて色をグラデーションでブレンド
vec3 auroraColor = mix(colorBottom, colorTop, auroraDensity);

mix(A, B, t) は、「割合 t に応じて色Aと色Bを混ぜ合わせる」という、Shaderの世界で最も多用される関数の1つです。 ここでは割合 t に auroraDensity(ノイズの濃度)を入れています。これにより、「ノイズが濃い(強い)部分は紫っぽく、薄い部分は緑っぽく」といった複雑で有機的な色の変化が生まれます。単純にY座標(高さ)を t に入れて、上に行くほど紫になるような素直なグラデーションにしても美しいです。

最終出力:光を空間に解き放つ

最後に、計算した「色(auroraColor)」に、セクション3で作った「マスクとノイズの強さ(finalAurora)」を掛け合わせてフラグメントシェーダーの出力(gl_FragColor)に渡します。

// RGBには強さを掛け合わせた色を、Alpha(透明度)には強さそのものを渡す
gl_FragColor = vec4(auroraColor * finalAurora, finalAurora);

ここで auroraColor * finalAurora と掛け算をしているのは、乗算済みアルファ(Pre-multiplied Alpha)というテクニックに近い考え方です。 透明度(Alpha)が下がるにつれて、色(RGB)自体も暗く(黒に近づく)なるように計算しておくことで、背景と合成した際にフチが白く濁ったり、不自然なグレーのハロー(後光)が出たりするのを防ぎ、闇夜へ滑らかに溶け込む極上の光のベールが完成します。

まとめ:空と大地、そして宇宙の境界線

今回の Stretching(引き伸ばし)と Domain Warping(空間の歪曲)の組み合わせにより、あなたの惑星は極地帯に美しい光のベールを纏いました。

  • 雲(Cloud): 全方位に均等な 3D FBM ノイズ
  • 台風(Storm): Curl Noise による渦を巻くベクトル場
  • オーロラ(Aurora): Y軸方向に引き伸ばされた異方性ノイズ

「ノイズのスケール(掛け算)」をほんの少し変えるだけで、同じアルゴリズムが全く異なる自然現象へと劇的に姿を変える。これこそが、数学とアートが交差するプロシージャル生成の最も面白く、そして恐ろしいところです。

次回予告:[Noise 入門 #39] Procedural Rings — 惑星を彩る塵と氷の軌道

極地の空も完成し、惑星そのものは圧倒的な完成度を誇るようになりました。しかし、カメラを少し引いて宇宙空間から全体を眺めたとき、周囲の空間がまだ少し寂しいかもしれません。

次回は、土星のような「惑星の環(Rings)」をプロシージャルに生成します。1Dノイズを用いた密度の制御と、極座標系(Polar Coordinates)を用いた円環状のテクスチャマッピングを組み合わせ、無数の塵と氷の軌道をShader上に錬成しましょう!お楽しみに。