[Noise 入門 #15] Simplex Noise を GLSL で実装する — 空間と時間を歪める 4D の魔法

1. 導入:魔法をコードに翻訳する

前回の記事(#14)では、Simplex Noise の背後にある「数学的な魔法」の正体を解き明かしました。

Perlin Noise が抱えていた「次元が上がるごとに計算量が爆発する」という弱点を克服するため、天才 Ken Perlin が閃いた解決策。それは、空間の基準を「四角形(Square)」から「正三角形(Simplex)」へとすり替えることでした。

そして、その変換を可能にした最大の発明こそが、空間の座標を斜めに引っ張って歪ませる「Skewing(スキューイング)」 でしたね。

しかし、理論を頭で理解しただけでは、まだ私たちの世界(画面)には何も描画されません。アルゴリズムという名の魔法陣は、コードという言語に翻訳されて初めて、GPUというエンジン上で光を放ちます。

今回のゴール:2Dから、3D、そして「4D」の世界へ

今回の記事では、前回学んだ理論を GLSL(Shader) のコードへと落とし込んでいきます。

あの複雑に見えた Skewing の計算式や、各頂点からの勾配(Gradient)の求め方を、実際に動くコードとして実装します。しかも、ただの2Dノイズでは終わりません。

Simplex Noise の真の力は「高次元での圧倒的なパフォーマンス」にあります。 そのため今回は、2Dの基本実装を確認したあと、一気に 3D(空間)、そして 時間(Time)をパラメータに加えた 4D ノイズ の実装まで駆け抜けます。

「時間」が加わる(4Dになる)と何が起きるのか? それは、3D空間に存在する立体的なノイズが、まるで呼吸しているかのように滑らかに変化し続ける「うねり」を手に入れることを意味します。流れる雲、燃え盛る炎、魔法のオーラ……プロシージャルな世界を彩る動的なエフェクトの数々は、この4Dノイズを土台にして作られているのです。

数学の理論から、GPUの力を極限まで解放する実践のステージへ。 準備はいいですか?さっそく、魔法をコードに翻訳していきましょう。

2. Skewing(歪み)と Unskewing(戻し)の係数の正体

Simplex Noise の GLSL 実装を初めて読んだ人が、ほぼ100%最初にぶつかる壁があります。 それが、コードの冒頭に突如として現れる 「謎のマジックナンバー(定数)」 です。

実際の Shader コードを見ると、こんな数字がしれっと置かれています。

// 2D Simplex Noise でよく見る謎の定数
const float F2 = 0.366025403;
const float G2 = 0.211324865;

「なんだこの中途半端な小数は?」と戸惑いますよね。 しかし、これらは適当な乱数でも、魔法の呪文でもありません。前回の理論編で解説した 「四角形のグリッドを、正三角形のグリッド(Simplex)に歪ませる」 ための、非常に精緻な数学的変換係数なのです。

F と G の役割:空間を行き来するパスポート

これらの定数は、直交座標系(いつもの 空間)と、Simplex座標系(歪んだ空間)を行き来するために使われます。

  • $F$ (Skewing Factor): 歪み係数 入力された直交座標を、Simplex空間に「歪ませる」ために使います。これにより、自分がどの正三角形(セル)の中にいるのかを特定します。

  • $G$ (Unskewing Factor): 戻し係数 特定したセルの頂点座標を、元の直交座標に「戻す」ために使います。最終的な距離や勾配を正しく計算するためには、歪んでいない現実の空間で計算する必要があるからです。

謎の小数の正体(数式への分解)

では、あの 0.366025... という数字はどこから来たのでしょうか? 実は、次元数を としたとき、空間を「すべての辺の長さが等しい正単体(Simplex)」に歪ませるための係数は、以下の美しい数式で定義されています。

【次元 $n$ における Skewing 係数 $F_n$】

$$F_n = \frac{\sqrt{n + 1} - 1}{n}$$

$$F_n = \frac{\sqrt{n + 1} - 1}{n}$$【次元 $n$ における Unskewing 係数 $G_n$】

$$G_n = \frac{1 - \frac{1}{\sqrt{n + 1}}}{n}$$

これを 2次元($n = 2$) に当てはめて計算してみましょう。

2Dの歪み係数 $F_2$:

$$F_2 = \frac{\sqrt{3} - 1}{2} \approx 0.36602540378$$

2Dの戻し係数 $G_2$:

$$G_2 = \frac{3 - \sqrt{3}}{6} \approx 0.2113248654$$

見事に GLSL のコード内に書かれている定数と一致しましたね! この定数は、正方形を対角線方向に「ぐっ」と押しつぶして、2つの完璧な正三角形(ひし形)を作るために必要な、幾何学的な比率だったというわけです。

3Dや4Dになるとどうなる?

この数式の素晴らしいところは、$n$ の値を変えるだけで高次元にもすぐに対応できる点です。 たとえば 3次元($n = 3$) ならどうなるでしょうか?

$$F_3 = \frac{\sqrt{4} - 1}{3} = \frac{1}{3}$$

$$G_3 = \frac{1 - \frac{1}{\sqrt{4}}}{3} = \frac{1}{6}$$

3Dの場合はルートが消え、$1/3$ や $1/6$ という非常にスッキリした分数になります。そのため、3D Simplex Noise のコードでは 0.333333 や 0.166666 といった数字が現れます。

そして 4次元($n = 4$) なら $\sqrt{5}$ が登場するため、再び複雑な小数($F_4 \approx 0.309016$)になります。


一見すると解読不能な暗号のようなマジックナンバーも、その正体を知れば「空間を正三角形に歪ませるための完璧な比率」であることがわかります。これで実装の最初の壁は越えました。

ここからはいよいよ、このパスポート($F$ と $G$)を使って、実際に頂点を見つけ、ノイズの値を計算していく コアロジックの実装 へと進みます。

3. Simplex Noise のコアロジックを実装する

ここからは、実際に GLSL で Simplex Noise を計算する手順を見ていきましょう。

とはいえ、高度に最適化されたノイズ関数をゼロからすべて自作するのは、車輪の再発明どころか「GPUの気持ちを読み解くパズル」になってしまいます。現在の WebGL や Shader 界隈において、Simplex Noise を使う際のデファクトスタンダード(業界標準)となっているのが、Ian McEwan 氏らによる Ashima Arts のオープンソース実装(webgl-noise)です。

今回はこの「完成された魔法の呪文」をベースに、内部で何が行われているのかを 3 つのステップに分けて解読していきます。

Step 1: 空間の歪み(入力座標から Simplex グリッド上の頂点を見つける)

まずは、入力された直交座標 v(ピクセルの位置など)を歪ませて、自分がどの「ひし形(2つの正三角形)」の中にいるのかを特定します。

// F2とG2の定数をまとめた便利なベクトル
const vec4 C = vec4(
    0.211324865405187,  // G2 (戻し係数)
    0.366025403784439,  // F2 (歪み係数)
    -0.577350269189626, // -1.0 + 2.0 * C.x
    0.024390243902439   // 1.0 / 41.0 (ハッシュ用)
);

// 1. 空間を歪ませて、基準となる底辺の頂点 (i) を求める
vec2 i  = floor(v + dot(v, C.yy));

// 2. 歪んでいない元の空間において、現在地から頂点(i)への距離ベクトル (x0) を計算
vec2 x0 = v - i + dot(i, C.xx);

ここで、先ほど解説した (C.yy) と (C.xx) が登場します。 v + dot(v, C.yy) で空間を斜めに歪ませ、floor を取ることで「自分が属するセルの左下の基準点 i」を確定させます。その後、すぐに を使って元の空間に引き戻し、「基準点から現在地までの正確な距離ベクトル x0」 を算出しています。

Step 2: 頂点の決定(自分がどの三角形に属しているかを判定する)

Simplex Noise の計算量削減の最大の鍵がここです。 正方形なら 4 つの頂点を計算する必要がありますが、ひし形を対角線で割った「正三角形」なら、計算する頂点は 3 つ で済みます。

自分が「右下の三角形」にいるのか、「左上の三角形」にいるのかを判定します。

// 3. 自分がどの三角形にいるかを判定する
// x0.x > x0.y なら右下の三角形 (1, 0)
// そうでなければ左上の三角形 (0, 1)
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);

// 4. 残りの2つの頂点への距離ベクトル (x1, x2) を計算
vec2 x1 = x0 - i1 + C.xx;         // 中間頂点へのベクトル
vec2 x2 = x0 - 1.0 + 2.0 * C.xx;  // 反対側の頂点へのベクトル

たった1行の (x0.x > x0.y) というシンプルな条件分岐で、自分が属する三角形を特定しています。この判定の軽さが、GPU 上で爆速で動く理由の一つです。 これで、影響を受ける 3 つの頂点への距離ベクトル x0, x1, x2 がすべて揃いました。

Step 3: 勾配の計算と補間(各頂点からの影響度を計算し、合算する)

最後に、3 つの頂点それぞれに「ランダムな方向(勾配ベクトル)」を持たせ、現在地との内積を取ります。そして、「頂点から離れるほど影響力がゼロになる」 ような減衰関数(Falloff)を掛け合わせます。

// 5. 減衰関数の計算(距離の2乗が 0.5 を超えると影響力ゼロになる)
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0);

