[Noise 入門 #58] Curl Noise × Audio — 音に乗って舞うパーティクル

[Noise 入門 #58] Curl Noise × Audio — 音に乗って舞うパーティクル

はじめに

今回は、第5集で苦労して組み上げた「GPGPUによる100万のパーティクルシミュレーション」と「Curl Noise(回転ノイズ)」のベクトル場に、前回の#57で取得した「音楽(Audio Data)」を流し込みます。

静かにうねっていた光の粒子たちが、キック(低音)の重低音で弾け飛び、ハイハット(高音)の刻みに合わせて細かい渦を巻く。音が物理的な「風」や「重力」となって空間を支配する、圧倒的なオーディオ・ビジュアル体験を錬成しましょう。

前回の記事:

1. 音楽が「流体」を支配する仕組み — 数式と音波の同期

これまで、Curl Noise(回転ノイズ)を用いたパーティクルは「静かな川」や「一定の風」のように、固定された数式のルールの下で美しく流れていました。しかし、今回はここに「音楽」という時間軸のエネルギー(外部パラメータ)を介入させます。

第5集で実装した通り、GPGPUによる100万のパーティクルは、テクスチャ(FBO)に保存された自身の座標データ(RGB = XYZ)を毎フレーム読み込み、そこへ「移動ベクトル」を足し合わせることでアニメーションしています。このなめらかな移動ベクトルを生み出すのが、発散ゼロ(Divergence-Free)の性質を持つ Curl Noise でした。

この Curl Noise の引数(空間の歪み具合)や、算出されたベクトル(移動の速さ)に対して、Web Audio APIのFFT(高速フーリエ変換)解析から抽出した「周波数ごとの音の強さ(0.0〜1.0)」を直接掛け合わせます。音波という物理現象で、流体力学のシミュレーションをリアルタイムにハックするのです。

具体的には、周波数帯域を大きく3つに分割し、それぞれに別々の「役割(物理法則への介入方法)」を与えます。

低音(Bass / Kick): 運動エネルギーの爆発

キックドラムやベースラインといった低音域は、楽曲の「心臓の鼓動」です。 このパラメータは、パーティクルの「移動スピード(Velocity)」や「推力」に直結させます。

普段は静かに漂っているパーティクルが、ドンッ! というキックの重低音が鳴った瞬間に一気に加速し、空間の奥へと吹き飛ぶ。あるいは、Curl Noiseの振幅(Amplitude)を一時的に跳ね上げることで、流体が怒り狂ったように荒れ狂う。低音はシミュレーションにおける「物理的な衝撃波」として機能します。

中音(Mid / Vocal & Synth): 空間のうねりと歪曲

ボーカル、シンセサイザー、スネアなど、楽曲のメインメロディを構成する中音域。 このパラメータは、Curl Noiseの「サンプリング座標のスケール(Frequency)」や「渦の大きさ」に介入させます。

中音が強く鳴り響くとき、ノイズ空間そのものがギュルギュルとねじ曲がり、大きな渦が急激に細かく分裂したり、逆に大きくうねったりします。流体の軌道そのものをリアルタイムに変形させることで、空間自体が音楽に合わせて呼吸するような、生々しく有機的な挙動を生み出します。

高音(High / Hi-Hat): 視覚的な明滅と微細な乱れ

ハイハットやシンバルのような鋭く細かい高音域。 この帯域は物理的な大きな移動よりも、「パーティクルの発光(Color / Emission)」や「微小な振動(Jitter)」と連動させます。

チキチキ とした細かなリズムに合わせて、100万の粒子が一斉に真っ白にフラッシュしたり、青からマゼンタへと色が変異したりします。低・中音で作られたダイナミックな流体のシルエットに対して、エッジの効いたディテールとサイバーパンク的な明滅を付与するスパイスの役割を果たします。

💡 空間が「音の彫刻」になる瞬間

画面下部で棒グラフが上下するような古典的なオーディオビジュアライザーとは異なり、音の波形がそのまま「流体の物理パラメータ」として変換されます。これにより、無音時は静かな煙のように振る舞い、ドロップ(サビ)で全帯域の音が爆発した瞬間には猛烈な光の竜巻へと変貌する。シミュレーションが文字通り「音楽の化身」となる、圧倒的な没入感を生み出すことができるのです。

2. GPGPU シミュレーション用 Shader (Compute Shader) — 数式への介入

それでは、100万のパーティクルの「脳」とも言えるGPGPU用のフラグメントシェーダー(Compute Shader相当)を見ていきましょう。 第5集で構築したPing-Pongバッファのシステムの上に、u_audioBassu_audioMid という2つのUniform変数を接続します。

ここでは、複雑な curlNoise 関数の内部ロジックはあえて省略し、「取得した位置座標に対して、音のデータがどのように介入するか」という力の流れ(データフロー)に注目してください。

// GPGPU Position Update Shader
uniform float u_time;
uniform float u_audioBass; // 低音の強さ (0.0 ~ 1.0)
uniform float u_audioMid;  // 中音の強さ (0.0 ~ 1.0)
uniform vec2 resolution;
uniform sampler2D texturePosition;

// Curl Noise 関数(第5集で実装したものをそのまま使用)
vec3 curlNoise(vec3 p) {
    // ... (偏微分と外積を用いたCurl Noiseの計算ロジック) ...
    return curl; // 発散ゼロの移動ベクトル(vec3)を返す
}

void main() {
    // 1. 現在のUV座標から、このピクセルが担当するパーティクルの現在位置(XYZ)を取得
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    vec3 pos = texture2D(texturePosition, uv).xyz;

    // 2. 【中音域の介入】Curl Noiseのサンプリング座標(渦の細かさ)を変形
    // 普段は1.5の周波数だが、中音(ボーカルやシンセ)が鳴るほどノイズ空間がギュッと縮む
    float noiseFrequency = 1.5 + (u_audioMid * 2.0);

    // Curl Noiseから、その座標で受ける「風の向き(ベクトル)」を取得
    vec3 velocity = curlNoise(pos * noiseFrequency + u_time * 0.1);

    // 3. 【低音域の介入】移動スピードの爆発
    // ベースとなる静かな移動速度(0.01)に対し、キックのパワーを乗せて強制加速させる
    float speed = 0.01 + (u_audioBass * 0.05);

    // 位置ベクトルを更新
    pos += velocity * speed;

    // ※ここで空間外に出たパーティクルを中心に戻すなどのリセット処理が入る(省略)
    // ...

    // 新しい位置(XYZ)をテクスチャ(FBO)に書き込んで保存
    gl_FragColor = vec4(pos, 1.0);
}

コードの解剖と「現象」の紐解き

この短いコードの中で、音波は「空間の歪曲(Frequency)」と「運動エネルギー(Speed)」の2つの物理法則を同時にハックしています。

  • 渦のスケールを操る (noiseFrequency) : u_audioMid が Curl Noise の引数に掛け合わされることで、見えないベクトル場の「うねりのサイズ」がリアルタイムに伸縮します。音が静かな時はゆったりとした大きな川の流れのようですが、メロディが盛り上がると、空間に無数の細かい竜巻が発生してパーティクルを複雑にかき混ぜ始めます。
  • 流体の限界突破 (speed) : このシェーダー最大の魔法がここです。基本となる移動係数 0.01 は、あくまで「微風」レベル。そこに u_audioBass のエネルギーが加算されることで、移動量が瞬時に数倍に跳ね上がります。

💡 【ディレクター・lainの視点(神の視点)】

実装を終えて画面を眺めるとき、この speed の計算式が最高のチューニングポイント(遊び場)になります。

例えば、EDMやダブステップなどの激しい楽曲を流した際、ドロップ(サビ)で u_audioBass が 1.0 近くまで跳ね上がります。その瞬間、静かに漂っていた100万のパーティクル群が、目に見えない巨大な衝撃波を受けたかのように一気に空間の奥深くへ吹き飛ぶのです。

もし手元の環境で動かしているなら、0.05 という係数を 0.5 など極端な値に書き換えてみてください。キックが鳴るたびに世界が弾け飛ぶ、鳥肌モノの暴走エフェクトを堪能できます。


3. 描画用 Shader (Particle Material) — 音の「瞬き」を視覚化する

GPGPUのCompute Shaderがパーティクルの「見えない脳と筋肉」だとしたら、こちらはそれを画面に焼き付ける「網膜」の役割を果たします。

計算された最新の座標(XYZ)をテクスチャから読み出し、Three.jsの空間に配置します。ここでは先ほどの低音・中音とは異なり、ハイハットやシンセサイザーなどの「高音(High)」のデータを受け取ります。高音の鋭いアタック感を利用して、パーティクルにサイバーパンク的な「ストロボ発光」の魔法をかけましょう。

// Particle Vertex Shader
uniform sampler2D texturePosition;
uniform float u_audioHigh; // 高音の強さ (0.0 ~ 1.0)

varying vec3 vColor;

void main() {
    // 1. GPGPUテクスチャからこの頂点の最新位置(XYZ)を取得
    vec3 pos = texture2D(texturePosition, position.xy).xyz;

    // 2. カメラの視点に合わせて画面上の位置を計算
    vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
    gl_Position = projectionMatrix * mvPosition;

    // 3. 遠近法(Perspective)の適用:遠くの粒は小さく、近くの粒は大きく
    gl_PointSize = (10.0 / -mvPosition.z);

    // --- 色の計算(オーディオ・リアクティブ) ---
    // 普段は深海のようなサイバーブルー、高音が鳴ると白っぽくフラッシュする
    vec3 baseColor = vec3(0.1, 0.5, 1.0);
    vec3 flashColor = vec3(1.0, 0.8, 1.0);

    // 高音の強さ(0.0~1.0)に応じて、2つの色を滑らかにブレンド(mix)する
    vColor = mix(baseColor, flashColor, u_audioHigh);
}
// Particle Fragment Shader
varying vec3 vColor;

void main() {
    // 1. gl_PointCoord (0.0~1.0の四角形) の中心を原点(0.0)にずらす
    vec2 uv = gl_PointCoord.xy - 0.5;

    // 2. 中心からの距離を測る
    float dist = length(uv);

    // 3. 半径0.5より外側は描画しない(四角形の角を削って丸にする)
    if (dist > 0.5) discard;

    // 4. グロウ効果:中心ほど濃く、外周に向かってフワッと消える光の粒にする
    float alpha = smoothstep(0.5, 0.1, dist);

    // Vertex Shaderから渡された色(vColor)と、計算した透明度(alpha)を適用
    gl_FragColor = vec4(vColor, alpha);
}

コードの解剖と「現象」の紐解き

WebGLにおける gl_PointSize を使ったパーティクル描画は、デフォルトではただの「四角いポリゴン」です。これをFragment Shaderの lengthsmoothstep で削り出し、中心が明るく発光する「球体(オーブ)」のように見せています。

そして視覚的なハイライトとなるのが、Vertex Shaderの mix(baseColor, flashColor, u_audioHigh) です。 u_audioHigh は、ドラムのハイハット(チキチキという音)や、鋭いシンセの音に反応して 0.0 から 1.0 へと激しく上下します。

  • 音が鳴っていない時(u_audioHigh = 0.0): baseColor が100%適用され、青く静かな粒子として描画されます。
  • 高音が弾けた瞬間(u_audioHigh = 1.0): flashColor が100%適用され、真っ白(あるいは薄いピンク)に発光します。

💡 【ディレクター・lainの視点(神の視点)】

ここも最高のチューニングポイントです。 baseColorflashColor のRGB値を書き換えるだけで、作品の「性格」が完全に裏返ります。

  • ダークファンタジー風: 普段は血のような赤(0.8, 0.0, 0.1)、高音で呪いのような紫(0.6, 0.0, 1.0)に。
  • 黄金の砂風: 普段は暗いオレンジ(0.5, 0.3, 0.0)、高音で眩いゴールド(1.0, 0.9, 0.5)に。

さらに、u_audioHigh の値に対して pow(u_audioHigh, 2.0) のように累乗をかけると、小さな音は無視され、「本当に強い高音が鳴った時だけパキッとフラッシュする」という、よりメリハリの効いたクラブライティングのような挙動を作ることも可能です。

4. Three.js 側での音響連携 (JavaScript) — 数式と音楽の架け橋

Shader側で受け入れる準備が整ったら、次はそのデータを流し込むJavaScript(Three.js)側の処理です。 前回(#56, #57)で構築した Web Audio API の AnalyserNode を使い、現在流れている音楽の「周波数(FFT)データ」を毎フレーム取得し、GLSLが扱いやすい形に変換して転送します。

ここでは「生の音響データをそのまま渡さない」という、オーディオビジュアル表現における極めて重要なテクニックが登場します。

// アニメーションループ内での処理
function render() {
    requestAnimationFrame(render);

    // 1. 周波数データを取得 (0 ~ 255の数値が配列に格納される)
    analyser.getByteFrequencyData(dataArray);

    // 2. 帯域ごとにエネルギーの平均値を計算し、0.0 ~ 1.0 に正規化
    // 配列のインデックスが低いほど低音、高いほど高音
    let bass = getAverage(dataArray, 0, 10) / 255.0;
    let mid  = getAverage(dataArray, 10, 100) / 255.0;
    let high = getAverage(dataArray, 100, 200) / 255.0;

    // 3. スムージング(線形補間 / Lerp)★超重要
    // 音のデータは毎フレーム激しく上下するため、そのまま渡すと視覚的にカクつく。
    // 前フレームの値に滑らかに近づけることで、有機的な動き(余韻)を作る。
    smoothedBass += (bass - smoothedBass) * 0.1;
    smoothedMid  += (mid - smoothedMid) * 0.1;
    smoothedHigh += (high - smoothedHigh) * 0.2; // 高音は少し反応を早くする(キレを出す)

    // 4. Shader (Uniform) へ送信
    gpgpuMaterial.uniforms.u_audioBass.value = smoothedBass;
    gpgpuMaterial.uniforms.u_audioMid.value = smoothedMid;
    particleMaterial.uniforms.u_audioHigh.value = smoothedHigh;

    // GPGPUの計算とThree.jsのレンダリング
    // ...
}

// 配列の特定範囲の平均を出すヘルパー関数
function getAverage(data, startIndex, endIndex) {
    let sum = 0;
    for (let i = startIndex; i < endIndex; i++) sum += data[i];
    return sum / (endIndex - startIndex);
}

コードの解剖と「現象」の紐解き

Web Audio APIの getByteFrequencyData は、音を周波数帯域(低音〜高音)に分割し、それぞれの強さを 0 から 255 までの整数値として返してくれます。

  • 帯域の分割(Band Splitting): 配列の最初の方(0 ~ 10)がキックドラムなどの重低音、真ん中(10 ~ 100)がボーカルやシンセ、後ろの方(100 ~ 200)がハイハットやシンバルに該当します。これを分割して getAverage で平均化し、さらに 255.0 で割ることで、Shaderが扱いやすい 0.0 ~ 1.0 の数値(正規化)に落とし込んでいます。
  • オーディオビジュアルの要「スムージング」: 生音のデータは非常にトゲトゲしています。キックが鳴った瞬間に 1.0 になり、次のフレームで 0.1 に落ちるようなことが平気で起こります。これをそのままパーティクルの速度(speed)に繋ぐと、流体が「カクッ、カクッ」と痙攣しているように見えてしまい、美しくありません。 そこで smoothedBass += (bass - smoothedBass) * 0.1; という線形補間(Lerp)を使います。これにより、音が消えた後もパーティクルが「フワッ……」と余韻を残して減速するようになり、物理シミュレーションとしての説得力(慣性)が生まれます。

💡 【ディレクター・lainの視点(神の視点)】

このJavaScript側は、Shaderをいじらなくても「動きの性格」をガラッと変えられる面白いポイントです。

ぜひ、スムージングの係数(0.10.2 の部分)をいじってみてください。

  • 係数を 0.50.9 にする(反応を早くする): パーティクルが非常に神経質になり、バキバキのテクノやグリッチミュージックに合う「キレッキレの動き」になります。
  • 係数を 0.01 にする(反応を極端に遅くする): 音の変化に対して非常に鈍感になり、まるで「水あめ」や「深海」の中で重低音を響かせているような、ドロドロとしたアンビエントな挙動に変わります。

また、getAverage のインデックス範囲を変えることで、「特定の楽器(例えばスネアドラムの周波数帯域だけ)に反応する魔法」にチューニングすることも可能です。

実装の核心:数学と音楽の融合

今回のプロジェクトで「生命を持った流体」を実現した4つの柱をまとめます。

1. GPGPUによる25万粒子の並列計算

  • 効率的なデータ処理: 512×512のテクスチャ(FBO)にパーティクルの座標(RGB = XYZ)を保存し、GPU上で一括計算することで、CPUの限界を超えた描画を実現しました。
  • 永続的な循環: 寿命(Life)の概念を導入し、エネルギーが尽きた粒子を中心の「コア」へリスポーンさせることで、爆発し続ける永続的なエフェクトを構築しています。

2. Curl Noise(回転ノイズ)による流体挙動

  • 発散ゼロのベクトル場: 物理的な流体シミュレーション(Navier-Stokesなど)を使わず、数学的なCurl演算のみで「うねり」を表現しています。
  • 音楽による空間歪曲: 音楽の中音域(Mid)をノイズの周波数(Frequency)に干渉させ、メロディに合わせて渦の細かさがリアルタイムに変化するようにハックしました。

3. Audio-Reactiveな物理法則の書き換え

  • 低音(Bass)の爆発力: キックの瞬間に移動速度(Speed)を指数関数的に跳ね上げ、視覚的な衝撃波(Explosion)を生み出しています。
  • 高音(High)の色彩変異: ハイハットやシンセの鋭い音に合わせてパーティクルをフラッシュさせ、サイバーパンク的な明滅を付与しました。
  • スムージングの魔法: 生の音響データをそのまま渡さず、線形補間(Lerp)を通すことで、粒子に「慣性」を感じさせる有機的な余韻を与えています。

4. 視覚的ディテール:Hot Core Effect

  • エネルギーの階層化: 誕生したばかりの粒子(Lifeが1.0に近い)ほど白く熱く発光させ、消えゆく粒子ほど深みのある色へ変化させることで、立体的なエネルギー密度を演出しました。
  • Additive Blending: パーティクル同士が重なるほど輝きを増す加算合成を最大限に活かし、中心部の「恒星のような輝き」を強調しています。

COMM_LOG: noise-intro-58-curl-noise-audio

NO DATA FOUND IN THIS SECTOR.