Skip to content

Commit e3e48f1

Browse files
authored
Merge pull request #1535 from mrxz/combineSkeletons
Add `VRMUtils.combineSkeletons` for reducing the number of `THREE.Skeleton`
2 parents 2d9acac + 1f0c9e9 commit e3e48f1

15 files changed

+136
-13
lines changed

packages/three-vrm/examples/animations.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181

8282
// calling these functions greatly improves the performance
8383
VRMUtils.removeUnnecessaryVertices( gltf.scene );
84-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
84+
VRMUtils.combineSkeletons( gltf.scene );
8585

8686
// Disable frustum culling
8787
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/basic.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282

8383
// calling these functions greatly improves the performance
8484
VRMUtils.removeUnnecessaryVertices( gltf.scene );
85-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
85+
VRMUtils.combineSkeletons( gltf.scene );
8686

8787
// Disable frustum culling
8888
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/bones.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080

8181
// calling these functions greatly improves the performance
8282
VRMUtils.removeUnnecessaryVertices( gltf.scene );
83-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
83+
VRMUtils.combineSkeletons( gltf.scene );
8484

8585
// Disable frustum culling
8686
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/debug.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888

8989
// calling this function greatly improves the performance
9090
VRMUtils.removeUnnecessaryVertices( gltf.scene );
91-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
91+
VRMUtils.combineSkeletons( gltf.scene );
9292

9393
// Disable frustum culling
9494
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/dnd.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282

8383
// calling these functions greatly improves the performance
8484
VRMUtils.removeUnnecessaryVertices( gltf.scene );
85-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
85+
VRMUtils.combineSkeletons( gltf.scene );
8686

