@@ -9,98 +9,201 @@ import * as THREE from 'three';
9
9
* @param root Root object that will be traversed
10
10
*/
11
11
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 > {
12
87
const skinnedMeshes = new Set < THREE . SkinnedMesh > ( ) ;
13
- const geometryToSkinnedMesh = new Map < THREE . BufferGeometry , THREE . SkinnedMesh > ( ) ;
14
88
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 ) {
18
91
return ;
19
92
}
20
93
21
94
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 ) ;
33
96
} ) ;
34
97
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
+ }
39
100
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
+ }
45
121
}
122
+ }
46
123
47
- // Add skinned mesh to the new skeleton
48
- newSkeleton . meshes . push ( skinnedMesh ) ;
124
+ return usedIndices ;
125
+ }
49
126
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 > ( ) ;
52
138
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 ;
57
140
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 ) ;
65
144
66
- const index = attribute . getComponent ( i , j ) ;
145
+ if ( ! usedIndicesSet ) {
146
+ throw new Error ( 'Unreachable. attributeUsedIndexSetMap does not know the skin index attribute' ) ;
147
+ }
67
148
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
+ }
74
152
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 ;
76
172
}
77
173
}
174
+ }
78
175
79
- attribute . needsUpdate = true ;
80
- } ) ;
176
+ return true ;
177
+ }
81
178
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 ) ;
86
188
}
87
- }
88
189
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 ) ;
101
203
}
102
- return true ;
103
- } ) ;
204
+ }
205
+
206
+ attribute . needsUpdate = true ;
104
207
}
105
208
106
209
// https://github.com/mrdoob/three.js/blob/r170/test/unit/src/math/Matrix4.tests.js#L12
0 commit comments