最適化と注意点

GLSL は並列実行・浮動小数・分岐コストに独特のクセがある。 本番投入前に通読しておく落とし穴と最適化の基本まとめ。

パフォーマンスの基本方針

  1. 解像度を下げる — 一番効く。fragment 数 = 全コスト
  2. 分岐を減らすif より mix/step
  3. texture サンプリングを減らす — 1 fragment で 5 回より 2 回
  4. 重い関数を避けるpow/exp/log/atan/sqrt は重い
  5. uniform が同じ値なら頂点シェーダで計算 — fragment は呼ばれる回数が多い
  6. varying をシンプルに — 多すぎると補間コストが増える

分岐は不利

GPU は同じ命令を多数並列に実行する。同じ workgroup(warp / wavefront)内の fragment が違う分岐を取ると、両方を実行して片方を捨てる動きになる。 これを thread divergence と呼ぶ。

条件分岐をブランチレスに

BAD: 分岐
if (x > 0.5) {
  col = a;
} else {
  col = b;
}
GOOD: mix + step
col = mix(b, a, step(0.5, x));

実用的にはuniform で一定なら if は OK(同じ workgroup 内で同じ分岐に行くため)。 fragment ごとに値が違う vUv/vNormal を使った if は要注意。

ループのコスト

// OK: コンパイル時定数
for (int i = 0; i < 8; i++) { ... }

// 遅い可能性: ループ内に重い処理
for (int i = 0; i < 64; i++) {
  texture(t, uv);   // 64 回サンプリング → 重い
}

重い関数 / 軽い関数

軽い重い
+ - * /pow
min / max / absexp / log
stepatan / asin / acos
mixsqrt
smoothstepinverse / determinant
dottexture / textureLod(毎回メモリ取得)
normalize動的ループ

軽い書き換えの例

// pow(x, 2.0) → x * x
float a = x * x;

// pow(x, 4.0) → x = x*x; x*x
float b = x * x; b = b * b;

// 1.0 / sqrt(x) → inversesqrt(x)
float c = inversesqrt(x);

// length(d) < r で比較するなら sqrt 回避
// length(d) < r ⇔ dot(d, d) < r * r
if (dot(diff, diff) < radius * radius) { ... }

precision(精度)

モバイル GPU ではmediump(16bit)の方が highp より速いことが多い。 色や軽い計算は mediump で十分。

precision highp float;        // 全 float のデフォルト

mediump vec3 color = vec3(1.0, 0.5, 0.0);   // 個別に精度を指定

注意: 座標・行列は精度が落ちると壊れる。highp 必須。色・α は mediump で OK。

varying の数を減らす

頂点 → fragment への varying は補間コストがかかる。 必要最小限にし、関連する値はパックする。

// BAD: 別々
out vec3 vWorldPos;
out vec3 vWorldNormal;
out vec2 vUv;

// GOOD: パック(vec2 × 4 = vec4 × 2)
out vec4 vPosUv;        // xyz = pos, w = u
out vec4 vNormalUv;     // xyz = normal, w = v

実用ではここまでやることは少ないが、varying が 8 個を超えるなら検討。

uniform は変えるたびに切替コスト

Three.js では material.uniforms.x.value = ... はほぼ無料だが、 頻繁に値を変える場合はInstancedMesh / instancedAttributeで持たせる方が速いケースもある。

シェーダ間の値の受け渡し

共通の落とし穴

1. 1.01 の混在

浮動小数点には必ず小数点: 1.00.52. はOK。 1 は int として解釈される場合がある。

// BAD(環境による)
float a = 1;

// GOOD
float a = 1.0;

2. 暗黙の型変換が無い

// BAD
vec3 v = vec2(1.0);   // エラー

// GOOD
vec3 v = vec3(vec2(1.0), 0.0);

3. ベクトル× はcomponent-wise

// 内積ではなく要素ごとの積
vec3 c = a * b;     // (a.x*b.x, a.y*b.y, a.z*b.z)

// 内積が欲しいなら
float d = dot(a, b);

4. normalize(0) は NaN

ゼロベクトルを normalize すると NaN になり、画面が黒や白で固まる。