8787
if ( currentVrm ) {
8888

packages/three-vrm/examples/expressions.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080

8181
// calling these functions greatly improves the performance
8282
VRMUtils.removeUnnecessaryVertices( gltf.scene );
83-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
83+
VRMUtils.combineSkeletons( gltf.scene );
8484

8585
// Disable frustum culling
8686
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/firstperson.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080

8181
// calling these functions greatly improves the performance
8282
VRMUtils.removeUnnecessaryVertices( gltf.scene );
83-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
83+
VRMUtils.combineSkeletons( gltf.scene );
8484

8585
// Disable frustum culling
8686
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/lookat-advanced.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151

152152
// calling these functions greatly improves the performance
153153
VRMUtils.removeUnnecessaryVertices( gltf.scene );
154-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
154+
VRMUtils.combineSkeletons( gltf.scene );
155155

156156
// Disable frustum culling
157157
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/lookat.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484

8585
// calling these functions greatly improves the performance
8686
VRMUtils.removeUnnecessaryVertices( gltf.scene );
87-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
87+
VRMUtils.combineSkeletons( gltf.scene );
8888

8989
// Disable frustum culling
9090
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/materials-debug.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080

8181
// calling these functions greatly improves the performance
8282
VRMUtils.removeUnnecessaryVertices( gltf.scene );
83-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
83+
VRMUtils.combineSkeletons( gltf.scene );
8484

8585
// Disable frustum culling
8686
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/meta.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797

9898
// calling these functions greatly improves the performance
9999
VRMUtils.removeUnnecessaryVertices( gltf.scene );
100-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
100+
VRMUtils.combineSkeletons( gltf.scene );
101101

102102
// Disable frustum culling
103103
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/mouse.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575

7676
// calling these functions greatly improves the performance
7777
VRMUtils.removeUnnecessaryVertices( gltf.scene );
78-
VRMUtils.removeUnnecessaryJoints( gltf.scene );
78+
VRMUtils.combineSkeletons( gltf.scene );
7979

8080
// Disable frustum culling
8181
vrm.scene.traverse( ( obj ) => {

packages/three-vrm/examples/webgpu-dnd.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898

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

103103
if ( currentVrm ) {
104104

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import * as THREE from 'three';
2+
3+
/**
4+
* Traverses the given object and combines the skeletons of skinned meshes.
5+
*
6+
* Each frame the bone matrices are computed for every skeleton. Combining skeletons
7+
* reduces the number of calculations needed, improving performance.
8+
*
9+
* @param root Root object that will be traversed
10+
*/
11+
export function combineSkeletons(root: THREE.Object3D): void {
12+
const skinnedMeshes = new Set<THREE.SkinnedMesh>();
13+
const geometryToSkinnedMesh = new Map<THREE.BufferGeometry, THREE.SkinnedMesh>();
14+
15+
// Traverse entire tree and collect skinned meshes
16+
root.traverse((obj) => {
17+
if (obj.type !== 'SkinnedMesh') {
18+
return;
19+
}
20+
21+
const skinnedMesh = obj as THREE.SkinnedMesh;
22+
23+
// Check if the geometry has already been encountered
24+
const previousSkinnedMesh = geometryToSkinnedMesh.get(skinnedMesh.geometry);
25+
if (previousSkinnedMesh) {
26+
// Skinned meshes that share their geometry with other skinned meshes can't be processed.
27+
// The skinnedMeshes already contain previousSkinnedMesh, so remove it now.
28+
skinnedMeshes.delete(previousSkinnedMesh);
29+
} else {
30+
geometryToSkinnedMesh.set(skinnedMesh.geometry, skinnedMesh);
31+
skinnedMeshes.add(skinnedMesh);
32+
}
33+
});
34+
35+
// Prepare new skeletons for the skinned meshes
36+
const newSkeletons: Array<{ bones: THREE.Bone[]; boneInverses: THREE.Matrix4[]; meshes: THREE.SkinnedMesh[] }> = [];
37+
skinnedMeshes.forEach((skinnedMesh) => {
38+
const skeleton = skinnedMesh.skeleton;
39+
40+
// Find suitable skeleton
41+
let newSkeleton = newSkeletons.find((candidate) => skeletonMatches(skeleton, candidate));
42+
if (!newSkeleton) {
43+
newSkeleton = { bones: [], boneInverses: [], meshes: [] };
44+
newSkeletons.push(newSkeleton);
45+
}
46+
47+
// Add skinned mesh to the new skeleton
48+
newSkeleton.meshes.push(skinnedMesh);
49+
50+
// Determine bone index mapping from skeleton -> newSkeleton
51+
const boneIndexMap: number[] = skeleton.bones.map((bone) => newSkeleton.bones.indexOf(bone));
52+
53+
// Update skinIndex attribute
54+
const geometry = skinnedMesh.geometry;
55+
const attribute = geometry.getAttribute('skinIndex');
56+
const weightAttribute = geometry.getAttribute('skinWeight');
57+
58+
for (let i = 0; i < attribute.count; i++) {
59+
for (let j = 0; j < attribute.itemSize; j++) {
60+
// check bone weight
61+
const weight = weightAttribute.getComponent(i, j);
62+
if (weight === 0) {
63+
continue;
64+
}
65+
66+
const index = attribute.getComponent(i, j);
67+
68+
// new skinIndex buffer
69+
if (boneIndexMap[index] === -1) {
70+
boneIndexMap[index] = newSkeleton.bones.length;
71+
newSkeleton.bones.push(skeleton.bones[index]);
72+
newSkeleton.boneInverses.push(skeleton.boneInverses[index]);
73+
}
74+
75+
attribute.setComponent(i, j, boneIndexMap[index]);
76+
}
77+
}
78+
79+
attribute.needsUpdate = true;
80+
});
81+
82+
// Bind new skeleton to the meshes
83+
for (const { bones, boneInverses, meshes } of newSkeletons) {
84+
const newSkeleton = new THREE.Skeleton(bones, boneInverses);
85+
meshes.forEach((mesh) => mesh.bind(newSkeleton, new THREE.Matrix4()));
86+
}
87+
}
88+
89+
/**
90+
* Checks if a given skeleton matches a candidate skeleton. For the skeletons to match,
91+
* all bones must either be in the candidate skeleton with the same boneInverse OR
92+
* not part of the candidate skeleton (as it can be added to it).
93+
* @param skeleton The skeleton to check.
94+
* @param candidate The candidate skeleton to match against.
95+
*/
96+
function skeletonMatches(skeleton: THREE.Skeleton, candidate: { bones: THREE.Bone[]; boneInverses: THREE.Matrix4[] }) {
97+
return skeleton.bones.every((bone, index) => {
98+
const candidateIndex = candidate.bones.indexOf(bone);
99+
if (candidateIndex !== -1) {
100+
return matrixEquals(skeleton.boneInverses[index], candidate.boneInverses[candidateIndex]);
101+
}
102+
return true;
103+
});
104+
}
105+
106+
// https://github.com/mrdoob/three.js/blob/r170/test/unit/src/math/Matrix4.tests.js#L12
107+
function matrixEquals(a: THREE.Matrix4, b: THREE.Matrix4, tolerance?: number) {
108+
tolerance = tolerance || 0.0001;
109+
if (a.elements.length != b.elements.length) {
110+
return false;
111+
}
112+
113+
for (let i = 0, il = a.elements.length; i < il; i++) {
114+
const delta = Math.abs(a.elements[i] - b.elements[i]);
115+
if (delta > tolerance) {
116+
return false;
117+
}
118+
}
119+
120+
return true;
121+
}

packages/three-vrm/src/VRMUtils/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { combineSkeletons } from './combineSkeletons';
12
import { deepDispose } from './deepDispose';
23
import { removeUnnecessaryJoints } from './removeUnnecessaryJoints';
34
import { removeUnnecessaryVertices } from './removeUnnecessaryVertices';
@@ -8,6 +9,7 @@ export class VRMUtils {
89
// this class is not meant to be instantiated
910
}
1011

12+
public static combineSkeletons = combineSkeletons;
1113
public static deepDispose = deepDispose;
1214
public static removeUnnecessaryJoints = removeUnnecessaryJoints;
1315
public static removeUnnecessaryVertices = removeUnnecessaryVertices;

0 commit comments

Comments
 (0)