Skip to content

Commit 81b6991

Browse files
authored
Merge pull request #1557 from pixiv/fix-combine-meshes
fix: fix removeUnnecessaryJoints and combineSkeletons
2 parents d21d2b4 + 78e3695 commit 81b6991

File tree

2 files changed

+204
-93
lines changed

2 files changed

+204
-93
lines changed

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

Lines changed: 171 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,98 +9,201 @@ import * as THREE from 'three';
99
* @param root Root object that will be traversed
1010
*/
1111
export function combineSkeletons(root: THREE.Object3D): void {
12+
const skinnedMeshes = collectSkinnedMeshes(root);
13+
14+
// List all used skin indices for each skin index attribute
15+
const attributeUsedIndexSetMap = new Map<THREE.BufferAttribute | THREE.InterleavedBufferAttribute, Set<number>>();
16+
for (const mesh of skinnedMeshes) {
17+
const geometry = mesh.geometry;
18+
const skinIndexAttr = geometry.getAttribute('skinIndex');
19+
const skinWeightAttr = geometry.getAttribute('skinWeight');
20+
const usedIndicesSet = listUsedIndices(skinIndexAttr, skinWeightAttr);
21+
attributeUsedIndexSetMap.set(skinIndexAttr, usedIndicesSet);
22+
}
23+
24+
// List all bones and boneInverses for each meshes
25+
const meshBoneInverseMapMap = new Map<THREE.SkinnedMesh, Map<THREE.Bone, THREE.Matrix4>>();
26+
for (const mesh of skinnedMeshes) {
27+
const boneInverseMap = listUsedBones(mesh, attributeUsedIndexSetMap);
28+
meshBoneInverseMapMap.set(mesh, boneInverseMap);
29+
}
30+
31+
// Group meshes by bone sets
32+
const groups: { boneInverseMap: Map<THREE.Bone, THREE.Matrix4>; meshes: Set<THREE.SkinnedMesh> }[] = [];
33+
for (const [mesh, boneInverseMap] of meshBoneInverseMapMap) {
34+
let foundMergeableGroup = false;
35+
for (const candidate of groups) {
36+
// check if the candidate group is mergeable
37+
const isMergeable = boneInverseMapIsMergeable(boneInverseMap, candidate.boneInverseMap);
38+
39+
// if we found a mergeable group, add the mesh to the group
40+
if (isMergeable) {
41+
foundMergeableGroup = true;
42+
candidate.meshes.add(mesh);
43+
44+
// add lacking bones to the group
45+
for (const [bone, boneInverse] of boneInverseMap) {
46+
candidate.boneInverseMap.set(bone, boneInverse);
47+
}
48+
49+
break;
50+
}
51+
}
52+
53+
// if we couldn't find a mergeable group, create a new group
54+
if (!foundMergeableGroup) {
55+
groups.push({ boneInverseMap, meshes: new Set([mesh]) });
56+
}
57+
}
58+
59+
// prepare new skeletons for each group, and bind them to the meshes
60+
for (const { boneInverseMap, meshes } of groups) {
61+
// create a new skeleton
62+
const newBones = Array.from(boneInverseMap.keys());
63+
const newBoneInverses = Array.from(boneInverseMap.values());
64+
const newSkeleton = new THREE.Skeleton(newBones, newBoneInverses);
65+
66+
const attributeProcessedSet = new Set<THREE.BufferAttribute | THREE.InterleavedBufferAttribute>();
67+
68+
for (const mesh of meshes) {
69+
const attribute = mesh.geometry.getAttribute('skinIndex');
70+
71+
if (!attributeProcessedSet.has(attribute)) {
72+
// remap skin index attribute
73+
remapSkinIndexAttribute(attribute, mesh.skeleton.bones, newBones);
74+
attributeProcessedSet.add(attribute);
75+
}
76+
77+
// bind the new skeleton to the mesh
78+
mesh.bind(newSkeleton, new THREE.Matrix4());
79+
}
80+
}
81+
}
82+
83+
/**
84+
* Traverse an entire tree and collect skinned meshes.
85+
*/
86+
function collectSkinnedMeshes(scene: THREE.Object3D): Set<THREE.SkinnedMesh> {
1287
const skinnedMeshes = new Set<THREE.SkinnedMesh>();
13-
const geometryToSkinnedMesh = new Map<THREE.BufferGeometry, THREE.SkinnedMesh>();
1488

15-
// Traverse entire tree and collect skinned meshes
16-
root.traverse((obj) => {
17-
if (obj.type !== 'SkinnedMesh') {
89+
scene.traverse((obj) => {
90+
if (!(obj as any).isSkinnedMesh) {
1891
return;
1992
}
2093

2194
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-
}
95+
skinnedMeshes.add(skinnedMesh);
3396
});
3497

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;
98+
return skinnedMeshes;
99+
}
39100

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);
101+
/**
102+
* List all skin indices used by the given geometry.
103+
* If the skin weight is 0, the index won't be considered as used.
104+
* @param skinIndexAttr The skin index attribute to list used indices
105+
* @param skinWeightAttr The skin weight attribute corresponding to the skin index attribute
106+
*/
107+
function listUsedIndices(
108+
skinIndexAttr: THREE.BufferAttribute | THREE.InterleavedBufferAttribute,
109+
skinWeightAttr: THREE.BufferAttribute | THREE.InterleavedBufferAttribute,
110+
): Set<number> {
111+
const usedIndices = new Set<number>();
112+
113+
for (let i = 0; i < skinIndexAttr.count; i++) {
114+
for (let j = 0; j < skinIndexAttr.itemSize; j++) {
115+
const index = skinIndexAttr.getComponent(i, j);
116+
const weight = skinWeightAttr.getComponent(i, j);
117+
118+
if (weight !== 0) {
119+
usedIndices.add(index);
120+
}
45121
}
122+
}
46123

47-
// Add skinned mesh to the new skeleton
48-
newSkeleton.meshes.push(skinnedMesh);
124+
return usedIndices;
125+
}
49126

50-
// Determine bone index mapping from skeleton -> newSkeleton
51-
const boneIndexMap: number[] = skeleton.bones.map((bone) => newSkeleton.bones.indexOf(bone));
127+
/**
128+
* List all bones used by the given skinned mesh.
129+
* @param mesh The skinned mesh to list used bones
130+
* @param attributeUsedIndexSetMap A map from skin index attribute to the set of used skin indices
131+
* @returns A map from used bone to the corresponding bone inverse matrix
132+
*/
133+
function listUsedBones(
134+
mesh: THREE.SkinnedMesh,
135+
attributeUsedIndexSetMap: Map<THREE.BufferAttribute | THREE.InterleavedBufferAttribute, Set<number>>,
136+
): Map<THREE.Bone, THREE.Matrix4> {
137+
const boneInverseMap = new Map<THREE.Bone, THREE.Matrix4>();
52138

53-
// Update skinIndex attribute
54-
const geometry = skinnedMesh.geometry;
55-
const attribute = geometry.getAttribute('skinIndex');
56-
const weightAttribute = geometry.getAttribute('skinWeight');
139+
const skeleton = mesh.skeleton;
57140

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-
}
141+
const geometry = mesh.geometry;
142+
const skinIndexAttr = geometry.getAttribute('skinIndex');
143+
const usedIndicesSet = attributeUsedIndexSetMap.get(skinIndexAttr);
65144

66-
const index = attribute.getComponent(i, j);
145+
if (!usedIndicesSet) {
146+
throw new Error('Unreachable. attributeUsedIndexSetMap does not know the skin index attribute');
147+
}
67148

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-
}
149+
for (const index of usedIndicesSet) {
150+
boneInverseMap.set(skeleton.bones[index], skeleton.boneInverses[index]);
151+
}
74152

