mcpField
T
threejs-performance

threejs-performance

github.com/cloudai-x開発3.8k2.0k 回インストール1 週間前に更新
リポジトリ

threejs-performance について

Three.js 開発者向けに、WebGL と WebGPU シーンの測定・最適化を扱うスキルです。描画、GPU メモリ、CPU の負荷を抑える具体策を測定方法とともに解説します。

主な機能

  1. 01レンダラー統計とフレーム時間の測定
  2. 02結合とインスタンス化による描画回数削減
  3. 03GPU テクスチャとジオメトリのメモリ管理
  4. 04ピクセル比、透明度、シャドウの調整
  5. 05フレームごとの CPU 変換処理の削減

活用シーン

  1. 01描画回数や三角形数のボトルネック調査
  2. 02シーン内の反復オブジェクトの最適化
  3. 03テクスチャの GPU メモリ使用量削減
  4. 04高 DPI ディスプレイでの描画負荷軽減

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cloudai-x/threejs-skills --skill threejs-performance

インストール後、Claude Code で/threejs-performanceを入力して読み込まれたか確認します。

詳細

Three.jsのパフォーマンス

Three.js r186を対象としています。例ではWebGLRendererを使用します。WebGPUとTSLのセクションではWebGPURendererを使用します。

クイックスタート

import * as THREE from "three";

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.add(new THREE.HemisphereLight(0xffffff, 0x445566, 1));
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(3, 6, 4);
scene.add(sun);

const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 8);

const mesh = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 16), new THREE.MeshStandardMaterial({ color: 0x4488ff }));
scene.add(mesh);

function animate() {
  mesh.rotation.y += 0.01;
  renderer.render(scene, camera);
  // the counters reset at the start of every render() call, so these cover the frame just drawn
  console.log(`${renderer.info.render.calls} calls, ${renderer.info.render.triangles} triangles, ${renderer.info.memory.textures} textures`);
}

renderer.setAnimationLoop(animate);

まず測定する

勘で疑った箇所ではなく、数値が示す箇所を最適化しましょう。WebGLRendererのrenderer.infoオブジェクトは、render()の実行後に読み取ります。

function animate() {
  renderer.render(scene, camera);
  console.table({
    calls: renderer.info.render.calls, // draw calls submitted by this render
    triangles: renderer.info.render.triangles,
    lines: renderer.info.render.lines,
    points: renderer.info.render.points,
    geometries: renderer.info.memory.geometries, // geometry buffers live on the GPU
    textures: renderer.info.memory.textures,
    programs: renderer.info.programs.length, // compiled shader programs in the cache
  });
}

renderer.setAnimationLoop(animate);

これらの数値を読む際に知っておくべき点は次のとおりです。

  • info.autoResetがtrue(デフォルト)の場合、カウンターはrender()の呼び出しごとにリセットされます。render()の前に読み取ると前のフレームの数値が表示され、1フレーム内で複数回render()を呼び出す場合(ピッキング、反射、ポスト処理パスなど)は、最後の呼び出しの数値だけが表示されます。
  • シャドウマップの描画にも同じカウンターが使われます。render()内では、シャドウを落とすライトごとに、そのライトの視点からシャドウを落とすオブジェクトが再描画され、これらの描画はcallsとtrianglesに表示されます。ライトのcastShadowを切り替えると、ここに直接反映されます。
  • info.memory.geometriesとinfo.memory.texturesが数えるのはバイト数ではなくアップロード数です。対応するdispose()が実行されると数値は減少します。ジオメトリとテクスチャは一度アップロードされ、参照するすべてのメッシュで共有されます。

1フレーム内で複数回render()を呼び出す場合は、フレーム全体で自分で累積します。

renderer.info.autoReset = false; // render() stops clearing the counters

function animate() {
  renderer.render(scene, camera);
  console.log(renderer.info.render.calls); // every render of this frame, summed
  renderer.info.reset(); // once per frame, after the last render
}

renderer.setAnimationLoop(animate);

