[Noise 入門 #59] 次元崩壊グリッチ(Post-Processing Audio Glitch) — 音の衝撃で世界がバグるサイバーパンク・エフェクト

[Noise 入門 #59] 次元崩壊グリッチ(Post-Processing Audio Glitch) — 音の衝撃で世界がバグるサイバーパンク・エフェクト

はじめに

ポストプロセス(EffectComposer)による画面全体へのエフェクトは、これまでの「オブジェクトに対するノイズ」とは異なり、レンダリングされた「1枚の画像(テクスチャ)」の UVUV 座標をノイズで直接歪ませる手法です。

今回は、オーディオのボリューム(強度)をトリガーにして、以下の2つの現象を同時に引き起こします。

  1. Block Glitch(ブロックノイズ): 画面を粗いグリッドに分割し、ランダムな座標へピクセルを吹き飛ばす。
  2. Chromatic Aberration(色収差): RGBの各チャンネルの読み込み座標(UV)を少しずつズラし、デジタルな色ズレを発生させる。

前回の記事:

1. 空間をブロック状に切り刻む数式 — UV座標の「ステップ化(離散化)」

サイバーパンク的な「グリッチ(バグ)」表現の要となるのは、デジタルエラー特有のカクカクとしたブロック感です。

これまで扱ってきたFBMやDomain Warpingは、空間を「滑らかなゴムシート」のように歪ませるものでした。しかし、今回は空間を「硬いガラス」のように叩き割り、ブロック単位でズラす必要があります。

滑らかな世界を破壊し、ピクセル単位のモザイク状の座標系を作り出すための魔法が、「UV座標のステップ化(離散化)」という数学的アプローチです。

空間を切り刻む3つのステップ

GLSLにおける画面のUV座標(UVUV)は、左下を (0.0,0.0)(0.0, 0.0)、右上を (1.0,1.0)(1.0, 1.0) とする「滑らかに連続したグラデーション」です。これをカクカクのブロック状に変換する数式が以下になります。

UVblock=UV×gridgridUV_{block} = \frac{\lfloor UV \times \text{grid} \rfloor}{\text{grid}}

この数式は、たった3つのシンプルな手順で空間を切り刻んでいます。例えば、画面を10分割(grid=10.0\text{grid} = 10.0)する場合で考えてみましょう。

  1. 拡大する(×grid\times \text{grid}0.01.00.0 \sim 1.0 だった滑らかな数値を、一時的に 0.010.00.0 \sim 10.0 のスケールに引き延ばします。

  2. 切り捨てる(\lfloor \dots \rfloor つまり GLSLの floor()) ここが最大のポイントです。小数点を容赦無く切り捨てます。 これにより、0.00.990.0 \sim 0.99 の範囲にあった座標はすべて強制的に 0.0 になります。1.01.991.0 \sim 1.99 はすべて 1.0 になります。滑らかな坂道が、「10段の階段」に変換される瞬間です。

  3. 元に戻す(÷grid\div \text{grid}) 最後に 10.0 で割ってスケールを戻します。 結果として出力されるのは、0.0,0.1,0.2,0.31.00.0, 0.1, 0.2, 0.3 \dots 1.0 という「飛び飛びの(離散化された)座標」になります。

なぜ「カクカクの座標」が必要なのか?

「別に座標をモザイク状にしなくても、直接ランダム関数を使えばいいのでは?」と思うかもしれません。

しかし、もし滑らかな UVUV 座標をそのままランダム関数 random(uv) に渡してしまうと、1ピクセルごとに全く違うランダム値が返ってきてしまいます。 その結果は、ただの「テレビの砂嵐(ホワイトノイズ)」です。

私たちが欲しいのは砂嵐ではなく、「面(ブロック)としての崩壊」です。

先ほどの数式で作った UVblockUV_{block} をシード値としてランダム関数に渡してみましょう。 例えば、画面の左下付近のピクセル群は、すべて強制的に (0.0, 0.0) という同じ座標に変換されています。つまり、そのブロック内にいるすべてのピクセルが「完全に同じランダムな値」を受け取ることになります。

// 画面を縦横30マスのグリッドに分割する
float grid = 30.0;

// 滑らかなUVを、30段の階段状のUVに変換(モザイク化)
vec2 blockUv = floor(uv * grid) / grid;

// ブロック化されたUVをシード値としてランダムな値を生成
// → ブロック内の全ピクセルが同じ値(同じ色・同じズレ幅)になる!
float blockNoise = random(blockUv);

💡 ディレクターズ・メモ

この blockUv の概念はグリッチ表現の基礎中の基礎です。「空間を floor で切り捨てて同盟(ブロック)を組ませる」という感覚を掴むと、モザイク処理やピクセルアート風のシェーダーなど、あらゆるレトロ表現・デジタル表現に応用できるようになります。

この「ブロックごとに統一されたランダム値」を、描画位置をズラすためのオフセット(Offset)として使うことで、初めて画面が「ガシャン!」とブロック状にスライドするサイバーパンクなエラー表現が成立するのです。

2. GLSL実装:次元崩壊シェーダー(CustomGlitchPass)

前段の「ステップ化」の理論を組み込み、Three.jsの ShaderPass(ポストプロセス)として機能させるためのカスタム・フラグメントシェーダーを構築します。

このシェーダーの主役は、外部(Web Audio API)から送られてくる uIntensity(音の強度) です。この変数が、ブロックのズレ、色収差、そして走査線のすべてを支配します。

// --- Vertex Shader (標準的なPost-Processing用) ---
varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

// --- Fragment Shader (Glitch Effect) ---
uniform sampler2D tDiffuse; // レンダリングされた元の画面
uniform float uTime;        // 経過時間
uniform float uIntensity;   // 音の強度(Audio Analyzerから取得: 0.0 ~ 1.0+)

varying vec2 vUv;

// 2Dランダム関数
float random(vec2 st) {
    return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}

void main() {
    vec2 uv = vUv;

    // 1. ブロック状のグリッドを生成 (画面を縦横30分割)
    float grid = 30.0;
    vec2 blockUv = floor(uv * grid) / grid;

    // 2. 時間を加味してブロックごとのランダム値を生成(ノイズの代わり)
    // ※高速でチカチカさせるために、uTimeの掛ける値を大きくする
    float blockNoise = random(blockUv + floor(uTime * 15.0));

    // 3. グリッチの発生確率と強度を計算
    // step(0.8, blockNoise) : ノイズ値が0.8以上のブロックだけ1.0になる
    // そこに音の強度(uIntensity)を掛けて、音が強い時だけ大きくズラす
    float glitchTrigger = step(0.8, blockNoise) * uIntensity;

    // 4. UV座標の水平方向のズレ(オフセット)
    float offset = (random(blockUv) - 0.5) * 0.2 * glitchTrigger;

    // 5. Chromatic Aberration (色収差: RGBのズレ)
    // 音の強度に比例して、赤と青の読み込み位置を左右に引き裂く
    float rgbShift = 0.03 * uIntensity;

    vec2 uvR = uv + vec2(offset + rgbShift, 0.0);
    vec2 uvG = uv + vec2(offset, 0.0);
    vec2 uvB = uv + vec2(offset - rgbShift, 0.0);

    // テクスチャ(画面)から色をサンプリング
    float r = texture2D(tDiffuse, uvR).r;
    float g = texture2D(tDiffuse, uvG).g;
    float b = texture2D(tDiffuse, uvB).b;

    // 6. デジタル感を強調するスキャンライン(走査線)
    float scanline = sin(uv.y * 800.0) * 0.04 * uIntensity;

    // 最終出力
    gl_FragColor = vec4(r - scanline, g - scanline, b - scanline, 1.0);
}

コードの解剖学:オーディオといかに連動させるか

このコードがどのようにして「音でバグる世界」を作り出しているのか、重要なギミックを解説します。

時間のコマ落ち(ストロボ効果)

2つ目のステップに注目してください。uTime(経過時間)をそのまま足すのではなく、floor(uTime * 15.0) としています。 時間を floor で切り捨てることで、アニメーションが滑らかに変化するのを防ぎ、「1秒間に15回だけカクッと値が変わる」 というストロボのようなチカチカした動きを作り出しています。デジタルなエラー表現において「滑らかさ」は敵です。

発生確率を絞る(閾値の操作)

3つ目のステップにある step(0.8, blockNoise) は、「画面全体を一斉にバグらせるのではなく、一部のブロックだけをランダムにバグらせる」ためのフィルターです。 random 関数は 0.0 〜 1.0 の値を返しますが、step(0.8, ...) を通すことで、「0.8以上の値が出たブロック(全体の約20%)だけが 1.0 になり、残りは 0.0 になる」という状態を作ります。 このフィルターと uIntensity を掛け合わせることで、「音が鳴った瞬間、画面の20%のブロックが弾け飛ぶ」というメリハリのあるグリッチが生まれます。

次元を引き裂く色収差(Chromatic Aberration)

5つ目のステップこそが、サイバーパンク表現の真骨頂です。 本来、画面の色はRGB(赤・緑・青)が同じ座標で重なり合って自然な色を作っています。しかし、ここでは読み込むUV座標を uvRuvGuvB に分裂させ、赤いチャンネルは右へ、青いチャンネルは左へ、uIntensity の強さに応じて強制的に引き裂いています。 大きな音が鳴るほど、このズレ(rgbShift)が広がり、3Dメガネを外して画面を見たような強烈な色ズレが発生します。

ブラウン管の記憶(CRTスキャンライン)

最後の仕上げが6つ目のステップです。 sin(uv.y * 800.0) によって、画面全体に極めて細かな横縞(走査線)を生成しています。これも uIntensity に掛け合わせているため、無音の時はクリアな画面ですが、音が鳴った瞬間に「古いブラウン管テレビがノイズを受信した」ような暗い横線が走ります。

3. Three.jsとの連携(ディレクターへの指示書)

Three.js側では、前回の #56 で構築したオーディオ・アナライザーのデータを、毎フレームこのシェーダーの uIntensity に流し込みます。

純粋なレンダリング結果(renderer.render)を横取りし、エフェクトのフィルターを通すための「ポストプロセス」を構築します。

// カスタムシェーダーの定義
const GlitchShader = {
    uniforms: {
        "tDiffuse": { value: null },
        "uTime": { value: 0.0 },
        "uIntensity": { value: 0.0 }
    },
    vertexShader: `...`, // 上記のVertex Shader
    fragmentShader: `...` // 上記のFragment Shader
};

// EffectComposerのセットアップ
const composer = new THREE.EffectComposer(renderer);
const renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);

const glitchPass = new THREE.ShaderPass(GlitchShader);
composer.addPass(glitchPass);

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

    // 時間の更新
    const time = clock.getElapsedTime();
    glitchPass.uniforms["uTime"].value = time;

    // 音声データの取得(低音域の平均ボリュームなどを算出)
    const audioData = getAudioFrequencyData(); // #56で実装した関数と仮定
    let kickVolume = audioData[10] / 255.0; // 例: 10番目の周波数帯域(キック)

    // 音が一定の閾値を超えたら強くグリッチをかける(スレッショルド処理)
    if (kickVolume < 0.5) kickVolume = 0.0;

    // シェーダーへ強度の伝達
    // ※急激に値が下がるのを防ぐためのイージング(Lerp)を入れるとより自然になります
    glitchPass.uniforms["uIntensity"].value += (kickVolume - glitchPass.uniforms["uIntensity"].value) * 0.2;

    composer.render();
}

JavaScript側の実装のコア・テクニック

このコードが単に「音量と変数を繋げただけ」ではない理由、つまり映像のクオリティを決定づける3つの重要な仕掛けについて解説します。

1. 帯域のピンポイント抽出(Kickの検知)

audioData[10] のように、取得した周波数配列(通常は128〜1024個のデータ)の中から、特定のインデックスを狙い撃ちしています。 配列の先頭付近(インデックスが若い数値)はバスドラムやベースなどの「低音(キック)」に該当します。曲全体のボリュームの平均値を取ってしまうと、メロディやボーカルにも反応してしまい、映像が終始ダラダラと揺れてしまいます。「ドンッ!」という強いビートの瞬間だけをトリガーにするのが、エフェクトをスタイリッシュに見せるコツです。

2. ノイズカットの閾値(スレッショルド処理)

if (kickVolume < 0.5) kickVolume = 0.0; この1行は極めて重要です。キック以外の微小な音(環境音や残響)による中途半端な数値を完全に「ゼロ」に切り捨てています。 グリッチエフェクトは「やるか、やらないか」のメリハリが命です。閾値を設けることで、静かなパートでは完全にクリアな画面を保ち、サビやドロップの瞬間にだけ爆発的な崩壊を引き起こすことができます。

3. 余韻のイージング(Lerpによる減衰)

glitchPass.uniforms["uIntensity"].value += (kickVolume - current) * 0.2; 生オーディオデータをそのままシェーダーに渡すと、音が鳴った「1フレーム」だけ画面がバグり、次のフレームで一瞬にして元に戻るため、非常に安っぽく、目に痛いチラつきになってしまいます。 この数式(線形補間:Lerp)を挟むことで、音が鳴った瞬間に値が跳ね上がり、その後 0.2 ずつゆっくりと滑らかにゼロへ向かって減衰(フェードアウト)していきます。物理的なバネのように「ガツンと揺れて、スッと収まる」心地よい余韻を作り出す、プロシージャル・アニメーションの必須テクニックです。

ディレクターズ・チューニング(パラメータの遊び方)

コードを走らせたら、以下の数値を書き換えてみてください。ディレクターの意図ひとつで、エフェクトの「性格」が劇的に変化します。

  • float grid = 30.0; (崩壊のスケール) ここを 10.0 にすると巨大なブロックノイズになり、ファミコンなどのレトロゲームがフリーズした時のような暴力的な崩壊表現になります。逆に 100.0 にすると、アナログテレビや古いVHSテープが擦り切れたような、細かい砂嵐(ザラザラとしたノイズ)へと変貌します。

  • float rgbShift = 0.03 * uIntensity; (色収差の引き裂き幅) RGBのズレ幅です。ここを 0.1 などの極端な数値に設定すると、元のオブジェクトの形が判別できないほど色が左右に(赤と青の3Dメガネのように)激しく分離し、強烈でサイケデリックなトリップ感を生み出します。

  • オーディオの取得帯域(キックからハイハットへ) JavaScript側で拾う音のインデックスを「キック(低音)」から「ハイハット(高音)」に変更してみてください。「ズンッ!」という重いビートに合わせた重厚な崩壊から、「チキチキ」という高音に合わせた神経質で痙攣するような細かいグリッチへと、画面のノリが完全に曲調と同期します。


すべてが音楽に支配され、空間の座標系がオーディオ信号によって無理やり引き裂かれる快感。これが「Post-Processing Audio Glitch」の魔力です。

次はいよいよ第6集完結、[Noise 入門 #60] です。 これまで錬成してきた「プラズマ」「パーティクル」「空間歪曲」の全VFXを一つの空間にブチ込み、究極の「神領域のオーディオ・ビジュアル・アート」を完成させましょう。

実装まとめ:音とノイズが交差する「次元崩壊グリッチ」

今回の実装では、オブジェクト単体の変形を超え、レンダリングされた画面全体(ポストプロセス)に対してノイズを適用する「次元崩壊」をテーマにしました。音響解析データと数式を融合させ、サイバーパンクな世界観を構築しています。

1. 空間を切り刻む「UV座標の離散化」

デジタルエラー特有のブロック状のズレを生むため、滑らかな UVUV 座標を意図的に「階段状」の座標系へと変換する手法を採用しました。 UVblock=UV×gridgridUV_{block} = \frac{\lfloor UV \times \text{grid} \rfloor}{\text{grid}} この UVblockUV_{block} をランダム関数のシード値に用いることで、ピクセルが1点ずつバラバラに動くのではなく、ブロック単位でまとまってスライドする「バグ感」を実現しています。

2. Audio-Reactive な動的制御

Web Audio API から取得した周波数データを Uniform 変数 uIntensity としてシェーダーに直接流し込んでいます。

  • 低音域(Kick)の抽出: 周波数配列の特定のインデックスをトリガーにし、ビートに同期した衝撃波を生成。
  • スレッショルド(閾値)処理: 微細なノイズ音をカットし、大きな音が鳴った瞬間にだけ崩壊が起きるメリハリを付与。
  • Lerp による減衰: 急激な値の変化にイージング(線形補間)を加え、衝撃の後にスッと収まる心地よい余韻を演出。

3. 多層的な VFX 統合(第6集の集大成へ)

単なるグリッチに留まらず、これまでの「Noise 入門」シリーズで培った技術を以下の形で統合し、表現の解像度を引き上げました。

  • UnrealBloomPass: ネオンのような光の溢れ出し(ブルーム)を強調し、サイバーな質感を極大化。
  • Audio-Reactive Floor: 平面ジオメトリの頂点を操作し、音圧で同心円状に波打つ「揺れる大地」を実装。
  • Camera Shake: 衝撃の瞬間にカメラ座標をランダムに振動させる「物理的な衝撃」の演出。
  • Particle Aura: オブジェクトの周囲に粒子を配置し、音に乗って放射状に飛散させるエネルギーの表現。

4. 視覚的解像度の洗練

実装の最終段階において、あえて「背景の星の点」をカットするなどの引き算を行いました。これにより、メインの造形(TorusKnot)の脈動やグリッチによる歪みがより鮮明に浮き上がり、暗闇の深淵にネオンが躍動する「神領域」のアートワークを完成させています。


COMM_LOG: noise-intro-59-post-processing-audio-glitch

NO DATA FOUND IN THIS SECTOR.