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

はじめに

惑星の空、夜景、そして極地のオーロラ。大気と地表の表現は極まり、星自体は圧倒的な完成度を誇るようになりました。

しかし、カメラを少し引いて宇宙空間から全体を眺めたとき、星の周囲が少し寂しく感じるかもしれません。今回は視点を再び宇宙空間へ戻し、土星のような「惑星の環(Rings)」をプロシージャルに生成します。

何億もの塵と氷が描く軌道は、複雑なテクスチャを使わずとも、数式とノイズだけで Shader 上に錬成することができます。極座標系(Polar Coordinates)と 1D ノイズを組み合わせて、美しい円環の魔法を展開しましょう。

前回の記事:

1. 円環を描く極座標系(Polar Coordinates)の復習

宇宙空間に浮かぶ美しい「惑星の環」を作るための最初の一歩は、四角形のポリゴン(Plane)の中に「円」を描くことです。

通常のUV座標(X, Y)は、左下を (0.0, 0.0)、右上を (1.0, 1.0) とする「縦横のグリッド(直交座標系)」です。しかし、この直交座標のまま円を描こうとすると、数式が非常に複雑になってしまいます。

そこで登場するのが、第23回の「魔法陣生成」でも活躍した極座標系(Polar Coordinates)への変換です。空間の捉え方を「縦横(X, Y)」から「中心からの距離($r$)と角度($\theta$)」に変換する、Shaderにおける必須テクニックです。

① 原点を中心に移動する

まずは、左下にある原点 (0.0, 0.0) を、Planeの「ど真ん中」に持ってくる必要があります。

// UVを (0.0 〜 1.0) から (-1.0 〜 1.0) に変換
vec2 centeredUV = uv * 2.0 - 1.0;

このたった1行の計算で、画面の中央が (0.0, 0.0) となり、四隅が -1.0 や 1.0 になる座標系に生まれ変わります。これがすべての円形描画の基準になります。

② 中心からの距離 $r$ を取得する

原点が中央に来たら、次は「中心から今計算しているピクセルまでの距離」を求めます。 数学的には、三平方の定理(ピタゴラスの定理)を使って以下の式で計算します。

$$r = \sqrt{x^2 + y^2}$$

GLSLでは、わざわざこの数式を書かなくても、ベクトルの長さを返す length() 関数を使えば一撃で計算できます。

// 中心からの距離(半径)を取得
// 中心は 0.0、端にいくほど 1.0 に近づくグラデーションになる
float radius = length(centeredUV);

この radius をそのまま色として出力すると、中心が黒(0.0)で、外側に向かって白(1.0)に広がっていく美しい放射状のグラデーションが現れます。

③ 内径と外径を削り「ドーナツ型(マスク)」を作る

距離 $r$ の値が取れたら、いよいよリングのベースとなる「ドーナツ型」を作ります。 「中心に近すぎる部分(内径)」と「外側すぎる部分(外径)」を透明にして切り落とす作業です。

ここで活躍するのが smoothstep 関数です。

// リングの内径と外径を定義
float innerRadius = 0.5; // これより内側は切り落とす
float outerRadius = 0.9; // これより外側は切り落とす

// 内側の円をくり抜く(0.5〜0.51の間で滑らかに白にする)
float innerMask = smoothstep(innerRadius, innerRadius + 0.01, radius);

// 外側の円を切り落とす(0.89〜0.9の間で滑らかに黒にする)
float outerMask = 1.0 - smoothstep(outerRadius - 0.01, outerRadius, radius);

// 2つのマスクを掛け合わせてドーナツ型を完成させる
float ringMask = innerMask * outerMask;

この ringMask こそが、リングを描画するためのキャンバスです。 値が 1.0 の部分(ドーナツの白い部分)にだけ、これからノイズを使って「塵と氷の細かな軌道」を描き込んでいくことになります。

💡 角度($\theta$)は使わないの?
極座標系のもう一つの要素である角度 $\theta$ は atan(y, x) で取得できます。
魔法陣の時は回転させるために角度を使いましたが、土星の環のような「完璧な同心円の縞模様」を作る場合、実は距離($r$)の情報だけで十分です。リングの密度は「中心からどれだけ離れているか」だけで決まるからです。

2. 1D Noise による「密度の縞模様」

