threejs-perf について
threejs-perfは、Three.jsゲーム向けのパフォーマンス最適化スキルです。静的オブジェクトと移動エンティティのインスタンシング手法や測定データを示し、描画負荷の高いプロジェクトに役立ちます。
主な機能
- 01静的な反復オブジェクトにInstancedMeshを使用
- 02移動エンティティの変換を一括更新
- 03ベンチマークと最適化例を提供
- 04インスタンシングの適用条件を説明
活用シーン
- 01反復する小道具が多いシーンを最適化
- 02多数の移動エンティティの描画呼び出しを削減
- 03シーングラフ走査のボトルネックを調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ playableintelligence/ game-creator --skill threejs-perfインストール後、Claude Code で/threejs-perfを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
instancing-moving.mdinstancing-static.mdtemplates/moving-entities-baseline.tsmoving-entities-instanced.tsstatic-world-baseline.tsstatic-world-instanced.ts詳細
Three.js のパフォーマンス最適化
Three.js r183 で測定した最適化前後の数値に基づく、Three.js ゲームのパフォーマンスパターンです(Playwright 経由のヘッドレス Chromium、Apple M1 Pro、ソフトウェア WebGL)。
参照ファイル
instancing-static.md— 大量の静的な反復オブジェクトに対する InstancedMesh(19,600 → 1 ドローコール)instancing-moving.md— 移動エンティティ向けのフラットな状態バッファと InstancedMesh のバッチ書き込み(8,000 エンティティ)templates/— 各パターンの最適化前と最適化後の実装例
このスキルを使う場面
- シーンに、同じジオメトリ/マテリアルを共有する反復オブジェクトが 100 個以上ある
- ドローコールが 500 を超え、フレーム時間が不安定
- 数千の移動エンティティについて、フレームごとにトランスフォームを更新する必要がある
- プロファイルでシーングラフの走査がボトルネックになっている
使わない場面
- オブジェクト数が少ない(ユニークなメッシュが 50 個未満) — シンプルなコードのほうが適しています
- すべてのオブジェクトに、バッチ処理を妨げる個別のマテリアル/シェーダーが必要
- ジオメトリの違いが大きく、インスタンシングによるバッチ処理のメリットがない
パターン 1:大量の静的オブジェクトをインスタンシングする
問題:森、がれき、装飾などを個別の Mesh として配置すると、不要なドローコールが発生します。
解決策:共有するジオメトリとマテリアルの組み合わせごとに、InstancedMesh を 1 つ使います。
根拠:ドローコールは約 19,365 → 2 に減少しました。レンダリング CPU p95 は 28.5ms → 0.5ms(約 57 倍高速化)。構築時間は 39.4ms → 3.9ms。詳細は instancing-static.md を参照してください。
// Anti-pattern: one Mesh per prop
for (let i = 0; i < 19600; i++) {
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, 0, z);
scene.add(mesh); // 19,600 draw calls
}
// Correct: one InstancedMesh
const im = new THREE.InstancedMesh(geometry, material, 19600);
const mat = new THREE.Matrix4();
for (let i = 0; i < 19600; i++) {
mat.makeTranslation(x, 0, z);
im.setMatrixAt(i, mat);
}
im.instanceMatrix.needsUpdate = true;
scene.add(im); // 1 draw call
パターン 2:移動エンティティの更新ループ
問題:数千の移動アクターを個別の Mesh として扱うと、シーングラフの更新負荷とトランスフォームの伝播が発生します。
解決策:フラットなエンティティ状態バッファと、InstancedMesh.setMatrixAt() によるバッチ書き込みを使います。
根拠:ドローコールは 8,000 → 1 に減少しました。レンダリング CPU p95 は 9.9ms → 0.5ms(約 20 倍高速化)。更新ループ p95 は 1.4ms → 0.3ms。詳細は instancing-moving.md を参照してください。
// Anti-pattern: per-entity Mesh position writes
meshes.forEach((mesh, i) => {
mesh.position.x = computeX(i, tick);
mesh.position.y = computeY(i, tick);
});
// Correct: batched instance matrix writes
const mat = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
mat.makeTranslation(computeX(i, tick), computeY(i, tick), computeZ(i, tick));
instancedMesh.setMatrixAt(i, mat);
}
instancedMesh.instanceMatrix.needsUpdate = true;
判断フローチャート
Is the object repeated 50+ times with same geometry+material?
├── YES → Is it static (no per-frame movement)?
│ ├── YES → Pattern 1: Static InstancedMesh (instancing-static.md)
│ └── NO → Pattern 2: Moving InstancedMesh with batched writes (instancing-moving.md)
└── NO → Standard Mesh is fine. Focus on material/geometry reuse.
測定結果
Playwright 経由のヘッドレス Chromium 147、Three.js r183、Apple M1 Pro で、ウォームアップ 30 フレームとサンプル 180 フレームを実行し、3 回の実行結果の中央値を示しています。
| シナリオ | 指標 | 最適化前 | 最適化後 | 改善 |
|---|---|---|---|---|
| 静的ワールド(19.6k キューブ) | ドローコール | ~19,365 | 2 | ~9,682× |
| 静的ワールド(19.6k キューブ) | レンダリング CPU p95 | 28.5ms | 0.5ms | ~57× |
| 静的ワールド(19.6k キューブ) | 構築時間 | 39.4ms | 3.9ms | ~10× |
| 移動エンティティ(8k wave-field) | ドローコール | 8,000 | 1 | 8,000× |
| 移動エンティティ(8k wave-field) | レンダリング CPU p95 | 9.9ms | 0.5ms | ~20× |
| 移動エンティティ(8k wave-field) | 更新ループ p95 | 1.4ms | 0.3ms | ~4.7× |
方法に関する補足
- CPU 側の指標が信頼できる判断材料です。 ドローコール、レンダリング CPU p95、更新ループ、構築時間は、1~2 桁の改善を確実に示します。
- ヘッドレス Chromium では FPS とフレーム時間 p95 の信頼性が低くなります。 Playwright に同梱された Chromium は SwiftShader(ソフトウェア WebGL)を使用しており、ドローコール数に関係なく、約 90 MB の可視ジオメトリに対するフラグメントシェーディングがボトルネックになります。実機の WebGL では FPS の差は大幅に大きくなり、実際のフィルレート負荷では最適化前の FPS は 1 桁まで低下する一方、最適化後は安定して垂直同期に達するでしょう。
- ドローコールが減少し、レンダリング CPU p95 が悪化していなければ、ベンチマークは合格です。