Timerはフレーム間隔を示します。単一フレームでは変動が大きすぎて比較できないため、1秒間のフレームで平均を取りましょう。間隔はディスプレイのリフレッシュ間隔(60 Hzでは16.7 ms)を下回らないため、フレーム落ちはわかりますが、余裕はわかりません。フレームのCPU処理時間を計測するには、処理の前後でperformance.now()を使います。

const timer = new THREE.Timer();
timer.connect(document); // pauses while the tab is hidden

let interval = 0;
let cpu = 0;
let frames = 0;

function animate() {
  timer.update();
  interval += timer.getDelta(); // seconds since the previous frame, never less than one display refresh
  const start = performance.now();
  renderer.render(scene, camera);
  cpu += performance.now() - start; // CPU time spent in render(); the GPU executes the commands later
  frames++;
  if (frames === 60) {
    console.log(`${((interval / frames) * 1000).toFixed(2)} ms per frame, ${(cpu / frames).toFixed(2)} ms CPU in render()`);
    interval = 0;
    cpu = 0;
    frames = 0;
  }
}

renderer.setAnimationLoop(animate);

フレーム時間より詳しく調べる場合は、ブラウザーのプロファイラーを使用してください。Chrome DevToolsのPerformanceパネルでは、メインスレッドの長時間タスク、スクリプト実行とレンダリングの時間、GPUトラック上のGPU処理を確認できます。数値を常時表示したい場合は、stats-glパッケージでフレームごとの統計情報をリアルタイムにオーバーレイ表示できます。

ドローコール

表示されている各メッシュはドローコールを 1 回発行します。マテリアルが配列の場合は、ジオメトリグループごとに 1 回発行します。また、次の 2 つの設定によりコール数が増えます。transparent マテリアルで side: THREE.DoubleSide を指定すると、各メッシュは裏面、続いて表面の順に 2 回描画されます(forceSinglePass: true を指定すると 1 回になります)。また、transmission が 0 より大きい MeshPhysicalMaterial が視界にある間は、すべての不透明オブジェクトが transmission バッファーにも 2 回目の描画を行います。この 2 つがなければ、単一マテリアルのメッシュが 1,000 個あると、ドローコールも 1,000 回です。注目すべき値は info.render.calls です。主な削減方法は次のとおりです。

静的ジオメトリをマージする。 動かず、同じマテリアルを共有する多数のメッシュを、1 つのメッシュと 1 回のコールにまとめます。

import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js";

const rockGeometry = new THREE.DodecahedronGeometry(1, 0);
const transform = new THREE.Matrix4();
const parts = [];
for (let i = 0; i < 200; i++) {
  const baked = rockGeometry.clone();
  transform.setPosition((Math.random() - 0.5) * 40, 0, (Math.random() - 0.5) * 40);
  baked.applyMatrix4(transform); // bake the placement into the vertices
  parts.push(baked);
}
const merged = BufferGeometryUtils.mergeGeometries(parts); // inputs need matching attributes and index state
scene.add(new THREE.Mesh(merged, new THREE.MeshNormalMaterial())); // 200 rocks, one call — but one bounding sphere for all of them

マージすると、コール数を減らす代わりにカリングの粒度が粗くなります。マージされたメッシュはバウンディングスフィアを 1 つだけ持つため、視錐台カリングは全体が対象になるか、まったく対象にならないかのどちらかです。ワールド全体を 1 つにマージするのではなく、ワールドサイズのクラスターをクラスターごとにマージしてください。

繰り返しをインスタンス化する。 インスタンスごとのトランスフォームを適用して同じジオメトリを何度も描画しても、コールは 1 回です。

const tree = new THREE.ConeGeometry(0.5, 2, 6);
const trees = new THREE.InstancedMesh(tree, new THREE.MeshNormalMaterial(), 500);
const placement = new THREE.Object3D();
for (let i = 0; i < 500; i++) {
  placement.position.set((Math.random() - 0.5) * 40, 1, (Math.random() - 0.5) * 40);
  placement.rotation.y = Math.random() * 6.28;
  placement.updateMatrix();
  trees.setMatrixAt(i, placement.matrix);
}
// culling tests one sphere around all instances (computed lazily on the first cull test), so all 500 draw whenever it touches the view;
// after moving instances later, set trees.instanceMatrix.needsUpdate = true and call trees.computeBoundingSphere()
scene.add(trees);