美しいドーナツ型のキャンバス(マスク)が用意できたら、次はいよいよ土星の環のような「無数の細い縞模様(軌道)」を描き込んでいきます。

これまで、地形や雲、オーロラを作るときは、主に 2D や 3D のノイズを使って空間全体にランダムな揺らぎを与えてきました。しかし、惑星の環を作るにあたって、2D や 3D のノイズは必要ありません。

なぜなら、リングを構成する無数の塵や氷は、重力と軌道共鳴によって特定の距離に集まる性質があり、その密度は「中心からの距離($r$)」のみに依存するからです。角度($\theta$)によって密度が変わることは基本的にありません。

① 距離 $r$ だけをノイズに渡す魔法

通常の2Dノイズであれば noise2D(vec2(x, y)) のようにXY座標を渡しますが、今回は先ほど計算した「中心からの距離 radius」というたった1つの数値(1次元データ)だけをノイズ関数に渡します。

// 1Dのフラクタルノイズを「距離」に対して適用
float ringDensity = fbm1D(radius);

これが何を意味するのか。 中心から同じ距離にあるピクセル(つまり、同じ円周上にあるすべてのピクセル)には、まったく同じノイズの値が返ってきます。その結果、2Dノイズのような「まだら模様」ではなく、完璧な同心円状の「リング(輪)」が浮かび上がるのです。

② 周波数(Frequency)を極端に引き上げる

しかし、単に fbm1D(radius) と書いただけでは、ふんわりとした巨大なグラデーションの輪が数本できるだけで、到底「無数の塵の集まり」には見えません。

そこで、ノイズの周波数(Frequency)を極端に引き上げます。

// 距離を150倍にスケールアップして高周波の縞を作る
float ringDensity = fbm1D(radius * 150.0);

入力値である radius に 150.0 のような大きな数値を掛けることで、ノイズの波がギューッと圧縮されます。これにより、レコード盤の溝のような、あるいはバームクーヘンの層のような、非常に細かくランダムな密度の変化が生まれます。

③ FBM でマクロな隙間とミクロな塵を作る

ここで単なる noise1D ではなく、第5回で学んだ FBM(Fractal Brownian Motion) を使っているのにも重要な理由があります。

実際の惑星の環(土星のリングなど)には、「カッシーニの間隙」と呼ばれるような巨大な隙間(マクロな構造)と、その中に無数に存在する極細の塵の帯(ミクロな構造)が共存しています。

FBMは、大きな波(低周波)と細かな波(高周波)を何層にも重ね合わせるアルゴリズムです。これを1Dノイズに適用することで:

  • 低周波成分:リング全体を分断する「大きな隙間(暗い帯)」を作る
  • 高周波成分:氷の粒が密集した「細かなレコードの溝(明るい帯)」を作る

という、自然界の複雑な軌道構造を数式だけで完璧にシミュレーションできるのです。

💡 さらに実在感を出すテクニック
出力された ringDensity に対して smoothstep(0.3, 0.7, ringDensity) のようにコントラストを強調する処理を挟むと、ノイズの低い値が完全に切り捨てられ、「何もない真っ暗な隙間」が明確に作られます。これにより、リングのパキッとした硬い質感が際立ちます。

3. GLSL 実装:塵と氷の軌道を錬成する

それでは、実際に Shader のコードを組んでみましょう。 GLSLには標準で「1次元のノイズ関数」が用意されていないため、まずはシンプルな noise1D と fbm1D を自作するところから始めます。

varying vec2 vUv;

// ==========================================
// 1D Noise & FBM の定義
// ==========================================
// 簡易的な1Dハッシュ(乱数)関数
float hash(float n) {
    return fract(sin(n) * 1e4);
}

// 1Dバリューノイズ
float noise1D(float x) {
    float i = floor(x);
    float f = fract(x);
    // ease曲線(smoothstepの数式版)で補間
    float u = f * f * (3.0 - 2.0 * f);
    return mix(hash(i), hash(i + 1.0), u);
}

// 1D FBM(フラクタル・ブラウン運動)
float fbm1D(float x) {
    float v = 0.0;
    float a = 0.5;
    float shift = 100.0; // 規則性を散らすためのシフト値
    for (int i = 0; i < 5; ++i) {
        v += a * noise1D(x);
        x = x * 2.0 + shift;
        a *= 0.5;
    }
    return v;
}

