|
| 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 | +} |
0 commit comments