この記事は、three.jsにおけるジオメトリ結合の
の3点を共有するためのものです。
お手元のthree.jsのバージョンを確認してからお読みください。バージョンが異なる場合、記事の内容が適用できない場合があります。
この記事では、three.jsのインストール方法は取り扱いません。あらかじめご承知ください。
まずはデモをご覧ください。画像をクリックすると、それぞれのデモページに遷移します。
手元の環境で、デモAの描画速度は10FPS程度、Bは60FPSです。またデモBのBox数はデモAの約4.1倍です。
2つのデモのもっとも大きな差はドローコールの数です。
デモAは1フレームあたり15625回のドローコールを処理しています。
デモBは1フレームあたり1回のドローコールしか処理していません。
では、パフォーマンスに大きな影響を与えたドローコールとは、いったいどんなものでしょう? ドローコール ( Draw calls ) とは、グラフィックスAPIにおける描画命令の総称です。具体的にどんな関数を呼び出すかは、グラフィックスAPIによって変わります。
一例として、three.jsではWebGLのdrawArrays関数を呼び出しています。WebGLRendererのこの箇所でdrawArrays関数を呼び出しています。drawArrays
関数を呼び出した段階でバッファーに描画結果が書き込まれます。Meshの重なり順にあわせて複数回drawArrays
関数を呼び出した後、WebGLはflush
関数でバッファーを画面に転送します。
参考記事 : wgld.org | ポリゴンのレンダリング
three.jsのWebGLRendererは、レンダリングの統計情報を持っています。 WebGLRenderer.info.renderプロパティで、最終フレームの統計情報にアクセスできます。
このドローコールを減らせば描画パフォーマンスが向上するということが、これまでのデモから判明してきました。それでは具体的にどうやってドローコールを減らせばいいのでしょうか?
ドローコールは、独自のジオメトリとマテリアルを持ったメッシュ1つにつき1回実行されます。デモAの場合XYZ軸にそれぞれ25、合計15625個のBoxがありますのでドローコールも15625回実行されます。
ドローコールを削減する方法はいくつかありますが、今回はジオメトリを結合して1つにするという方針を採用しました。ジオメトリを結合すれば、そのジオメトリがどんな複雑な形状でもドローコールは1回しか実行されません。
ジオメトリの結合方法を説明する前に、必要となる用語を整理します。
ジオメトリとは立体形状のことです。three.jsにおいては、立体形状であるジオメトリと、質感を表すマテリアルが1セットになって立体を構成しています。
アトリビュートとは、各頂点に割り当てられた属性のことです。ジオメトリは複数のアトリビュートで構成されます。three.jsにおいてはpositionもアトリビュートの一種として扱われます。頂点に個別の色を割り当てる場合はジオメトリにcolorアトリビュートを追加します。これ以外にもユーザーが任意のアトリビュートを追加できます。
three.jsにはmergeBufferGeometriesというユーティリティ関数があります。この関数にジオメトリを格納した配列を渡すと、結合されたジオメトリが返ってきます。
mergeBufferGeometries
で結合したあとも、ジオメトリに設定したカスタムアトリビュートは維持されます。ジオメトリをインスタンス化したり、各種ローダーで読み込んだ後、結合する前にカスタムアトリビュートを設定することで、結合前のモデルを区別できます。
three.jsのr127で各頂点に個別のAlpha値を割り当てるvertexAlpha
が追加されました。
現在のバージョンでは
という設定をすることで、vertexAlphaが有効になります。
上記の設定3は、具体的には以下のように指定します。
const count = geometry.getAttribute("position").count; //頂点の数を調べる。
/**
* 頂点の数の4倍の固定長配列を生成して、colorという名前でGeometryのAttributeに追加する。
*/
geometry.setAttribute(
'color',
new THREE.BufferAttribute( new Float32Array(count * 4), 4 )
);
原点からの距離でアルファを変化させるデモです。
BufferAttributeが格納するデータは固定長配列です。そのためジオメトリを後から追加したり、削り落としたりはできません。 positionを含むすべてのAttributeを再生成してセットすれば増減は可能ですが、CPUの負荷が大きくなり現実的な速度で動きません。
こちらの公式サンプルに、BufferGeometryでのマウスインタラクションの方法があります。
しかしこのサンプル1ポリゴンを対象としてマウスイベントを受け付けているため、ジオメトリ結合前のモデル単位でのマウスインタラクションができません。
BufferGeometryでマウスインタラクションをさせる場合、ジオメトリの結合前に下処理をするという方法があります。
参考記事 : Raycaster : Get ID of individual geometries of a mergeBufferGeometry
デモDでは、結合後のジオメトリから、結合前のIDを取り出すために以下の処理をしています。
デモページのソースがありますが、ここには上記の処理に関連する部分を抜粋します。
▼ カスタムAttributeにメッシュIDを格納する。
// ジオメトリ生成処理。個別のIDをattributeに格納する。
const generateCube = (x: number, y: number, z: number, id: number) => {
const geometry = new BoxGeometry();
const positions = geometry.getAttribute("position");
const count = positions.count;
geometry.setAttribute(
"mesh_id",
new BufferAttribute(new Float32Array(count), 1)
);
const idAttribute = geometry.getAttribute("mesh_id");
for (let i = 0; i < count; i++) {
idAttribute.setX(i, id); //各頂点にIDを割り振る
}
geometryArray.push(geometry);
};
let idCounter = 0;
for (let x = 0; x < numCube; x++) {
for (let y = 0; y < numCube; y++) {
for (let z = 0; z < numCube; z++) {
generateCube(x, y, z, idCounter);
idCounter++;
}
}
}
//ジオメトリをマージする。"mesh_id"Attributeは結合後も維持される。
const mergedMesh = new Mesh(
mergeBufferGeometries(geometryArray),
new MeshBasicMaterial({ transparent: true, vertexColors: true })
);
▼ レンダリング中にマウスポインターとの交差を確認する
const rendering = () => {
this.raycaster?.setFromCamera(this.mousePoint, camera);
const intersects = this.raycaster?.intersectObject(mergedMesh, false);
if (intersects?.length > 0) {
const idAttribute = geo.getAttribute("mesh_id");
/**
* IDアトリビュートに、Raycasterで取得したfaceのa,b,cのいずれかをインデックスとしてアクセスすると、
* "mesh_id"が取り出せる。
**/
const meshID = idAttribute.getX(intersects[0].face.a);
const count = idAttribute.count;
for (let i = 0; i < count; i++) {
if (idAttribute.getX(i) === meshID) {
/**
* ここで各頂点を操作する。colorAttributeを上書きするなど。
* 変更したAttributeはneedUpdateを設定しないと画面に反映されないので注意。
**/
colorAttribute.needsUpdate = true;
}
}
}
renderer.render(scene, camera);
requestAnimationFrame(rendering);
};
レンダリングループのたびにcolorAttributeを書き換えるため、CPU負荷が大きい処理です。より複雑な結合ジオメトリでのインタラクションを実装する場合は、一部処理をシェーダーに置き換えることも検討してください。
WebGLはPCの処理性能が向上した現在でも軽い処理ではありません。豊富なリソースでゴリ押しできるようになるにはまだまだ時間がかかりそうです。それまではGPUがどのような処理を得意とするのかを把握し、設計の段階でコンテンツを得意な形に落とし込む必要があります。