// ==========================================
// メイン処理
// ==========================================
void main() {
    // 1. 座標を中心化し、極座標系(距離 r)に変換
    vec2 p = vUv * 2.0 - 1.0;
    float radius = length(p);

    // 2. リングの内径と外径を定義(ドーナツ型のマスク)
    float innerRadius = 0.5;
    float outerRadius = 0.9;

    // 内側と外側の境界を smoothstep で滑らかに切り落とす
    float ringMask = smoothstep(innerRadius, innerRadius + 0.01, radius)
                   - smoothstep(outerRadius - 0.01, outerRadius, radius);

    // 3. 距離に応じた1Dノイズで「細かな縞模様(塵の密度)」を生成
    // 距離(radius)を200倍にスケールアップして超高周波の縞を作る
    float density = fbm1D(radius * 200.0);

    // コントラストを強調して、暗い部分を完全に削り落とし「隙間」を作る
    density = smoothstep(0.3, 0.7, density);

    // 4. 色とアルファ(透明度)の決定
    vec3 dustColor = vec3(0.8, 0.75, 0.7); // 塵(ダスト)のくすんだ色
    vec3 iceColor = vec3(0.9, 0.95, 1.0);  // 氷の反射(青白い色)

    // 色のブレンド用には、密度とは別の「少し緩やかなノイズ」を使う
    float colorNoise = fbm1D(radius * 50.0);
    vec3 finalColor = mix(dustColor, iceColor, colorNoise);

    // ベースのドーナツ型(ringMask)と、ノイズの縞模様(density)を掛け合わせる
    float alpha = ringMask * density;

    // 最終出力
    gl_FragColor = vec4(finalColor, alpha);
}

コードが魔法に変わる「3つの実装ポイント」

ただ数式を並べただけに見えますが、このコードには自然現象をシミュレーションするための強力な工夫が詰まっています。

  • 極座標から1Dへの次元圧縮 通常 vec2 で扱う空間のピクセル座標を、中心からの距離 radius という「1次元のデータ」に圧縮してからノイズ関数に渡しています。これにより、どれだけノイズが荒ぶっても、必ず完璧な同心円状の模様として出力されます。
  • 高周波な FBM によるスケール感の演出 fbm1D(radius * 200.0) のように、入力値に対して非常に大きなスケール値を掛けています。1枚の小さなポリゴンの中に、何万kmにも及ぶ微細なリングの地層をギュッと圧縮して表現するためのテクニックです。
  • 異なる周波数を用いた「色」の分離 密度(アルファ値)の計算には radius * 200.0 の細かなノイズを使いましたが、色のブレンドには radius * 50.0 という少し大きな波のノイズを使っています。これにより、「細かな塵の軌道」が集まった大きな帯ごとに、氷の成分が多い(青白い)帯と、岩石の成分が多い(茶色い)帯を自然に分離させています。

⚠️ Three.js で実装する際の注意点
このマテリアルは透明度(Alpha)を使用するため、Three.js 側で ShaderMaterial を定義する際は、必ず transparent: true と side: THREE.DoubleSide(裏面からも見えるようにする)を設定するのを忘れないようにしましょう。

4. 影の投影(Shadowing)のアイデア — 空間に「実在感」を定着させる

リングそのものの描画が完成したら、次のステップとして「影の計算」に挑戦してみましょう。 現在のままでは、惑星の周りに明るいリングの板が「ただ重なっているだけ」に見えてしまいます。ここに光の遮蔽(しゃへい)の概念を取り入れることで、一気に宇宙のスケール感と実在感が増します。

実装すべき影は、大きく分けて以下の2つです。

  1. 惑星がリングに落とす影(リングの後ろ側が、星本体に遮られて暗くなる)
  2. リングが惑星に落とす影(星の地表や大気に、リングのノイズ縞模様が投影される)

これらを Shader(GLSL)内で実装するには、主に2つのアプローチがあります。

① 究極のリアル:Raymarching(レイマーチング)