// スムーズな曲線にするために4乗する
m = m * m;
m = m * m;

// 6. 各頂点のハッシュ(乱数)から勾配ベクトルを求め、内積を取る
// (※ハッシュ関数による疑似乱数生成部分は省略)
vec3 gradients = /* 頂点の座標(i)から生成されたランダムな勾配 */;
vec3 p = /* 勾配ベクトルと距離ベクトル(x0,x1,x2)の内積 */;

// 7. 減衰関数と掛け合わせて、3つの頂点の影響をすべて足し合わせる
float n = dot(m, p);

// 最終的に -1.0 ~ 1.0 の範囲に正規化して出力
return 130.0 * n;

ここでのポイントは max(0.5 - 距離の2乗, 0.0) という減衰関数です。 距離が一定以上離れると、計算結果が強制的に 0.0 になります。つまり、自分が属していない遠くの頂点からの影響を完全にシャットアウトしているのです。

この「影響範囲が円形(球形)にスッと消える」性質のおかげで、隣の三角形に移動してもノイズの境界線にカクつきが生まれず、非常に滑らかな連続性が保たれます。


これが Simplex Noise が内部で行っている「歪み → 判定 → 補間」の全貌です。

Ashima Arts のコードは一見すると魔法の暗号のように見えますが、こうしてステップごとに分解すると、Ken Perlin の「四角形を三角形にすれば計算が減る」というアイデアが、いかに美しく Shader に翻訳されているかが分かりますね。