異なるジオメトリをバッチ処理する。 同じマテリアルを共有する多数のジオメトリを、共有バッファーにまとめます。

const material = new THREE.MeshNormalMaterial();
const batchedMesh = new THREE.BatchedMesh(500, 20000, 20000, material);
const coneId = batchedMesh.addGeometry(new THREE.ConeGeometry(0.5, 2, 6));
const boxId = batchedMesh.addGeometry(new THREE.BoxGeometry(1, 1, 1));
const placement = new THREE.Matrix4();
for (let i = 0; i < 500; i++) {
  const instanceId = batchedMesh.addInstance(i % 2 ? coneId : boxId); // a geometry renders only through an instance
  placement.makeTranslation((Math.random() - 0.5) * 40, 1, (Math.random() - 0.5) * 40);
  batchedMesh.setMatrixAt(instanceId, placement);
}
scene.add(batchedMesh);

InstancedMesh と異なり、各インスタンスを個別にカリングおよびソートします(perObjectFrustumCulled と sortObjects は、どちらもデフォルトで有効です)。ブラウザーが WEBGL_multi_draw に対応している場合、WebGLRenderer はこれらを 1 回のコールで複数描画します。対応していない場合はインスタンスごとに 1 回描画します。WebGPU バックエンドでは、インスタンスごとに 1 回描画します。両 API の詳細については、threejs-geometry をご覧ください。

距離に応じて詳細度を下げる。 LOD はすべてのレベルを子として保持し、各 render() の中で(autoUpdate はデフォルトで有効です)、現在のカメラ距離に応じたレベルだけを表示します。そのため、遠くのオブジェクトでは送信される三角形が少なくなりますが、コール数は変わりません。

const detailMaterial = new THREE.MeshNormalMaterial();
const lod = new THREE.LOD();
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 64, 32), detailMaterial), 0); // distances are camera-to-object
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 16, 8), detailMaterial), 12);
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 8, 4), detailMaterial), 30);
scene.add(lod);

カリングでコール全体をスキップする。 視錐台カリングはデフォルトで有効です。メッシュがキューに追加される前に、そのジオメトリのバウンディングスフィアがカメラの視錐台に対してテストされ、テストに失敗すると描画全体が取り除かれます。気づかないうちにカリングが機能しなくなる原因は 2 つあります。CPU 上で頂点を編集した後に geometry.computeBoundingSphere() を呼び出さないと、バウンディングスフィアが古いままになります。移動した頂点より小さくなったバウンディングスフィアが残ると、メッシュがまだ視界にあるのに非表示になります。また、シェーダー内で頂点を移動しても CPU 側のバウンディングスフィアは更新されません。そのため、シーン全体ではなく、そのメッシュだけに mesh.frustumCulled = false を設定してください。

GPU メモリ

info.memory.textures はテクスチャ数を数えます。バイト数の計算は自分で行う必要があります。RGBA8 テクスチャは 1 ピクセルあたり 4 バイトで、デフォルトのミップチェーンを含めると、WebGPURenderer のバイト数の推定値は 1.333 倍になります。2048 x 2048 のカラーマップは VRAM を約 22 MB 使用します。調整方法は 3 つあります。

  • KTX2 圧縮。 Basis Universal の KTX2 テクスチャ(ETC1S または UASTC)は、GPU が対応するブロック圧縮形式(ASTC、BC7、BC1 または BC3、ETC2、ETC1 または PVRTC)にトランスコードされ、PNG や JPEG のように 1 ピクセルあたり 4 バイトに展開されるのではなく、VRAM 内で圧縮されたままになります。これらの形式のいずれにも適合しない場合(ASTC は UASTC ファイルのみ、ETC1 はアルファ非対応、PVRTC はサイズが 2 の累乗である必要があります)、KTX2Loader は非圧縮 RGBA にフォールバックします。KTX2Loader で読み込みます。