一つ目は、第9回の「Volumetric Clouds(雲の生成)」でも登場した Raymarching を用いるアプローチです。 現在計算しているピクセルから、光源(太陽)に向かって仮想の光のレイ(Ray)を少しずつ進ませます。その途中に「惑星のボリューム」や「リングの密度」が存在すれば、その分だけ光を減衰させるという物理的に正しい手法です。

  • メリット: 非常に正確で、リングの細かなノイズ密度がそのまま影の濃淡に反映される美しい結果が得られます。
  • デメリット: ピクセルごとにループ計算が発生するため、GPUへの負荷が跳ね上がります。Webブラウザ(Three.js)で動かす場合、スマートフォンなどでは処理落ちの原因になりやすいです。

② パフォーマンスと手軽さ:Fake Shadow(擬似シャドウ)

そこで、リアルタイムレンダリング(特に WebGL / Three.js)でよく使われるのが、数学的な判定を用いた Fake Shadow(擬似的な影)です。 光の方向ベクトル(lightDir)と、オブジェクトの法線や位置関係を組み合わせて、「ここは影になるはずだ」というマスクを計算ででっち上げます。

たとえば「惑星がリングに落とす影」であれば、以下のようなシンプルな条件で擬似的な影を作ることができます。

  • リングのピクセルが、惑星よりも「奥(光源の反対側)」にあるか?
  • そのピクセルから光源へ向かう直線が、惑星の半径(球体)と交差するか?

これを GLSL の組み込み関数である内積(dot)や距離計算(length)を使って判定します。

// --- 擬似シャドウのアイデア(リング側のShader) ---

// 光の方向ベクトル(例:右斜め上から)
vec3 lightDir = normalize(vec3(1.0, 0.5, 0.0));

// ピクセルの3D空間上のローカル座標
vec3 pos = vec3(vUv.x * 2.0 - 1.0, 0.0, vUv.y * 2.0 - 1.0);

// 光源に向かって惑星(原点にある半径1.0の球と仮定)を横切るか判定
float shadowMask = 1.0;

// リングのピクセルが光の反対側にある場合
if(dot(pos, lightDir) < 0.0) {
    // 光源ベクトルに直交する平面上での、原点(惑星中心)からの距離
    float distToLightRay = length(pos - dot(pos, lightDir) * lightDir);

    // 距離が惑星の半径(例: 0.45)より小さければ影の中
    if(distToLightRay < 0.45) {
        shadowMask = 0.0; // 影を落として暗くする
    }
}

// 最終的な色に影のマスクを掛ける
finalColor *= shadowMask;

💡 さらにクオリティを上げるなら
shadowMask を 0.0 と 1.0 でパキッと分けるのではなく、境界付近で smoothstep を使って滑らかに補間すると、「半影(光源が面積を持つことで生じるぼやけた影)」が表現でき、より自然で巨大な天体の雰囲気が出ます。

このような数学的アプローチ(解析的アプローチ)を使えば、重いループ処理(Raymarching)を行わずに、非常に軽量かつ説得力のある影を Three.js の空間に落とすことができます。

まとめ

今回は、1Dノイズと極座標系(Polar Coordinates)を組み合わせて、無数の塵と氷が描く「惑星の環(Rings)」をプロシージャルに生成しました。

1Dノイズは、数あるノイズ関数の中でも最もシンプルで、一見すると地味な存在です。しかし、極座標系という「空間の捉え方」を通し、さらにFBMによって周波数(Frequency)を極限まで高めることで、宇宙規模の巨大で複雑な構造物へと劇的に変化します。

地形の隆起、燃え盛る炎、細胞の蠢き、そして静かな土星の環。 ノイズは次元と座標系の壁を超えて、あらゆる自然現象を数学的に模倣できる、本当に魔法のような強力なツールですね。

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

ノイズによる地表の生成、バイオームの塗り分け、大気と雲海、荒れ狂う嵐、極地のオーロラ、そして今回のリング。 これにて、一つの「星」を構成するすべての要素が揃いました。

次回は、いよいよ第4集「Three.js編」の完結編です。 これまで作ってきたすべてのシェーダーとオブジェクトを統合し、複数のプロシージャル惑星がそれぞれの軌道を描く「ミニ太陽系(Procedural Solar System)」を Three.js 上に展開します。

ノイズが創り出した広大な宇宙を、カメラを操作して自由に飛び回る準備をしましょう!
お楽しみに。