Skip to content

Commit

Permalink
Merge pull request #1535 from mrxz/combineSkeletons
Browse files Browse the repository at this point in the history
Add `VRMUtils.combineSkeletons` for reducing the number of `THREE.Skeleton`
  • Loading branch information
0b5vr authored Nov 26, 2024
2 parents 2d9acac + 1f0c9e9 commit e3e48f1
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 13 deletions.
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/animations.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/bones.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/debug.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@

// calling this function greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/dnd.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

if ( currentVrm ) {

Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/expressions.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/firstperson.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/lookat-advanced.html
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/lookat.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/materials-debug.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/meta.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/mouse.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene );
VRMUtils.combineSkeletons( gltf.scene );

// Disable frustum culling
vrm.scene.traverse( ( obj ) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/three-vrm/examples/webgpu-dnd.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@

// calling these functions greatly improves the performance
VRMUtils.removeUnnecessaryVertices( gltf.scene );
VRMUtils.removeUnnecessaryJoints( gltf.scene, { experimentalSameBoneCounts: true } );
VRMUtils.combineSkeletons( gltf.scene );

if ( currentVrm ) {

Expand Down
121 changes: 121 additions & 0 deletions packages/three-vrm/src/VRMUtils/combineSkeletons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as THREE from 'three';

/**
* Traverses the given object and combines the skeletons of skinned meshes.
*
* Each frame the bone matrices are computed for every skeleton. Combining skeletons
* reduces the number of calculations needed, improving performance.
*
* @param root Root object that will be traversed
*/
export function combineSkeletons(root: THREE.Object3D): void {
const skinnedMeshes = new Set<THREE.SkinnedMesh>();
const geometryToSkinnedMesh = new Map<THREE.BufferGeometry, THREE.SkinnedMesh>();

// Traverse entire tree and collect skinned meshes
root.traverse((obj) => {
if (obj.type !== 'SkinnedMesh') {
return;
}

const skinnedMesh = obj as THREE.SkinnedMesh;

// Check if the geometry has already been encountered
const previousSkinnedMesh = geometryToSkinnedMesh.get(skinnedMesh.geometry);
if (previousSkinnedMesh) {
// Skinned meshes that share their geometry with other skinned meshes can't be processed.
// The skinnedMeshes already contain previousSkinnedMesh, so remove it now.
skinnedMeshes.delete(previousSkinnedMesh);
} else {
geometryToSkinnedMesh.set(skinnedMesh.geometry, skinnedMesh);
skinnedMeshes.add(skinnedMesh);
}
});

// Prepare new skeletons for the skinned meshes
const newSkeletons: Array<{ bones: THREE.Bone[]; boneInverses: THREE.Matrix4[]; meshes: THREE.SkinnedMesh[] }> = [];
skinnedMeshes.forEach((skinnedMesh) => {
const skeleton = skinnedMesh.skeleton;

// Find suitable skeleton
let newSkeleton = newSkeletons.find((candidate) => skeletonMatches(skeleton, candidate));
if (!newSkeleton) {
newSkeleton = { bones: [], boneInverses: [], meshes: [] };
newSkeletons.push(newSkeleton);
}

// Add skinned mesh to the new skeleton
newSkeleton.meshes.push(skinnedMesh);

// Determine bone index mapping from skeleton -> newSkeleton
const boneIndexMap: number[] = skeleton.bones.map((bone) => newSkeleton.bones.indexOf(bone));

// Update skinIndex attribute
const geometry = skinnedMesh.geometry;
const attribute = geometry.getAttribute('skinIndex');
const weightAttribute = geometry.getAttribute('skinWeight');

for (let i = 0; i < attribute.count; i++) {
for (let j = 0; j < attribute.itemSize; j++) {
// check bone weight
const weight = weightAttribute.getComponent(i, j);
if (weight === 0) {
continue;
}

const index = attribute.getComponent(i, j);

// new skinIndex buffer
if (boneIndexMap[index] === -1) {
boneIndexMap[index] = newSkeleton.bones.length;
newSkeleton.bones.push(skeleton.bones[index]);
newSkeleton.boneInverses.push(skeleton.boneInverses[index]);
}

attribute.setComponent(i, j, boneIndexMap[index]);
}
}

attribute.needsUpdate = true;
});

// Bind new skeleton to the meshes
for (const { bones, boneInverses, meshes } of newSkeletons) {
const newSkeleton = new THREE.Skeleton(bones, boneInverses);
meshes.forEach((mesh) => mesh.bind(newSkeleton, new THREE.Matrix4()));
}
}

/**
* Checks if a given skeleton matches a candidate skeleton. For the skeletons to match,
* all bones must either be in the candidate skeleton with the same boneInverse OR
* not part of the candidate skeleton (as it can be added to it).
* @param skeleton The skeleton to check.
* @param candidate The candidate skeleton to match against.
*/
function skeletonMatches(skeleton: THREE.Skeleton, candidate: { bones: THREE.Bone[]; boneInverses: THREE.Matrix4[] }) {
return skeleton.bones.every((bone, index) => {
const candidateIndex = candidate.bones.indexOf(bone);
if (candidateIndex !== -1) {
return matrixEquals(skeleton.boneInverses[index], candidate.boneInverses[candidateIndex]);
}
return true;
});
}

// https://github.com/mrdoob/three.js/blob/r170/test/unit/src/math/Matrix4.tests.js#L12
function matrixEquals(a: THREE.Matrix4, b: THREE.Matrix4, tolerance?: number) {
tolerance = tolerance || 0.0001;
if (a.elements.length != b.elements.length) {
return false;
}

for (let i = 0, il = a.elements.length; i < il; i++) {
const delta = Math.abs(a.elements[i] - b.elements[i]);
if (delta > tolerance) {
return false;
}
}

return true;
}
2 changes: 2 additions & 0 deletions packages/three-vrm/src/VRMUtils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { combineSkeletons } from './combineSkeletons';
import { deepDispose } from './deepDispose';
import { removeUnnecessaryJoints } from './removeUnnecessaryJoints';
import { removeUnnecessaryVertices } from './removeUnnecessaryVertices';
Expand All @@ -8,6 +9,7 @@ export class VRMUtils {
// this class is not meant to be instantiated
}

public static combineSkeletons = combineSkeletons;
public static deepDispose = deepDispose;
public static removeUnnecessaryJoints = removeUnnecessaryJoints;
public static removeUnnecessaryVertices = removeUnnecessaryVertices;
Expand Down

0 comments on commit e3e48f1

Please sign in to comment.