import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js";

const ktx2Loader = new KTX2Loader().detectSupport(renderer);
const map = await ktx2Loader.loadAsync("color.ktx2"); // a CompressedTexture for a Basis Universal file
map.colorSpace = THREE.SRGBColorSpace; // KTX2Loader already sets it from the file's transfer function; this line only matters for a color file tagged linear
mesh.material.map = map;
mesh.material.needsUpdate = true; // the material rendered without a map before
  • サイズ。 1024 のテクスチャは 2048 のテクスチャの 4 分の 1 のサイズです。画面上で表示されるサイズに合わせてカラーマップを作成し、距離に応じた処理はミップマッピングに任せてください。
  • 破棄。 geometry.dispose() は GPU バッファーをすぐに解放します。texture.dispose() は、同じテクスチャを共有する、まだ破棄されていないテクスチャがなくなると GPU テクスチャを解放します(texture.clone() は画像を共有し、ラップ、フィルター、形式、色空間が同じであれば GPU コピーも共有します)。メッシュ数はカウントされないため、どこからも描画に使われなくなったものだけを破棄してください。破棄済みのジオメトリやテクスチャをまだ使うメッシュは、次の描画時に再度アップロードします。
scene.remove(mesh);
mesh.geometry.dispose(); // frees the GPU buffers now, even if another mesh still uses this geometry

// material.dispose() releases its shader programs (deleted once no material uses them), never its textures
for (const value of Object.values(mesh.material)) {
  if (value?.isTexture) value.dispose();
}
mesh.material.dispose();
mesh.dispose(); // renderer-side state kept for this one object

console.log(renderer.info.memory.textures); // the count drops as each texture frees

サブツリー全体を解放するには、オブジェクトを走査してください。object.dispose() は子を走査しません。WebGPURenderer では memory.geometries と memory.textures は数のままで、*Size フィールドと memory.total にバイト数の推定値が加わります。WebGPU のセクションをご覧ください。

フィルレート

フィルレートとは、フレームごとにシェーディングされるピクセル数のことです。描画バッファは width × ratio × height × ratio ピクセルなので、シェーディングのコストはピクセル比の2乗に比例します。3×ディスプレイでは、1×の9倍のピクセルをシェーディングします。上限を設定しましょう。

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

オーバードローと透明度。 不透明なジオメトリが最初に描画され(グループ、レンダー順、マテリアル、距離の順にソート)、次に透過性のあるもの、最後に透明なものが描画されます。透明なものはブレンドが正しく合成されるよう、奥から手前の順に描画されます。透明なレイヤー同士は互いを描画対象から除外できません。別の透明な面の背後にあるレイヤーもすべてそのピクセルをシェーディングし、画面全体を覆う透明なオーバーレイは、シーンに加えて画面のすべてのピクセルをもう一度シェーディングします。見た目に問題がなければ不透明なマテリアルを優先し、透明なクワッドは少数かつ小さく保ちましょう。オーバードローは renderer.info には表示されません。呼び出し数と三角形数は変わらず、変化するのはフレーム時間だけです。

シャドウマップ。 シャドウを描画する各ライトは、シャドウの更新が有効な間、毎フレーム、シャドウを落とすオブジェクトを深度テクスチャにレンダリングします。デフォルトのサイズは小さく(512 × 512)、必要性を確認したうえで大きくし、静的なシーンでは更新を停止しましょう。

const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(3, 6, 4);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
scene.add(sun);

renderer.shadowMap.enabled = true;

// static scenery: draw the shadow maps once instead of every frame
renderer.shadowMap.autoUpdate = false;
renderer.shadowMap.needsUpdate = true; // render them one more time, then freeze

// per light instead of globally: sun.shadow.autoUpdate = false; sun.shadow.needsUpdate = true;

