[Noise 入門 #25] Three.js × GLSL の架け橋 — ShaderMaterial と EffectComposer でノイズをブラウザに解き放つ

はじめに

これまで私たちは、数学という名の魔法陣を描き、GPUの力(GLSL)を借りて「炎」「ブラックホール」「空間の歪み」を錬成してきました。しかし、それらはまだShaderという独立した空間(例えばShadertoyのような環境)に閉じ込められています。

今回からは、生のWebGLコードを書く際の膨大なボイラープレート(定型文)を避け、Three.jsの洗練されたフレームワーク上でノイズをシームレスに統治・稼働させる手法を深掘りします。

前回の記事:

1. ShaderMaterial — メッシュにノイズの魂を宿す

これまで学んできたGLSLのコードは、そのままではブラウザ上に現れません。それらを「マテリアル(質感)」という形にパッケージ化し、Three.jsの3D空間に送り出すための特急券、それが THREE.ShaderMaterial です。

構造:3つの柱で成り立つ「カスタムマテリアル」

ShaderMaterial を定義する際、私たちは主に3つの要素を記述します。これらは、GPUという巨大な工場の各セクションへの「指示書」のようなものです。

  1. Vertex Shader(頂点シェーダー) メッシュの「骨組み(頂点)」をどこに配置するかを決めます。後述する行列計算によって、3D空間の座標をブラウザ画面上の2D座標へ変換します。
  2. Fragment Shader(フラグメントシェーダー) 「皮膚(ピクセル)」の色を決めます。私たちのメインステージはここです。 座標(UV)や時間(uTime)を受け取り、FBMやSimplex Noiseの関数を回して、めくるめく色や模様を生成します。
  3. Uniforms(ユニフォーム変数) CPU(JavaScript)からGPU(GLSL)へ送られる「共通のルール」です。時間は止まっているか動いているか、マウスはどこにあるか、といった外部情報をシェーダーに伝える唯一の通信経路です。

Three.js の裏側にある「暗黙の合意」

なぜ、私たちの書くGLSLはこれほど短くて済むのでしょうか?それは、Three.jsが裏側で「当然これ使うでしょ?」という変数を自動的に注入してくれているからです。

1. 魔法の行列計算

Vertex Shader内で必ず目にするこの一行:

$$\text{gl_Position} = \text{projectionMatrix} \times \text{modelViewMatrix} \times \text{vec4}(\text{position}, 1.0)$$

これは、以下の3段階の変換を一度に行っています。

  • position: メッシュ自体のローカル座標。
  • modelViewMatrix: 「物体が世界の中でどこにあり、カメラがどこから見ているか」を反映する行列。
  • projectionMatrix: 「カメラのレンズ(広角か望遠か)」を反映し、最終的に画面の2D座標へ押しつぶす行列。

生のWebGLなら、これらすべての行列をJavaScript側で計算し、毎フレームGPUに送り届けるコード(数十行)を書かなければなりません。ShaderMaterial を使うことで、私たちは「座標変換の苦労」から解放され、「ノイズのデザイン」に全神経を集中できるのです。

2. ShaderMaterial と RawShaderMaterial の違い

ここはエンジニアとして押さえておきたいポイントです。

  • ShaderMaterial: 上記の行列や uvnormal といった変数を自動で定義(Prepend)してくれます。初心者や効率を重視するプロ向けです。
  • RawShaderMaterial: 一切の自動定義を行いません。attributeuniform をすべて自分で宣言する必要があります。シェーダーの極限の最適化や、特殊な構造を持たせたい時に使います。

lain’s Note:
最初は ShaderMaterial で十分です。Three.jsが提供する「親切心」に甘えましょう。ノイズのアルゴリズムを実装するだけでも、脳のメモリは十分に消費されますから。


実装のヒント:GLSLをどう管理するか?

JavaScriptの中にバッククォート( )でGLSLを書き続けると、コードが長くなりすぎて「文字列の海」で溺れてしまいます。

  • 小規模なら: 文字列として定義。
  • 中規模なら: .glsl ファイルとして分離し、Viteなどのビルドツールで import する。
  • 大規模なら: glslify などのツールを使い、数学関数(FBMなど)をモジュール化して使い回す。

第4集の Three.js 編が進むにつれ、これら「コードの統治術」も重要になってきます。

2. JavaScript と GLSL の通信 — Uniforms による時間の支配

ノイズの数式は、それ単体では数学的な「静止したグラフ」に過ぎません。そこに「時間」という次元を流し込み、アニメーションへと変貌させるための通信経路が Uniforms(ユニフォーム変数) です。

ユニフォーム変数の本質:一斉送信の「放送」

GLSLにおける uniform とは、描画されるすべてのピクセルに対して「全く同じ値」を届ける仕組みです。

  • Attribute(属性): 頂点ごとに違う値(座標、UVなど)。
  • Varying(変化量): 頂点シェーダーからフラグメントシェーダーへ「補間」されながら渡される値。
  • Uniform(一様): CPUから「今の時間は $1.25$ 秒だよ!」と叫べば、画面上の全ピクセルが同時にその値を受け取ります。

この「一斉送信」こそが、画面全体のノイズを同期させ、滑らかな「流れ」や「揺らぎ」を作る鍵となります。


深掘り:THREE.Clock が提供する「安定」

サンプルコードにある THREE.Clock() は、単に時間を測るストップウォッチ以上の役割を担っています。

1. 秒単位への正規化

JavaScriptの標準である Date.now()performance.now() はミリ秒単位ですが、clock.getElapsedTime() は秒単位(float)で値を返します。 GLSLの計算(特に三角関数やノイズの周波数計算)は秒単位を前提に設計することが多いため、この「型」と「単位」の変換をThree.js側で吸収してくれるのは大きなメリットです。

2. フレームレートからの解放

requestAnimationFrame はブラウザの負荷やディスプレイのリフレッシュレート(60Hz、144Hzなど)によって実行間隔が変動します。 もし「1フレームごとに $0.01$ 加算する」という組み方をしてしまうと、ハイスペックなモニターでは爆速でノイズが動き、古いスマホではスローモーションになってしまいます。 getElapsedTime() を使うことで、実行環境に依存しない「絶対時間」としての動きを保証できるのです。


エンジニアの落とし穴:浮動小数点の精度問題

ここで一つ、長時間の運用(デジタルサイネージや24時間稼働のWebサイトなど)で発生する「精度の罠」について触れておきます。

GPU(GLSL)の float は通常32bitです。uTime の値が大きくなりすぎると(例えば数日間動かし続けると)、値の「きざみ」が粗くなり、ノイズのアニメーションがガタつき始めることがあります。

lain’s Note: 数日レベルで動かすコンテンツの場合、JS側で material.uniforms.uTime.value = clock.getElapsedTime() % 1000.0; のように、一定周期でループさせる工夫が必要になることもあります。ノイズのループ化(Seamless Loop)という技術と組み合わせれば、永遠に滑らかな揺らぎを維持できます。


応用:時間以外に何を「放送」すべきか?

Uniforms で送れるのは時間だけではありません。

  • マウス座標 (uMouse): カーソル周辺だけノイズを激しくする。
  • オーディオデータ (uAudio): 音楽の低音に合わせてノイズを膨張させる。
  • テクスチャ (uTexture): 写真をノイズでドロドロに溶かす。

JavaScript側で計算したあらゆる「動的な値」を、この uniforms という窓口からGPUへ投げ込む。これこそが、数式を「インタラクティブな体験」へと昇華させるエンジニアリングの醍醐味です。

3. EffectComposer — ポストプロセスで世界全体を歪める

これまで私たちが ShaderMaterial で行ってきたのは、特定のメッシュ(例えば球体や平面)に対する「表面の加工」でした。これに対し EffectComposer を使ったポストプロセスは、レンダリングされた「最終的な絵」全体に対してノイズのフィルターをかける手法です。

概念:FBO(Frame Buffer Object)という「見えない画用紙」

通常、renderer.render(scene, camera) を実行すると、計算結果は直接ブラウザの Canvas に書き込まれます。しかし、ポストプロセスではこの流れを一度せき止めます。

  1. オフスクリーンレンダリング: シーンを画面に出さず、メモリ上のテクスチャ(FBO / Render Target)に描き込みます。
  2. ノイズの適用: そのテクスチャを「一枚の画像」としてシェーダーに渡し、UV座標をノイズで揺らしたり、色を反転させたりします。
  3. 最終出力: 加工が終わった画像を初めて Canvas に表示します。

この「一度描いてから加工する」という多段構えの構造こそが、画面全体を水中に沈めたり、空間をグリッチさせたりすることを可能にしています。


ShaderPass と tDiffuse の秘密

ShaderPass を使う際、自作のシェーダー内で必ず宣言しなければならない特別な変数があります。それが uniform sampler2D tDiffuse; です。

// ShaderPassが自動的に「直前のレンダリング結果」をここに流し込む
uniform sampler2D tDiffuse;
varying vec2 vUv;

void main() {
    // #24で学んだノイズによるUVの歪み
    vec2 distortedUv = vUv + noise2D(vUv * 10.0) * 0.01;

    // 歪ませたUVで「元の絵」をサンプリングする
    vec4 color = texture2D(tDiffuse, distortedUv);

    gl_FragColor = color;
}

Three.js の ShaderPass は、前のパス(RenderPass など)で描き込まれたテクスチャを自動的に tDiffuse という名前のユニフォーム変数としてシェーダーにバインドしてくれます。私たちはこの tDiffuse をどう料理するか(どう歪ませるか)だけを考えれば良いのです。


解像度とピクセル密度の罠(エンジニアの急所)

もっとも多くのエンジニアがハマるのが、「計算上のサイズ(CSSピクセル)」と「実際の描画サイズ(物理ピクセル)」のズレです。

Retinaディスプレイなどの高DPI環境では、window.innerWidth1000px でも、実際に描画されているのは 2000px だったりします(Device Pixel Ratio = 2 の場合)。

何が起きるのか?

シェーダー内で uResolution を使って「1ピクセル単位のグリッチ」などを作ろうとした際、CSSピクセルベースで値を渡してしまうと、高精細なディスプレイではエフェクトが2倍の大きさでボヤけて表示されてしまいます。

正解の作法:

uResolution には必ず renderer.getDrawingBufferSize() で得られる物理ピクセルサイズを渡しましょう。

const size = new THREE.Vector2();
renderer.getDrawingBufferSize(size); // 物理ピクセルサイズを取得

// これをユニフォームに更新し続ける
customNoisePass.uniforms.uResolution.value.set(size.x, size.y);

lain’s Note:
ポストプロセスは「画面全体のピクセル数分だけシェーダーを走らせる」ため、非常に重い処理です。特に EffectComposer でパスを何枚も重ねると、スマホなどのモバイル端末では一気にフレームレートが落ちます。ノイズの計算回数を減らす(Octaveを下げる)など、見た目と負荷のトレードオフを見極めるのが、プロシージャル・エンジニアの腕の見せ所です。


応用:Passのスタッキング(重ねがけ)

EffectComposer の真価は、複数のパスを重ねられる点にあります。

  • Pass 1: RenderPass(通常の描画)
  • Pass 2: UnrealBloomPass(光を溢れさせる)
  • Pass 3: ShaderPass(ノイズで空間を歪ませる)
  • Pass 4: FilmPass(走査線やノイズを乗せてレトロにする)

このように「数学(ノイズ)」と「光学エフェクト」をレイヤー状に重ねていくことで、ただの3Dモデルが「映画のような質感」を帯びた「世界」へと変わっていきます。

まとめ:数式が「Webの体験」へと変わる特異点

純粋な数学的アルゴリズムから始まったノイズの探求は、Three.js の ShaderMaterialEffectComposer を経由することで、ついにWebブラウザ上の「実体あるアプリケーション」へと進化しました。

これまでの24回で積み上げてきた「GPUの中で起きている魔法(GLSL)」を、現実のエンジニアリング・システムとして美しく統治するための両翼が、今回学んだ2つのクラスです。

  • ShaderMaterial: 複雑なWebGLの初期化や行列計算をThree.jsに委ね、オブジェクトの表面(頂点とピクセル)に直接ノイズの魂を焼き付け、鼓動させる。
  • EffectComposer: オフスクリーンレンダリングの仕組みを利用し、カメラのレンズ越しに「世界全体」のピクセルをノイズで歪曲・拡張させる。

次なる次元:インタラクションの海へ

ここから先の第4集「Three.js 編」では、この強固な基盤システムを駆使し、静的なノイズを「ユーザーの操作に呼応する生きたエフェクト」へと昇華させていきます。

  • マウスの軌跡に合わせて水面(ノイズ)に波紋を広げる。
  • スクロール量に応じて、空間のDomain Warping(ねじれ)を加速させる。
  • 音楽のビートをUniformsに流し込み、フラクタルな炎を明滅させる。

ノイズはもはや「眺めるだけの模様」ではなく、ユーザーと対話する「インタラクティブな世界」のコアエンジンとなります。

次回、#26「Vertex Shaderによる頂点変形(Displacement) — ノイズで立体を鼓動させる」では、ピクセルの色を塗るフラグメントシェーダーの段階を終え、いよいよノイズの力で3Dモデルの「形そのもの」をリアルタイムに変形させる領域へと踏み込みます。

ブラウザという広大なキャンバスで、錬成したノイズがいよいよ暴れ回る準備が整いました。次回の展開も、ぜひお楽しみに!