はじめに
前回の記事では、Domain Warping を実装し、空間そのものを歪めることで「複雑な地形」や「エネルギー表現」を作り出しました。
しかし、まだ足りないものがあります。
「流れ(Flow)」です。
通常の Perlin Noise や FBM は、あくまで「高さ(値)」を返します。
これをそのまま粒子の動き(速度)に使うと、粒子は一点に集まってしまったり、逆に散り散りになったりします。
自然界の「水」や「煙」はそう動きません。
水はどこからも湧き出さないし、どこへも消えない。ただ、渦を巻きながら循環し続けます。
この「圧縮されない、滑らかな渦の動き」を、物理演算なしで、ノイズだけで作り出す魔法。
それが Curl Noise です。
今回は、静止したノイズの世界から、動き続ける流体の世界へと足を踏み入れます。
前回の記事:
[Noise 入門 #07] Three.js + GLSLでDomain Warpingを実装する — 数式を世界に変換する
Noise 入門シリーズ第7回。Domain Warping を Shader で実装し、Three.js 上で動かします。理論からGPUコードへの変換、FBMとの融合、strengthパラメータの効果、時間拡張(4D化)までを整理し、ノイズを“世界生成アルゴ …
https://humanxai.info/posts/noise-intro-07-domain-warping-shader/1. なぜ普通のノイズでは「流れ」を作れないのか?
「ノイズを使ってパーティクルを動かしたい」 そう思った時、誰もが最初に思いつく直感的な実装があります。
「ノイズはランダムな値を返す関数だ。だから、X軸の速度にノイズを、Y軸の速度に別のノイズを入れれば、ランダムに動き回るんじゃないか?」
コードにするとこんな感じです。
// ❌ 失敗する実装例
float vx = noise(pos.x, pos.y);
float vy = noise(pos.x + 123.4, pos.y + 567.8); // オフセットで別の波形にする
vec2 velocity = vec2(vx, vy);
position += velocity * deltaTime;
これを実行するとどうなるか? 期待するのは「煙のように漂う動き」ですが、現実は残酷です。
失敗の光景:パーティクルの死
画面上のパーティクルは、最初は動きます。しかし数秒後、奇妙なことが起こります。
- 凝集(Clumping): ある場所にパーティクルがギュッと固まり、団子状になって動かなくなる。
- 空洞(Void): 逆に、パーティクルが絶対に寄り付かない「真空地帯」ができる。
なぜこうなるのでしょうか? それは、私たちが作ったベクトル場に「吸い込み口(Sink)」と「湧き出し口(Source)」が存在してしまっているからです。
ノイズの値(ハイトマップ)をそのまま速度として使うと、パーティクルはあたかも「坂道を転がるボール」のように、特定の凹みに落ち込んで動けなくなったり、特定の凸部から永遠に弾き飛ばされ続けたりします。
自然界のルール:「流体は圧縮されない」
ここで、自然界の「水」や「空気」の動きを思い出してください。
お風呂に手を入れてかき混ぜたとき、水はどこか一点に圧縮されて消えたりしません。 また、何もない空間から水が無限に湧き出すこともありません。
「入ってきた量だけ、出ていく」
これが、水や煙のような流体(非圧縮性流体)の鉄の掟です。 これを数学用語で Divergence Free(発散ゼロ) と呼びます。
$$\nabla \cdot \vec{v} = 0 \quad (\text{発散がゼロ})$$
- 発散が正(Positive Divergence): そこから水が湧き出している(Source)。
- 発散が負(Negative Divergence): そこへ水が吸い込まれて消滅している(Sink)。
- 発散がゼロ(Divergence Free): 水はただ通り抜けるか、回転しているだけ。
先ほどの「ノイズをそのまま速度にする」という方法は、数学的に発散がゼロになりません。だから、パーティクルが吸い込まれて消えたり(凝集)、湧き出してスカスカになったり(空洞化)するのです。
解決策:Curl(回転)演算子
では、どうすれば「発散ゼロ」のベクトル場を作れるのでしょうか? 複雑な流体シミュレーション(Navier-Stokes方程式)を解く必要があるのでしょうか?
いいえ、もっとズルい方法があります。 それが Curl(カール / 回転) です。
ベクトル解析には面白い性質があります。 「どんなスカラー場(地形)であっても、その『回転(Curl)』を取って作ったベクトル場は、常に発散がゼロになる」 という数学的保証があるのです。
$$\nabla \cdot (\nabla \times \vec{\Psi}) = 0$$
(ベクトルの回転の発散は常にゼロである)
つまり、
- 適当なノイズ(スカラー場やベクトルポテンシャル)を用意する。
- それを微分して「回転(Curl)」の形に変換する。
- その結果を速度として使う。
これだけで、物理演算を一切しなくても、数学の力によって「絶対に圧縮されず、どこへも消えず、永遠に循環し続ける流れ」が手に入るのです。
これが Curl Noise の正体です。 地形の傾きを「滑り落ちる力」として使うのではなく、「その周りを回る力」として再解釈するテクニックなのです。
2. Curl Noise の正体 — 勾配を「ねじる」
「発散ゼロ」なんて難しい言葉を使うと、なんだか高度な物理演算が必要な気がしてきます。 でも、そのアルゴリズムの核心は、驚くほどシンプルで幾何学的です。
一言で言えば、「坂を転がり落ちる力を、横に滑る力に変える」だけです。
山登りで理解する「勾配(Gradient)」
想像してみてください。あなたは今、Perlin Noise で作られたボコボコした地形(山)の上にいます。
通常のノイズ活用法では、この地形の「傾き」を計算します。これを 勾配(Gradient) と呼びます。 勾配ベクトル は、常に「最も急な坂を登る方向」を指します。
もし、この勾配をそのまま粒子の速度に使ったらどうなるでしょうか?
- 粒子は、山の頂上(極大値)に向かって全力で登ります。
- あるいは、符号を反転させれば、谷底(極小値)に向かって転がり落ちます。
- そして、そこで止まります。
これでは「流れ」になりません。ただの「集まり」です。水たまりに水が溜まって動かなくなるのと同じ現象です。
90度の魔法「Curl(回転)」
Curl Noise のアイデアは、ここで「進行方向を90度ねじる」というものです。
- 坂を「登る」のではなく、坂の「横」へ進む。
- 重力に逆らうのでもなく、従うのでもなく、等高線(Contour Line)に沿って歩く。
こうすると、粒子はどうなるでしょうか?
山頂を目指すことが禁止された粒子は、山頂の周りをぐるぐると回り始めます。 谷底に落ちることを許されない粒子は、谷の淵を永遠に周回します。
これが「渦(Curl)」です。
粒子は決して止まることなく、地形の周りをヌルヌルと滑るように移動し続けます。これが、流体のような「滞留しない動き」を生み出す正体です。
数式で見る(2次元の場合)
この「90度ねじる」という操作、数学的には非常に単純な成分の入れ替えで実現できます。
ある地点 におけるノイズの高さを とします。 通常の勾配(Gradient)ベクトルは、偏微分を使ってこう書けます。
$$\vec{g} = \nabla N = \left( \frac{\partial N}{\partial x}, \ \frac{\partial N}{\partial y} \right)$$
(X方向にどれくらい傾いているか、Y方向にどれくらい傾いているか)
これを90度回転させて、Curl(回転)ベクトル を作ります。 2次元ベクトルの直交を作る公式 を適用するだけです。
$$\vec{v} = \text{curl}(N) = \left( \frac{\partial N}{\partial y}, \ -\frac{\partial N}{\partial x} \right)$$
たったこれだけです。 「Yで微分したものをX成分に、Xで微分したものをY成分(マイナス)にする」。
この単純な操作が、数学的な奇跡を起こします。 勾配ベクトルと、この回転ベクトルを内積(Dot Product)にかけてみましょう。
$$\vec{g} \cdot \vec{v} = \left( \frac{\partial N}{\partial x} \cdot \frac{\partial N}{\partial y} \right) + \left( \frac{\partial N}{\partial y} \cdot -\frac{\partial N}{\partial x} \right) = 0$$
内積がゼロ。つまり、元の「坂を登る力」と「Curlの力」は完全に直交しています。 粒子は坂を一切登らず、高さ(ポテンシャル)を一定に保ったまま移動します。
これこそが、エネルギーが保存され、動きが永遠に続く「発散ゼロ」のフィールドの正体なのです。
Point
- Gradient Noise: 坂を転がるボール(やがて止まる)
- Curl Noise: 惑星の周りを回る衛星(回り続ける)
ノイズを「高さ」ではなく「ポテンシャル(場のエネルギー)」として扱うことで、静止画の世界から動画(ダイナミクス)の世界へ移行できるのです。
3. GLSL で Curl Noise を実装する
理論がわかったところで、GPU(GLSL)の世界に落とし込みます。 ここで一つ壁があります。
「GLSL には微分関数がない」
数学の教科書なら と書けば終わりですが、シェーダーの中では「この関数の傾きを教えてくれ」と命令することはできません(dFdx などの関数はありますが、これはスクリーン空間の微分であり、3D空間のノイズ計算には使えません)。
そこで、「差分法(Finite Difference)」 という原始的ですが強力な手法を使います。
差分法とは?
「この場所の坂の角度を知りたい」と思った時、あなたならどうしますか? 数式がわからなくても、こうすれば測れます。
- 今の場所から、ほんの少し右()へ進んで高さを測る。
- 今の場所から、ほんの少し左()へ戻って高さを測る。
- その差(右の高さ - 左の高さ) が、坂の傾きです。
これを X, Y, Z の3軸すべてで行います。 つまり、1つの地点の Curl を計算するために、その周囲 6ヶ所 のノイズをサンプリングします。
GLSL コード解説
以下は、3次元空間で Curl Noise を計算する標準的な実装です。
Simplex Noise などのノイズ関数(snoise)が定義されている前提で動きます。
// 1. 微小な値(EPSILON)
// これが「ほんの少しズレる」ための歩幅です。
// 小さすぎると精度誤差が出ますが、大きすぎると大雑把になります。
const float EPSILON = 0.001;
// 2. 3次元 Curl Noise 関数
// 入力: 座標 p (vec3)
// 出力: 速度ベクトル (vec3)
vec3 curlNoise(vec3 p) {
// 3. 周囲6点のサンプリング(中心差分)
// p を中心に、X, Y, Z それぞれの正方向・負方向にズレた値を取ります。
// Y軸方向の勾配用
float n1 = snoise(p + vec3(0.0, EPSILON, 0.0));
float n2 = snoise(p - vec3(0.0, EPSILON, 0.0));
// Z軸方向の勾配用
float n3 = snoise(p + vec3(0.0, 0.0, EPSILON));
float n4 = snoise(p - vec3(0.0, 0.0, EPSILON));
// X軸方向の勾配用
float n5 = snoise(p + vec3(EPSILON, 0.0, 0.0));
float n6 = snoise(p - vec3(EPSILON, 0.0, 0.0));
// 4. 偏微分の近似
// (f(x+e) - f(x-e)) という操作です。本来は 2*EPSILON で割るべきですが、
// 後でまとめて係数を掛けるので、ここでは差分だけを取ります。
float x = n1 - n2; // ∂N/∂y に相当
float y = n3 - n4; // ∂N/∂z に相当
float z = n5 - n6; // ∂N/∂x に相当
// 5. カール(回転)の構成
// ここが魔法のスパイスです。
// 単純な勾配 (dx, dy, dz) を使うのではなく、成分を入れ替えて引き算します。
// 数学的には ∇ × Ψ (ベクトルポテンシャルの回転) を計算しています。
vec3 curl;
// X成分: ∂N/∂y - ∂N/∂z
curl.x = x - y;
// Y成分: ∂N/∂z - ∂N/∂x
curl.y = y - z;
// Z成分: ∂N/∂x - ∂N/∂y
curl.z = z - x;
// 6. 正規化とスケーリング
// 速度として使いやすい大きさに調整して返します。
// (流体の激しさを調整したい場合は、ここで strength を掛けます)
return curl * (1.0 / (2.0 * EPSILON));
}
コードの裏側にある「最適化と妥協」
このコードを見て、「あれ? 3次元の回転(Curl)って、もっと複雑じゃなかったっけ?」と思った方は鋭いです。
厳密な流体力学のシミュレーション(Vector Potential 法)では、X, Y, Z それぞれに独立した3つのノイズ関数を用意し、それらを合成して回転させるのが最も「正しい」手法です。
$$\vec{\Psi} = (\text{Noise}_1, \text{Noise}_2, \text{Noise}_3)$$
しかし、シェーダーアートの世界では「計算負荷」が敵です。 ノイズを3回呼ぶとその分重くなります(上記の差分法だと 回ものサンプリングが必要になってしまいます)。
そこで、上記のコードは「1つのノイズ関数 を使い回す」というトリックを使っています。 あたかも であるかのように扱い、その回転を取っています。
数学的には簡易版ですが、視覚的にはこれで十分すぎるほど「流体っぽい」動きが生まれます。 「正しさ」より「それっぽさと速さ」を取る。 これもまた、リアルタイムグラフィックスの重要な技術です。
4. 視覚化:パーティクルを流す
数学的な準備はすべて整いました。
手元にあるのは curlNoise(pos) という関数。これは、空間のあらゆる地点における「風の向き(ベクトル)」を教えてくれる羅針盤です。
では、この羅針盤を使って、何もない空間に数万個の粒子(パーティクル)を解き放ってみましょう。
ここからは静止画ではありません。シミュレーションの世界です。
命を吹き込むループ
Three.js などの WebGL ライブラリを使って、以下のようなシミュレーション・ループを回します。
- 初期状態: 画面全体にランダムに粒子をばら撒く。
- 更新ループ(毎フレーム):
- 「ねえ、今君がいる場所の風向きはどうなってる?」と関数に聞く。
- その風向きに沿って、少しだけ移動する。
- それを繰り返す。
コードのイメージはこうです。
// GLSL (GPGPU) 上での位置更新ロジック
// 1. 現在の位置を取得
vec3 currentPosition = texture2D(positions, uv).xyz;
// 2. その場所の「流れ」を計算(ここが核心!)
// frequency: ノイズの細かさ
// strength: 流れる勢い
vec3 velocity = curlNoise(currentPosition * frequency) * strength;
// 3. 未来の位置へ移動
// deltaTime: 前フレームからの経過時間
vec3 newPosition = currentPosition + velocity * deltaTime;
// 4. 保存(次のフレームの currentPosition になる)
gl_FragColor = vec4(newPosition, 1.0);
その瞬間、世界が変わる
このプログラムを実行した瞬間、モニターの中の点は、もはや「点」ではなくなります。
それらは「意思を持った群れ」のように動き出します。
ある場所では大きな川のように合流し、ある場所では渦を巻いて滞留し、決して互いに衝突して消滅することなく、滑らかにすれ違います。
- ランダムノイズの動き: TVの砂嵐。チカチカして落ち着きがない。
- Curl Noise の動き: 水面に浮いたインク。煙草の煙。オイルの中の気泡。
「ヌルヌル動く」
「生きているみたい」
「ずっと見ていられる」
これが Curl Noise を初めて実装した人が必ず口にする感想です。
「発散ゼロ(非圧縮)」という数学的特性が、人間の目には「有機的な自然さ」として映るのです。
神のパラメータ:世界をどう動かすか?
この「流れ」の性格を決める重要なパラメータがいくつかあります。
これを調整することで、あなたの作る世界は「深海」にも「嵐」にもなります。
1. Frequency(周波数): 渦のサイズ
ノイズの座標にかけるスケール値です (curlNoise(pos * frequency))。
-
Low Frequency (0.1 ~ 0.5):
-
雄大な海流。画面全体を大きくうねるような、巨大な渦が生まれます。ゆったりとしたアンビエントな表現に最適です。
-
High Frequency (2.0 ~ 5.0):
-
乱流(Turbulence)。細かく荒ぶる流れになります。沸騰するお湯や、激しい炎の内部のような、エネルギー密度の高い表現になります。
2. Strength(強度): 流速
ベクトルに乗算する値です。
- これは単純に「風速」です。値を大きくすれば嵐のように激しく飛び回り、小さくすれば水槽の中のように静かに漂います。
- 場所によって Strength を変える(例えば中心だけ速くする)と、ブラックホールのような吸い込み効果も作れます。
3. Time(時間経過): 流れそのものの変化
ここが一番の魔法です。
curlNoise の内部で参照するノイズの座標に、時間を加えます。
$$\text{Noise}(x, y, z + \text{time} \times \text{speed})$$
-
Time = 0 (固定):
-
「凍った川」です。流れのルート(流線)は固定されており、粒子はレールの上を走るように同じ軌跡をなぞります。
-
Time > 0 (動かす):
-
「生きている川」です。時間の経過とともに、渦の位置や形そのものが変化します。
-
粒子は「さっきまでは右に曲がっていたのに、今は左に流される」という複雑な挙動を見せます。煙や雲の表現には、この「場の変化」が不可欠です。
Note: 残像(Trails)の魔力 Curl Noise の美しさを最大限に引き出すテクニックがあります。それは「残像」を残すことです。
粒子そのものを見るのではなく、粒子が通った「軌跡(Trail)」を描画すると、空間に見えない「チューブ」や「毛細血管」が走っているような、驚異的な構造が浮かび上がります。
5. 応用:Curl Noise が作る世界
Curl Noise は、ジェネラティブアートやゲーム開発の現場における「魔法の調味料」です。
「物理演算をするほどでもないが、ランダムだと安っぽい」
そんな時、エンジニアは迷わず Curl Noise を取り出します。
重い計算コストを払わずに、あたかも高度な流体シミュレーションが行われているかのような「フェイク」を作り出す。この技術がどこで使われているか、その実例を見てみましょう。
① 煙・炎のエフェクト — 「上昇気流」との融合
ゲームの中で焚き火の煙や、爆発の炎を見たことがあるでしょう。
あれらを真面目に流体力学(ナビエ・ストークス方程式)で計算すると、最新のGPUでも悲鳴を上げます。
そこで Curl Noise の出番です。
作り方はシンプルです。
「上に向かう力(浮力)」と「Curl Noise(乱流)」を足し合わせるのです。
- Curl Noise: その場でくるくると回る渦の動きを作る。
- Up Vector: のような、ただ上に昇る力。
この2つを合成すると、「渦を巻きながら、ゆらゆらと昇っていく動き」が生まれます。
直線的な上昇でもなく、ランダムな拡散でもない。
熱せられた空気が周囲を巻き込みながら上昇する、あの複雑な「火の揺らぎ」が、たったこれだけの足し算で再現できてしまうのです。
② 髪の毛・ファーの生成 — 「静止した時間」の流れ
Curl Noise は「動き(アニメーション)」だけに使われるわけではありません。
「時間の流れを、空間の線として固定する」ことで、形状(ジオメトリ)の生成に使えます。
- ある点からスタートする。
- Curl Noise のベクトルに従って少し移動し、線を引く。
- その先でまたベクトルを取得し、線を引く。
- これを繰り返す。
こうして描かれた線(流線 / Streamline)は、「髪の毛」や「動物の毛並み」そのものです。
普通のノイズだと線が交差したり、一点に集まったりして「ぐしゃぐしゃ」になりますが、Curl Noise は「発散ゼロ」の性質上、線同士が一定の距離を保ちながら、綺麗に並んでうねります。
ゴッホの『星月夜』のような筆致や、筋肉の繊維のような有機的なパターン。これらは全て、ベクトル場を可視化したものと言えます。
③ 惑星のガス表現 — 「混ぜ合わせる」技術
木星の大赤斑のような、巨大なガスの惑星。
あれは、色の異なるガスが何億年もかけて「混ざり合っている(Mixing)」状態です。
前回の記事で紹介した Domain Warping(座標の歪み)だけだと、どうしても「引き伸ばされたゴム」のような質感になりがちです。
しかし、歪ませる力として Curl Noise を使うと、結果は劇的に変わります。
流体が回転しながら混ざり合うため、色は引き伸ばされるだけでなく、「マーブル模様」のように複雑に巻き込まれていきます。
インクを水に垂らしてかき混ぜた瞬間のような、あの美しいカオス。
これを Shader 上で再現するには、Curl Noise が不可欠です。
Point: “Fake Physics” の美学
重要なのは、これらが全て「物理的には正しくない」ということです。
本当の空気抵抗も、圧力計算もしていません。
しかし、人間の目には「リアル」に見えます。
「計算は嘘をつくが、結果は真実を語る」
これこそが、プロシージャル・モデリングとVFXの真髄であり、Curl Noise がその王様である理由です。
6. まとめ — 静止画から「動画」へ
旅の始まりを思い出してください。
最初は、ただの「ランダムな数値の並び」でした。それが線形補間によって「滑らかな波」になり、フラクタル合成(FBM)によって「地形」になりました。
そして今回、数学的な手術 —— Curl(回転)演算子 —— を通すことで、ノイズはついに「流体」へと進化しました。
ここで、私たちが手に入れた武器を整理しましょう。
-
Scalar Noise(通常のノイズ):
-
役割: 高さ、濃度、温度を決める。
-
結果: 山、雲の形、模様(静的な構造)。
-
Vector Noise(Curl Noise):
-
役割: 風向き、水流、動きを決める。
-
結果: 煙の揺らぎ、川の流れ、毛並み(動的なエネルギー)。
この2つは、車の「車体」と「エンジン」のような関係です。
前回の Domain Warping(空間の歪み) が「形を複雑にする技術」だとすれば、今回の Curl Noise(空間の回転) は「命を吹き込む技術」です。
「形」と「動き」。
この2つが揃った今、もはや表現できない自然現象はほとんどありません。
次回予告:空を見上げろ
道具は全て揃いました。
次回は、これまでの知識(FBM、Warping、Curl)を総動員して、プロシージャル・アートの「王道にして頂点」に挑みます。
「雲」です。
ただし、ペラペラの板に貼り付けたテクスチャではありません。
中に入って飛ぶことができる、密度と厚みを持った「ボリューメトリック(体積)な雲」です。
次回、
[Noise 入門 #09] Volumetric Clouds — ノイズで空に雲を浮かべる(レイマーチング基礎)
平面(Surface)の世界から、ついに体積(Volume)のある世界へ飛び込みます。
光の散乱、密度の蓄積、そして「空」そのものを計算する旅に出ましょう。
💬 コメント