// BAD
vec3 dir = normalize(toLight);   // toLight が 0 だと NaN

// GOOD
float len = length(toLight);
vec3 dir = (len > 0.0) ? toLight / len : vec3(0.0, 1.0, 0.0);

5. pow(0.0, 0.0) も実装依存

WebGL の pow負の底や 0 の負乗で結果が undefined。max(x, 0.0) 等で守る。

6. discard 多用は遅い

discard は早期 z 書き込み等の最適化を阻害する。 大量の discard(パーティクル・葉っぱ等)は普通に α 合成(transparent)に置き換える方が速いことも。

7. varying 名が vertex/fragment で違う

真っ黒になる典型バグ。リンクエラーが console に出るので確認。

8. texture ループ展開で重くなる

固定回数ループでも、中で texture サンプリングするとその回数だけ全部実行される。 ぼかしの半径を可変にする時は注意。

9. WebGL1 / WebGL2 の差

ES 1.00(WebGL1)ES 3.00(WebGL2)
texturetexture2Dtexture
NPOT で wrap=Repeat不可
動的配列インデックス不可
動的ループ回数不可
整数ビット演算不可
3D テクスチャ不可

10. Three.js のシェーダ chunk と onBeforeCompile

既存マテリアルを書き換える onBeforeCompile は強力だが、Three.js 内部のシェーダ chunk 名を知らないと書けない。 chunk のリストは Three.js のソースコード(src/renderers/shaders/ShaderChunk/)参照。

デバッグ

1. 値を色として出す

GLSL には console.log が無い。色として可視化するのがデバッグの基本。

// 値を可視化
outColor = vec4(vec3(myValue), 1.0);

// 法線を可視化
outColor = vec4(N * 0.5 + 0.5, 1.0);

// UV を可視化
outColor = vec4(vUv, 0.0, 1.0);

// 距離を可視化
outColor = vec4(vec3(d * 2.0 + 0.5), 1.0);

// マイナスを可視化(負を赤、正を青)
outColor = vec4(max(-d, 0.0), 0.0, max(d, 0.0), 1.0);

2. シェーダエラーをコンソールで確認

Three.js はシェーダコンパイルエラーを console に出してくれる。 エラー行と内容が出るので、まず console を見る。

3. ホットリロード

Vite なら GLSL の変更は HMR で反映される。vite-plugin-glsl を使うと .glsl / .vert / .frag ファイルを直接 import できて、HMR と#include も効く。

4. シェーダの中身を確認

Three.js の onBeforeCompile 後の最終シェーダコードは:

material.onBeforeCompile = (shader) => {
  // ...
  console.log("vertex:", shader.vertexShader)
  console.log("fragment:", shader.fragmentShader)
}

5. Spector.js

Chrome/Firefox 拡張。1 フレーム分の WebGL コールを全部キャプチャして、各 draw call の入力・シェーダ・テクスチャを確認できる。 重いシェーダや謎の挙動を追う時に強力。

6. WebGL Inspector / RenderDoc

さらに低レベルなデバッグツール。GPU のステート・バッファ内容まで見られる。

シェーダのプロファイル

本番で気にすべきブラウザ差

セキュリティ・配布

判断ガイド

症状疑い
真っ黒outColor 書き忘れ、varying 名ミスマッチ、コンパイルエラー
真っ白値がオーバー(例: color = vec3(100))、tone mapping
NaN(チラつき・固まる)normalize(0)0/0pow の負底
遅い解像度・分岐・texture サンプリング数・discard 多用
モバイルだけ崩れるprecision 不足、float16 の精度切れ
iOS だけ遅いhighp の遅さ、独特の最適化挙動
色が薄い・濃いsRGB / リニアの取り違え、texture.colorSpace
UV ずれgeometry の UV、flipY、vUv vs gl_FragCoord
学び方の道筋

1. 模写する: Shadertoy で気に入った作品をコピーして手で動かす
2. パラメータをいじる: 数値を変えて「何が変わるか」を見る
3. 自分の表現を作る: パターン → グラデ → 動き → 3D 拡張 と段階的に
4. 既存の math を覚える: SDF / noise / FBM / palette は毎回コピペでよい。仕組みは後から