はじめに
これまで私たちは、数学という名の魔法陣を描き、GPUの力(GLSL)を借りて「炎」「ブラックホール」「空間の歪み」を錬成してきました。しかし、それらはまだShaderという独立した空間(例えばShadertoyのような環境)に閉じ込められています。
今回からは、生のWebGLコードを書く際の膨大なボイラープレート(定型文)を避け、Three.jsの洗練されたフレームワーク上でノイズをシームレスに統治・稼働させる手法を深掘りします。
前回の記事:
[Noise 入門 #24] Post-Processing × Noise — 世界を歪める「空間ハック」の魔法(水中の揺らぎ・陽炎・グリッチ)
Noise 入門シリーズ第24回。空間内のオブジェクトではなく、レンダリングされた画面全体(カメラ)に対してノイズを適用する「ポストプロセス」の世界へ。UV座標のサンプリング偽装を用いて、水中、陽炎、次元が崩壊するグリッチエフェクトをGLSLで実装する手法を直 …
https://humanxai.info/posts/noise-intro-24-post-processing/1. ShaderMaterial — メッシュにノイズの魂を宿す
これまで学んできたGLSLのコードは、そのままではブラウザ上に現れません。それらを「マテリアル(質感)」という形にパッケージ化し、Three.jsの3D空間に送り出すための特急券、それが THREE.ShaderMaterial です。
構造:3つの柱で成り立つ「カスタムマテリアル」
ShaderMaterial を定義する際、私たちは主に3つの要素を記述します。これらは、GPUという巨大な工場の各セクションへの「指示書」のようなものです。
- Vertex Shader(頂点シェーダー) メッシュの「骨組み(頂点)」をどこに配置するかを決めます。後述する行列計算によって、3D空間の座標をブラウザ画面上の2D座標へ変換します。
- Fragment Shader(フラグメントシェーダー) 「皮膚(ピクセル)」の色を決めます。私たちのメインステージはここです。 座標(UV)や時間(uTime)を受け取り、FBMやSimplex Noiseの関数を回して、めくるめく色や模様を生成します。
- 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: 上記の行列やuv、normalといった変数を自動で定義(Prepend)してくれます。初心者や効率を重視するプロ向けです。RawShaderMaterial: 一切の自動定義を行いません。attributeやuniformをすべて自分で宣言する必要があります。シェーダーの極限の最適化や、特殊な構造を持たせたい時に使います。
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 に書き込まれます。しかし、ポストプロセスではこの流れを一度せき止めます。
- オフスクリーンレンダリング: シーンを画面に出さず、メモリ上のテクスチャ(FBO / Render Target)に描き込みます。
- ノイズの適用: そのテクスチャを「一枚の画像」としてシェーダーに渡し、UV座標をノイズで揺らしたり、色を反転させたりします。
- 最終出力: 加工が終わった画像を初めて 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.innerWidth が 1000px でも、実際に描画されているのは 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 の ShaderMaterial と EffectComposer を経由することで、ついにWebブラウザ上の「実体あるアプリケーション」へと進化しました。
これまでの24回で積み上げてきた「GPUの中で起きている魔法(GLSL)」を、現実のエンジニアリング・システムとして美しく統治するための両翼が、今回学んだ2つのクラスです。
- ShaderMaterial: 複雑なWebGLの初期化や行列計算をThree.jsに委ね、オブジェクトの表面(頂点とピクセル)に直接ノイズの魂を焼き付け、鼓動させる。
- EffectComposer: オフスクリーンレンダリングの仕組みを利用し、カメラのレンズ越しに「世界全体」のピクセルをノイズで歪曲・拡張させる。
次なる次元:インタラクションの海へ
ここから先の第4集「Three.js 編」では、この強固な基盤システムを駆使し、静的なノイズを「ユーザーの操作に呼応する生きたエフェクト」へと昇華させていきます。
- マウスの軌跡に合わせて水面(ノイズ)に波紋を広げる。
- スクロール量に応じて、空間のDomain Warping(ねじれ)を加速させる。
- 音楽のビートをUniformsに流し込み、フラクタルな炎を明滅させる。
ノイズはもはや「眺めるだけの模様」ではなく、ユーザーと対話する「インタラクティブな世界」のコアエンジンとなります。
次回、#26「Vertex Shaderによる頂点変形(Displacement) — ノイズで立体を鼓動させる」では、ピクセルの色を塗るフラグメントシェーダーの段階を終え、いよいよノイズの力で3Dモデルの「形そのもの」をリアルタイムに変形させる領域へと踏み込みます。
ブラウザという広大なキャンバスで、錬成したノイズがいよいよ暴れ回る準備が整いました。次回の展開も、ぜひお楽しみに!
💬 コメント