4. 3D、そして 4D へ:時間をパラメータにする

ここからが、Simplex Noise 最大の真骨頂です。 Perlin Noise から Simplex Noise へとアルゴリズムを進化させた最大の理由は、「次元が増えたときの計算量の爆発を防ぐため」でした。

例えば 4次元(4D)のノイズを計算する場合、従来の Perlin Noise では四角形(超立方体)の頂点である 16個 ものポイントの勾配を計算して補間する必要があります。しかし、Simplex Noise(超四面体)であれば、計算すべき頂点はわずか 5個 で済みます。

この圧倒的な計算コストの削減が、私たちに「時間」という強力なパラメータを Shader 内で自由に使いこなす特権を与えてくれます。

「動くノイズ」の正体:次元を1つ切り下げる

ノイズをアニメーションさせようと思ったとき、初心者が最初によくやってしまうのが「2Dノイズの座標に時間を足す」というアプローチです。

// ❌ ただのスクロール(風で流れているだけに見える)
float n = snoise(vec2(uv.x + u_time, uv.y));

これだと、ノイズの模様自体は変わらず、画像が横にスライドしていくだけ(パンニング)になってしまいます。 私たちが本当に欲しいのは、その場でグニョグニョと形を変えながらうねるような、生命感のある動きのはずです。

これを実現するために「次元を1つ上げる」という魔法を使います。 2D の画面にうねるノイズを描画したいなら、3D ノイズ を呼び出し、その 軸(奥行き)に時間(u_time)を代入するのです。