シャドウを描画するライトごとに専用のパスが追加され、その描画は info.render.calls に表示されます。シャドウカメラの視錐台内にある castShadow オブジェクトをすべて再描画します。PointLight の場合、オブジェクトが含まれるキューブ面ごとに1回(最大6回)描画し、VSMShadowMap では receiveShadow オブジェクトも描画したうえで、さらに2回のブラー描画を行います。シャドウカメラの境界(sun.shadow.camera.left、right、top、bottom を設定し、その後 sun.shadow.camera.updateProjectionMatrix() を呼び出します)を適切に絞ると、描画されるシャドウを落とすオブジェクトを減らせます。マップサイズによって各パスのフィルコストが決まります。

ループ内の CPU 処理

静的な変換。 すべての render() 呼び出しでは scene.updateMatrixWorld() が実行され、すべてのオブジェクトを走査します。matrixAutoUpdate が true(デフォルト)の場合、位置、回転、スケールからローカル行列を再構成します。動かないシーンオブジェクトでは、行列を一度だけ合成しましょう。

mesh.matrixAutoUpdate = false;
mesh.updateMatrix(); // bake position/rotation/scale into mesh.matrix before the first render

// later, when this object does move:
mesh.position.set(1, 2, 3);
mesh.updateMatrix(); // manual now: the renderer no longer recomposes it

レイキャスティング。 intersectObjects(objects, true) はすべてのオブジェクトの子孫を走査し、それぞれをテストします。まずレイヤーをテストし、次にジオメトリのバウンディングスフィア、最後に三角形をテストします。結果をフィルタリングするのではなく、入力を絞り込みましょう。

const raycaster = new THREE.Raycaster();
raycaster.layers.set(1); // only objects enabled on layer 1 are tested, before any geometry work

const pickables = [];
scene.traverse((child) => {
  if (child.isMesh && child.userData.pickable) {
    child.layers.enable(1); // adds layer 1 while keeping layer 0 membership
    pickables.push(child);
  }
});

const hits = raycaster.intersectObjects(pickables, false); // an explicit list, no recursion

レイキャスティングを実行する pointermove ハンドラーはイベントごとに動作するため、ピッキングを行うシーンでは高負荷になります。大規模な静的ジオメトリでは、three-mesh-bvh パッケージによって三角形のテストを高速化できます。

ループ内でメモリを割り当てない。 フレームごとに作成されるオブジェクトはガベージになり、ガベージコレクションによる一時停止が定期的なフレームスパイクとして現れます。ループの外で宣言したベクトルや行列を再利用しましょう。

const timer = new THREE.Timer();
timer.connect(document);

const step = new THREE.Vector3(); // allocated once, reused every frame
const target = new THREE.Vector3(0, 1, 0);

function animate() {
  timer.update();
  step.subVectors(target, mesh.position).multiplyScalar(Math.min(timer.getDelta() * 2, 1));
  mesh.position.add(step);
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

シェーダーコンパイルによる停止

プログラムは遅延ビルドされます。マテリアルがこれまでに見たことのない条件(ライトの数が異なる、フォグが有効になるなど)で初めてレンダリングされるフレームでは、シェーダーのコンパイル中に停止します。代わりに、読み込み中にビルドしましょう。

// during loading, before the first frame: compile everything the scene will render with
await renderer.compileAsync(scene, camera); // uses KHR_parallel_shader_compile where available

renderer.setAnimationLoop(() => {
  renderer.render(scene, camera); // material programs are ready; shadow-depth programs still build on the first shadow render
});

renderer.compile(scene, camera) も同じビルドを開始しますが、すぐに戻るため、まだ完了していないプログラムがあれば、最初のレンダリング時に引き続き待つことになります。非同期形式を優先しましょう。シェーダーにコンパイルされる設定(transparent、side、blending、flatShading、vertexColors、dithering、premultipliedAlpha、alphaHash、またはマップの追加・削除)を変更すると、material.needsUpdate = true が必要になり、新しいプログラムがビルドされます。そのため、これらの設定を実行時に切り替えるのは避けましょう(すべての設定は threejs-materials に記載されています)。

WebGPU と TSL

import * as THREE from "three/webgpu";
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111); // the WebGPU canvas is transparent by default
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 4, 18);
function animate() {
  renderer.render(scene, camera);
  // autoReset clears per-frame counters at each loop tick; render.calls counts render passes since the app started
  console.log(`${renderer.info.render.drawCalls} draw calls, ${renderer.info.memory.total} bytes of GPU memory`);
}
renderer.setAnimationLoop(animate);