75-
attribute.setComponent(i, j, boneIndexMap[index]);
153+
return boneInverseMap;
154+
}
155+
156+
/**
157+
* Check if the given bone inverse map is mergeable to the candidate bone inverse map.
158+
* @param toCheck The bone inverse map to check
159+
* @param candidate The candidate bone inverse map
160+
* @returns True if the bone inverse map is mergeable to the candidate bone inverse map
161+
*/
162+
function boneInverseMapIsMergeable(
163+
toCheck: Map<THREE.Bone, THREE.Matrix4>,
164+
candidate: Map<THREE.Bone, THREE.Matrix4>,
165+
): boolean {
166+
for (const [bone, boneInverse] of toCheck.entries()) {
167+
// if the bone is in the candidate group and the boneInverse is different, it's not mergeable
168+
const candidateBoneInverse = candidate.get(bone);
169+
if (candidateBoneInverse != null) {
170+
if (!matrixEquals(boneInverse, candidateBoneInverse)) {
171+
return false;
76172
}
77173
}
174+
}
78175

79-
attribute.needsUpdate = true;
80-
});
176+
return true;
177+
}
81178

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()));
179+
function remapSkinIndexAttribute(
180+
attribute: THREE.BufferAttribute | THREE.InterleavedBufferAttribute,
181+
oldBones: THREE.Bone[],
182+
newBones: THREE.Bone[],
183+
): void {
184+
// a map from bone to old index
185+
const boneOldIndexMap = new Map<THREE.Bone, number>();
186+
for (const bone of oldBones) {
187+
boneOldIndexMap.set(bone, boneOldIndexMap.size);
86188
}
87-
}
88189

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]);
190+
// a map from old skin index to new skin index
191+
const oldToNew = new Map<number, number>();
192+
for (const [i, bone] of newBones.entries()) {
193+
const oldIndex = boneOldIndexMap.get(bone)!;
194+
oldToNew.set(oldIndex, i);
195+
}
196+
197+
// replace the skin index attribute with new indices
198+
for (let i = 0; i < attribute.count; i++) {
199+
for (let j = 0; j < attribute.itemSize; j++) {
200+
const oldIndex = attribute.getComponent(i, j);
201+
const newIndex = oldToNew.get(oldIndex)!;
202+
attribute.setComponent(i, j, newIndex);
101203
}
102-
return true;
103-
});
204+
}
205+
206+
attribute.needsUpdate = true;
104207
}
105208