// ✨ 3DノイズのZ軸を「時間」として使う(その場で形がうねる!)
float n = snoise(vec3(uv.x, uv.y, u_time * 0.5));

これは視覚的に言うと、「3次元空間に固定された雲のような立体ノイズの中を、2次元の平面(スクリーン)が時間とともに前進しながらスライスし続けている状態」です。断面が刻一刻と変化するため、結果として2D画面上ではノイズが滑らかに変形し続けているように見えます。

究極の 4D ノイズ:3D空間 × 時間

では、Three.js などで「3Dの球体」の表面をノイズでボコボコに揺らしたい場合はどうなるでしょうか?

この場合、すでに頂点の座標として $X, Y, Z$ の 3次元を使い切ってしまっています。

ここで登場するのが、4D(4次元)ノイズ です。

空間の $X, Y, Z$ に加えて、4次元目($W$軸)に時間を流し込みます。

// 🌌 4Dノイズ:3Dオブジェクトの表面が時間とともにうねる
float n = snoise(vec4(position.x, position.y, position.z, u_time));

人間には 4次元空間を視覚的に想像することは困難ですが、数学的には何の問題もありません。「時間経過とともに連続的に変化し続ける 3D 空間のノイズ」がこれで完成します。

流体シミュレーションを使わずに燃え盛る炎を作ったり、魔法陣から立ち昇るプラズマオーラを描画したり、あるいは #09 で触れた Volumetric Clouds(体積雲)を風に流すだけでなく「雲自体をモクモクと変化させる」といった表現は、すべてこの 「時間入り高次元 Simplex Noise」 が土台になっています。

そして何より素晴らしいのは、これほど複雑な 4D の計算をピクセル(フラグメント)ごとに毎フレーム 60fps で実行しても、Simplex Noise なら GPU は平然と処理しきれてしまう という事実です。


次元の壁を越え、空間と時間を歪める。 これで私たちは、パフォーマンスと表現力を両立した「次世代の魔法の基盤」を完全に手に入れました。

5. まとめと次回予告 — 次世代の魔法を手に、さらなる深淵へ

「四角形から正三角形へ」という理論の閃きから始まり、空間を歪ませる Skewing 係数の謎を解き明かし、ついに 4D(空間+時間)でうねるノイズを GLSL で実装するところまで辿り着きました。

今回、私たちが Simplex Noise という「次世代の魔法の基盤」を手に入れたことの最大の意義は、「圧倒的なパフォーマンスの余裕」 が生まれたことです。

第5回(#05)で解説した FBM(Fractal Brownian Motion) を思い出してください。

ノイズを自然界の複雑さに近づけるためには、周波数と振幅を変えながらノイズ関数を何回も重ね合わせる(オクターブを増やす)必要がありました。重い Perlin Noise で 4D の FBM を計算しようとすれば、あっという間に GPU は悲鳴を上げ、フレームレートは低下してしまいます。

しかし、Simplex Noise なら違います。 その極めて軽量な計算コストのおかげで、4Dノイズを何層にも重ねた FBM をリアルタイムで、しかもピクセルシェーダー上で軽々と動かすことができるのです。

これこそが、Procedural な表現において Simplex Noise が「業界標準(デファクトスタンダード)」として君臨し続けている理由です。計算の軽さは、そのまま「表現の豊かさ」に直結します。

次回予告:Simplex Noise を解き放つ(仮)

理論と実装の基盤は、これで完全に固まりました。 基礎層、アルゴリズム層を抜け、私たちの旅はいよいよ「これらをどう組み合わせて美しい世界(魔法)を創り出すか」という、Shader / WebGL の実践的な応用レイヤーへと突入していきます。

次回(#16)は、今回手に入れた 4D Simplex Noise と FBM、そして Domain Warping(#06)をすべて融合 させます。 静止していたノイズが「時間」を手に入れたことで、ただの模様が「燃え盛る炎」「うねるプラズマ」「空間の歪み」といった Procedural VFX(エフェクト) へと進化する瞬間を、実際のコードとともに体験していきましょう。

ノイズが「アート」になる瞬間をお楽しみに!