コンピュート処理によってパーティクル処理を CPU から切り離します。パーティクルごとのループも、フレームごとのバッファー再アップロードも不要で、1 回のディスパッチですべてのパーティクルを更新できます。

import * as THREE from "three/webgpu";
import { Fn, cos, hash, instanceIndex, instancedArray, sin, time, TWO_PI, vec3 } from "three/tsl";
const count = 100000;
const positionBuffer = instancedArray(count, "vec3"); // a storage buffer living on the GPU
const simulate = Fn(() => {
  const rand = hash(instanceIndex);
  const angle = rand.mul(TWO_PI).add(time); // time updates itself every frame
  positionBuffer.element(instanceIndex).assign(vec3(cos(angle).mul(8), sin(rand.mul(97).add(time.mul(2))).mul(2).add(2), sin(angle).mul(8)));
});
const simulateCompute = simulate().compute(count); // one compute thread per particle
const material = new THREE.SpriteNodeMaterial();
material.positionNode = positionBuffer.toAttribute(); // the material reads the storage buffer directly
material.colorNode = vec3(1, 0.7, 0.3);
const particles = new THREE.Sprite(material);
particles.count = count; // the sprite draws once per buffer element
particles.frustumCulled = false; // culling tests one small sphere at the sprite origin, not the particles
scene.add(particles);
function animate() {
  renderer.compute(simulateCompute); // GPU updates the buffer, then the draw reads it
  renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
  • WebGPU のカウンターにはそれぞれ固有の名前があります。info.render.drawCalls はフレームごと、info.render.calls(レンダーパス:各 render() と、出力パスなどの内部パス)はアプリの起動後からの累積値、info.calls(レンダーとコンピュートの呼び出し)はアプリの起動後からの累積値、info.compute.frameCalls はフレームごとです。また、info.memory.total は頂点バッファー、インデックスバッファー、ストレージバッファー、テクスチャ、ユニフォームバッファー、プログラムのバイト数の推定値です(memory.texturesSize、memory.attributesSize、その他の *Size フィールドに内訳が記録されます)。圧縮テクスチャはこの値では 1 バイトとして数えられます。これは概算値のため、KTX2 のメモリ使用量は実際より少なく表示されます。
  • 2 つ目のブロックのループは、1 つ目のブロックの animate を置き換えます。await renderer.init() がすでに実行されているため、renderer.compute() は直接送信されます。

よくある間違い

  • function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } → renderer.setAnimationLoop(animate);:WebXR セッションでは、セッション独自のフレームループを通じて、setAnimationLoop に渡されたコールバックだけが実行されます。また、WebGPURenderer では、setAnimationLoop は最初のフレームの前に init() も待機します。
  • const clock = new THREE.Clock(); const dt = clock.getDelta(); const t = clock.getElapsedTime(); → const timer = new THREE.Timer(); timer.update(); const dt = timer.getDelta(); const t = timer.getElapsed();:Clock は r183 で非推奨になりました。また、getElapsedTime() は内部で getDelta() を呼び出すため、同じフレーム内で後から getDelta() を呼び出すと、値はほぼゼロになります。
  • console.log(renderer.info.render.calls); renderer.render(scene, camera); → renderer.render(scene, camera); console.log(renderer.info.render.calls);:カウンターは render() の開始時にリセットされるため、その前に読み取ると前のフレームの値が報告されます。
  • renderer.info.autoReset = false → renderer.info.autoReset = false; function animate() { renderer.render(scene, camera); console.log(renderer.info.render.calls); renderer.info.reset(); }:autoReset を無効にすると、各フレームの最後の render() の後に reset() を1回実行しない限り、カウンターはリセットされず、フレームをまたいで累積します。
  • webgpuRenderer.info.render.calls → webgpuRenderer.info.render.drawCalls:WebGPURenderer では、render.calls はアプリの起動後のレンダーパス数をカウントし、drawCalls はこのフレームの描画呼び出し数をカウントします。WebGLRenderer には drawCalls がなく、render.calls は render() ごとの描画数です。
  • renderer.compile(scene, camera) → await renderer.compileAsync(scene, camera):compile() はビルドをキューに登録して戻るため、その直後にレンダリングしても、未完了のプログラムの処理でブロックされます。一方、compileAsync() は、ブラウザーが KHR_parallel_shader_compile 拡張機能を公開している場合に、すべてのプログラムが完了したことをその拡張機能が報告してから解決します。
  • renderer.shadowMap.type = THREE.PCFSoftShadowMap → renderer.shadowMap.type = THREE.PCFShadowMap:PCFSoftShadowMap は r182 で非推奨になり、コンソールに警告を出して PCF にフォールバックします。
  • mesh.material.dispose() → mesh.material.dispose(); for (const value of Object.values(mesh.material)) if (value?.isTexture) value.dispose();:material.dispose() はシェーダープログラムを解放しますが、マテリアルのテクスチャは解放しません。そのため、info.memory.textures の値が減るのは、各テクスチャに対して texture.dispose() を実行したときだけです。
  • gltf.scene.dispose() → gltf.scene.traverse((child) => { child.geometry?.dispose(); for (const material of [child.material ?? []].flat()) { for (const value of Object.values(material)) if (value?.isTexture) value.dispose(); material.dispose(); } child.dispose(); }):Object3D.dispose() が解放するのは、そのオブジェクト1つ分のレンダラーの状態だけです。子オブジェクト、ジオメトリ、マテリアル、テクスチャは解放しません。
  • const hits = raycaster.intersectObjects(scene.children, true) → raycaster.layers.set(1); const hits = raycaster.intersectObjects(pickables, false);:再帰呼び出しではイベントごとにすべての子孫をたどってテストします。一方、レイヤーチェックは各オブジェクトのジオメトリテストより前に実行され、明示的なリストを使えばシーンの残りの部分にはアクセスしません。
  • scene.traverse((child) => (child.frustumCulled = false)) → geometry.computeBoundingSphere(); // after moving vertices on the CPU:カリングは各ジオメトリのバウンディングスフィアを使って描画呼び出し全体をスキップします。そのため、シーン全体でカリングを無効にすると、毎フレームすべてが描画されます。
  • function animate() { mesh.position.add(new THREE.Vector3(0, 0.01, 0)); } → const offset = new THREE.Vector3(0, 0.01, 0); function animate() { mesh.position.add(offset); }:フレームごとの割り当てはすべてガベージになり、ガベージコレクターが回収のために一時停止する必要があります。その結果、定期的なフレーム時間の急増が発生します。
  • const dt = uniform(0); /* fed from the timer each frame */ → const dt = deltaTime;:組み込みの time ノードと deltaTime ノードは、レンダラーが各レンダリングで更新するユニフォームです。そのため、値を手動で渡すと重複し、独自の Timer も必要になります。

関連項目

  • threejs-fundamentals - ここで測定するすべてを支える、レンダラーの構築、レンダーループ、リサイズ処理です。
  • threejs-geometry - ドローコール削減の基盤となる、インスタンシング、バッチ処理、マージ、属性の各 API 全般です。
  • threejs-materials - シェーダーにコンパイルされ、再構築を引き起こすマテリアル設定について説明します。
  • threejs-loaders - これらの調整項目によって生成される圧縮ファイル向けの、KTX2、Draco、meshopt ローダーの連携について説明します。
  • threejs-lighting - このスキルでコストを測定する、ライトの種類、単位、シャドウの設定について説明します。
  • threejs-postprocessing - コンポーザー独自のバッファー、MSAA、ピクセル比の設定について説明します。
  • threejs-asset-pipeline - KTX2 テクスチャや、圧縮・インスタンス化・結合された glTF ファイルを生成する gltf-transform コマンドについて説明します。

コメント