[Noise 入門 #08] Curl Noise — ノイズは“流れ”になる。流体シミュレーションを使わずに流体を作る

はじめに

前回の記事では、Domain Warping を実装し、空間そのものを歪めることで「複雑な地形」や「エネルギー表現」を作り出しました。

しかし、まだ足りないものがあります。

「流れ(Flow)」です。

通常の Perlin Noise や FBM は、あくまで「高さ(値)」を返します。
これをそのまま粒子の動き(速度)に使うと、粒子は一点に集まってしまったり、逆に散り散りになったりします。

自然界の「水」や「煙」はそう動きません。
水はどこからも湧き出さないし、どこへも消えない。ただ、渦を巻きながら循環し続けます。

この「圧縮されない、滑らかな渦の動き」を、物理演算なしで、ノイズだけで作り出す魔法。
それが Curl Noise です。

今回は、静止したノイズの世界から、動き続ける流体の世界へと足を踏み入れます。

前回の記事:

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;

これを実行するとどうなるか? 期待するのは「煙のように漂う動き」ですが、現実は残酷です。

失敗の光景:パーティクルの死

画面上のパーティクルは、最初は動きます。しかし数秒後、奇妙なことが起こります。

  1. 凝集(Clumping): ある場所にパーティクルがギュッと固まり、団子状になって動かなくなる。
  2. 空洞(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$$

(ベクトルの回転の発散は常にゼロである)

つまり、

  1. 適当なノイズ(スカラー場やベクトルポテンシャル)を用意する。
  2. それを微分して「回転(Curl)」の形に変換する。
  3. その結果を速度として使う。

これだけで、物理演算を一切しなくても、数学の力によって「絶対に圧縮されず、どこへも消えず、永遠に循環し続ける流れ」が手に入るのです。

これが Curl Noise の正体です。 地形の傾きを「滑り落ちる力」として使うのではなく、「その周りを回る力」として再解釈するテクニックなのです。

2. Curl Noise の正体 — 勾配を「ねじる」

「発散ゼロ」なんて難しい言葉を使うと、なんだか高度な物理演算が必要な気がしてきます。 でも、そのアルゴリズムの核心は、驚くほどシンプルで幾何学的です。

一言で言えば、「坂を転がり落ちる力を、横に滑る力に変える」だけです。

山登りで理解する「勾配(Gradient)」

想像してみてください。あなたは今、Perlin Noise で作られたボコボコした地形(山)の上にいます。

通常のノイズ活用法では、この地形の「傾き」を計算します。これを 勾配(Gradient) と呼びます。 勾配ベクトル は、常に「最も急な坂を登る方向」を指します。

もし、この勾配をそのまま粒子の速度に使ったらどうなるでしょうか?

  1. 粒子は、山の頂上(極大値)に向かって全力で登ります。
  2. あるいは、符号を反転させれば、谷底(極小値)に向かって転がり落ちます。
  3. そして、そこで止まります。

これでは「流れ」になりません。ただの「集まり」です。水たまりに水が溜まって動かなくなるのと同じ現象です。

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)」 という原始的ですが強力な手法を使います。

差分法とは?

「この場所の坂の角度を知りたい」と思った時、あなたならどうしますか? 数式がわからなくても、こうすれば測れます。

  1. 今の場所から、ほんの少し右()へ進んで高さを測る。
  2. 今の場所から、ほんの少し左()へ戻って高さを測る。
  3. その差(右の高さ - 左の高さ) が、坂の傾きです。

これを 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 ライブラリを使って、以下のようなシミュレーション・ループを回します。

  1. 初期状態: 画面全体にランダムに粒子をばら撒く。
  2. 更新ループ(毎フレーム):
  • 「ねえ、今君がいる場所の風向きはどうなってる?」と関数に聞く。
  • その風向きに沿って、少しだけ移動する。
  • それを繰り返す。

コードのイメージはこうです。

// 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 は「動き(アニメーション)」だけに使われるわけではありません。
「時間の流れを、空間の線として固定する」ことで、形状(ジオメトリ)の生成に使えます。

  1. ある点からスタートする。
  2. Curl Noise のベクトルに従って少し移動し、線を引く。
  3. その先でまたベクトルを取得し、線を引く。
  4. これを繰り返す。

こうして描かれた線(流線 / 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)のある世界へ飛び込みます。
光の散乱、密度の蓄積、そして「空」そのものを計算する旅に出ましょう。