Stack Overflowで質問したところ、素晴らしい回答をgman氏から頂けたので転載する。
ちなみにStack Overflow上のコードは特段の記述が無い限り、CC BY-SA 3.0でライセンスされる。
gman氏はさらに自身のコードをCC0でライセンスすると明言しているので、この記事にライセンス上の問題は特にない。
問題
例えばこの動作サンプルにおける剣士メッシュを構成する各頂点の座標を取得するにはどうすればよいか?
ボーンアニメーションやモーフアニメーションをしないメッシュの頂点座標
まず前提知識として。
こちらは簡単に取得できる。
const localPosition = mesh.geometry.vertices[vertexIndex];
mesh.updateMatrixWorld();
const globalPosition = mesh.geometry.vertices[vertexIndex].clone();
globalPosition.applyMatrix4(mesh.matrixWorld);
先の例の剣士メッシュはボーンアニメーション、モーフアニメーションを行っているのでこの方法では頂点座標を取得できない。
ボーンアニメーションやモーフアニメーションの効果が及ぶ前の頂点座標が取得できるのみである。
ボーンアニメーションやモーフアニメーションをするメッシュの頂点座標
そもそもの作りとして、Three.jsはJavaScriptのコード中でGLSLのプログラムを自動生成し、描画処理の大半を自動生成したGLSLに任せている。
ボーンアニメーションやモーフアニメーションをするメッシュの頂点座標を計算する処理もGLSLの中で行われており、JavaScript側で計算結果を取得することは原則できない。
そこでgman氏はGLSL中の頂点座標計算処理をJavaScriptに翻訳した。
なお、あらゆるケースで完璧に動く保証まではしないので、うまく動かないケースに当たったら頑張れということである。
// 各種パラメータを最新の状態にしておく
mesh.updateMatrixWorld();
mesh.skeleton.update();
// バッファ
const position = new THREE.Vector3();
const transformed = new THREE.Vector3();
const temp1 = new THREE.Vector3();
const tempBoneMatrix = new THREE.Matrix4();
const tempSkinnedVertex = new THREE.Vector3();
const tempSkinned = new THREE.Vector3();
// 頂点座標計算処理
for (let vndx = 0; vndx < mesh.geometry.vertices.length; ++vndx) {
position.copy(mesh.geometry.vertices[vndx]);
transformed.copy(position);
for (let i = 0; i < mesh.geometry.morphTargets.length; ++i) {
temp1.copy(mesh.geometry.morphTargets[i].vertices[vndx]);
transformed.add(temp1.sub(position).multiplyScalar(mesh.morphTargetInfluences[i]));
}
tempSkinnedVertex.copy(transformed).applyMatrix4(mesh.bindMatrix);
tempSkinned.set(0, 0, 0);
const skinIndices = mesh.geometry.skinIndices[vndx];
const skinWeights = mesh.geometry.skinWeights[vndx];
for (let i = 0; i < 4; ++i) {
const boneNdx = skinIndices.getComponent(i);
const weight = skinWeights.getComponent(i);
tempBoneMatrix.fromArray(mesh.skeleton.boneMatrices, boneNdx * 16);
temp1.copy(tempSkinnedVertex);
tempSkinned.add(temp1.applyMatrix4(tempBoneMatrix).multiplyScalar(weight));
}
transformed.copy(tempSkinned).applyMatrix4(mesh.bindMatrixInverse);
// この時点でのtransformedがmeshのvndx番目の頂点のローカル座標
transformed.applyMatrix4(mesh.matrixWorld);
// この時点でのtransformedがmeshのvndx番目の頂点のグローバル座標
}
動作サンプル
剣士メッシュを構成する各頂点の座標に赤い箱を設置している。
正しく頂点座標を取得できていることが分かる。
ただ、本来GPUでやる処理をCPUでやっているのでカクカクにはなってしまう。
おまけ: 描画に使われているGLSLのコードの取得方法
あるmaterial
で描画に使われている頂点シェーダのソースはrenderer.context.getShaderSource(material.program.vertexShader)
、ピクセルシェーダのソースはrenderer.context.getShaderSource(material.program.fragmentShader)
で見れる模様。