106209
// https://github.com/mrdoob/three.js/blob/r170/test/unit/src/math/Matrix4.tests.js#L12

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

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,64 +46,72 @@ export function removeUnnecessaryJoints(
4646
skinnedMeshes.push(obj as THREE.SkinnedMesh);
4747
});
4848

49-
// A map from meshes to bones and boneInverses
49+
// A map from meshes to new-to-old bone index map
5050
// some meshes might share a same skinIndex attribute, and this map also prevents to convert the attribute twice
51-
const bonesList: Map<
52-
THREE.SkinnedMesh,
53-
{
54-
bones: THREE.Bone[];
55-
boneInverses: THREE.Matrix4[];
56-
}
51+
const attributeToBoneIndexMapMap: Map<
52+
THREE.BufferAttribute | THREE.InterleavedBufferAttribute,
53+
Map<number, number>
5754
> = new Map();
5855

5956
// A maximum number of bones
6057
let maxBones = 0;
6158

62-
// Iterate over all skinned meshes and collect bones and boneInverses
59+
// Iterate over all skinned meshes and remap bones for each skin index attribute
6360
for (const mesh of skinnedMeshes) {
6461
const geometry = mesh.geometry;
6562
const attribute = geometry.getAttribute('skinIndex');
6663

67-
const bones: THREE.Bone[] = []; // new list of bone
68-
const boneInverses: THREE.Matrix4[] = []; // new list of boneInverse
69-
const boneIndexMap: { [index: number]: number } = {}; // map of old bone index vs. new bone index
64+
if (attributeToBoneIndexMapMap.has(attribute)) {
65+
continue;
66+
}
67+
68+
const oldToNew = new Map<number, number>(); // map of old bone index vs. new bone index
69+
const newToOld = new Map<number, number>(); // map of new bone index vs. old bone index
7070

7171
// create a new bone map
7272
for (let i = 0; i < attribute.count; i++) {
7373
for (let j = 0; j < attribute.itemSize; j++) {
74-
const index = attribute.getComponent(i, j);
74+
const oldIndex = attribute.getComponent(i, j);
75+
let newIndex = oldToNew.get(oldIndex);
7576

7677
// new skinIndex buffer
77-
if (boneIndexMap[index] == null) {
78-
boneIndexMap[index] = bones.length;
79-
bones.push(mesh.skeleton.bones[index]);
80-
boneInverses.push(mesh.skeleton.boneInverses[index]);
78+
if (newIndex == null) {
79+
newIndex = oldToNew.size;
80+
oldToNew.set(oldIndex, newIndex);
81+
newToOld.set(newIndex, oldIndex);
8182
}
8283

83-
attribute.setComponent(i, j, boneIndexMap[index]);
84+
attribute.setComponent(i, j, newIndex);
8485
}
8586
}
8687

8788
// replace with new indices
8889
attribute.needsUpdate = true;
8990

9091
// update boneList
91-
bonesList.set(mesh, { bones, boneInverses });
92+
attributeToBoneIndexMapMap.set(attribute, newToOld);
9293

9394
// update max bones count
94-
maxBones = Math.max(maxBones, bones.length);
95+
maxBones = Math.max(maxBones, oldToNew.size);
9596
}
9697

9798
// Let's actually set the skeletons
9899
for (const mesh of skinnedMeshes) {
99-
const { bones, boneInverses } = bonesList.get(mesh)!;
100+
const geometry = mesh.geometry;
101+
const attribute = geometry.getAttribute('skinIndex');
102+
const newToOld = attributeToBoneIndexMapMap.get(attribute)!;
103+
104+
const bones: THREE.Bone[] = [];
105+
const boneInverses: THREE.Matrix4[] = [];
100106

101107
// if `experimentalSameBoneCounts` is `true`, compensate skeletons with dummy bones to keep the bone count same between skeletons
102-
if (experimentalSameBoneCounts) {
103-
for (let i = bones.length; i < maxBones; i++) {
104-
bones[i] = bones[0];
105-
boneInverses[i] = boneInverses[0];
106-
}
108+
const nBones = experimentalSameBoneCounts ? maxBones : newToOld.size;
109+
110+
for (let newIndex = 0; newIndex < nBones; newIndex++) {
111+
const oldIndex = newToOld.get(newIndex) ?? 0;
112+
113+
bones.push(mesh.skeleton.bones[oldIndex]);
114+
boneInverses.push(mesh.skeleton.boneInverses[oldIndex]);
107115
}
108116

109117
const skeleton = new THREE.Skeleton(bones, boneInverses);

0 commit comments

Comments
 (0)