vendor: 8,618 bytes, lines 1-311
1import { 2 AnimationClip, 3 AnimationMixer, 4 Matrix4, 5 Quaternion, 6 QuaternionKeyframeTrack, 7 SkeletonHelper, 8 Vector3, 9 VectorKeyframeTrack, 10} from './three.module.js'; 11 12/** 13 * @module SkeletonUtils 14 * @three_import import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js'; 15 */ 16 17function getBoneName(bone, options) { 18 if (options.getBoneName !== undefined) { 19 return options.getBoneName(bone); 20 } 21 22 return options.names[bone.name]; 23} 24 25/** 26 * Retargets the skeleton from the given source to the target. 27 * 28 * Both `target` and `source` can be a 3D object with a skeleton property (e.g. a skinned mesh) 29 * or a {@link Skeleton} directly. 30 * 31 * @param {Object3D|Skeleton} target - The target object. 32 * @param {Object3D|Skeleton} source - The source object. 33 * @param {module:SkeletonUtils~RetargetOptions} options - The options. 34 */ 35function retarget(target, source, options = {}) { 36 const quat = new Quaternion(), 37 scale = new Vector3(), 38 relativeMatrix = new Matrix4(), 39 globalMatrix = new Matrix4(); 40 41 options.preserveBoneMatrix = 42 options.preserveBoneMatrix !== undefined ? options.preserveBoneMatrix : true; 43 options.preserveBonePositions = 44 options.preserveBonePositions !== undefined ? options.preserveBonePositions : true; 45 options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false; 46 options.hip = options.hip !== undefined ? options.hip : 'hip'; 47 options.hipInfluence = 48 options.hipInfluence !== undefined ? options.hipInfluence : new Vector3(1, 1, 1); 49 options.scale = options.scale !== undefined ? options.scale : 1; 50 options.names = options.names || {}; 51 52 const sourceBones = source.isObject3D ? source.skeleton.bones : getBones(source), 53 bones = target.isObject3D ? target.skeleton.bones : getBones(target); 54 55 let bone, name, boneTo, bonesPosition; 56 57 // reset bones 58 59 if (target.isObject3D) { 60 target.skeleton.pose(); 61 } else { 62 options.useTargetMatrix = true; 63 options.preserveBoneMatrix = false; 64 } 65 66 if (options.preserveBonePositions) { 67 bonesPosition = []; 68 69 for (let i = 0; i < bones.length; i++) { 70 bonesPosition.push(bones[i].position.clone()); 71 } 72 } 73 74 if (options.preserveBoneMatrix) { 75 // reset matrix 76 77 target.updateMatrixWorld(); 78 79 target.matrixWorld.identity(); 80 81 // reset children matrix 82 83 for (let i = 0; i < target.children.length; ++i) { 84 target.children[i].updateMatrixWorld(true); 85 } 86 } 87 88 for (let i = 0; i < bones.length; ++i) { 89 bone = bones[i]; 90 name = getBoneName(bone, options); 91 92 boneTo = getBoneByName(name, sourceBones); 93 94 globalMatrix.copy(bone.matrixWorld); 95 96 if (boneTo) { 97 boneTo.updateMatrixWorld(); 98 99 if (options.useTargetMatrix) { 100 relativeMatrix.copy(boneTo.matrixWorld); 101 } else { 102 relativeMatrix.copy(target.matrixWorld).invert(); 103 relativeMatrix.multiply(boneTo.matrixWorld); 104 } 105 106 // ignore scale to extract rotation 107 108 scale.setFromMatrixScale(relativeMatrix); 109 relativeMatrix.scale(scale.set(1 / scale.x, 1 / scale.y, 1 / scale.z)); 110 111 // apply to global matrix 112 113 globalMatrix.makeRotationFromQuaternion(quat.setFromRotationMatrix(relativeMatrix)); 114 115 if (target.isObject3D) { 116 if (options.localOffsets) { 117 if (options.localOffsets[bone.name]) { 118 globalMatrix.multiply(options.localOffsets[bone.name]); 119 } 120 } 121 } 122 123 globalMatrix.copyPosition(relativeMatrix); 124 } 125 126 if (name === options.hip) { 127 globalMatrix.elements[12] *= options.scale * options.hipInfluence.x; 128 globalMatrix.elements[13] *= options.scale * options.hipInfluence.y; 129 globalMatrix.elements[14] *= options.scale * options.hipInfluence.z; 130 131 if (options.hipPosition !== undefined) { 132 globalMatrix.elements[12] += options.hipPosition.x * options.scale; 133 globalMatrix.elements[13] += options.hipPosition.y * options.scale; 134 globalMatrix.elements[14] += options.hipPosition.z * options.scale; 135 } 136 } 137 138 if (bone.parent) { 139 bone.matrix.copy(bone.parent.matrixWorld).invert(); 140 bone.matrix.multiply(globalMatrix); 141 } else { 142 bone.matrix.copy(globalMatrix); 143 } 144 145 bone.matrix.decompose(bone.position, bone.quaternion, bone.scale); 146 147 bone.updateMatrixWorld(); 148 } 149 150 if (options.preserveBonePositions) { 151 for (let i = 0; i < bones.length; ++i) { 152 bone = bones[i]; 153 name = getBoneName(bone, options) || bone.name; 154 155 if (name !== options.hip) { 156 bone.position.copy(bonesPosition[i]); 157 } 158 } 159 } 160 161 if (options.preserveBoneMatrix) { 162 // restore matrix 163 164 target.updateMatrixWorld(true); 165 } 166} 167 168/** 169 * Retargets the animation clip of the source to the target 3D object. 170 * 171 * The `source` can be a 3D object with a skeleton property (e.g. a skinned mesh) 172 * or a {@link Skeleton} directly. 173 * 174 * @param {Object3D} target - The target 3D object. Must have a `skeleton` property. 175 * @param {Object3D|Skeleton} source - The source object. 176 * @param {AnimationClip} clip - The animation clip. 177 * @param {module:SkeletonUtils~RetargetOptions} options - The options. 178 * @return {AnimationClip} The retargeted animation clip. 179 */ 180function retargetClip(target, source, clip, options = {}) { 181 options.useFirstFramePosition = 182 options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false; 183 184 // Calculate the fps from the source clip based on the track with the most frames, unless fps is already provided. 185 options.fps = 186 options.fps !== undefined 187 ? options.fps 188 : Math.max(...clip.tracks.map((track) => track.times.length)) / clip.duration; 189 options.names = options.names || []; 190 191 if (!source.isObject3D) { 192 source = getHelperFromSkeleton(source); 193 } 194 195 const numFrames = Math.round(clip.duration * (options.fps / 1000) * 1000), 196 delta = clip.duration / (numFrames - 1), 197 convertedTracks = [], 198 mixer = new AnimationMixer(source), 199 bones = getBones(target.skeleton), 200 boneDatas = []; 201 202 let positionOffset, bone, boneTo, boneData, name; 203 204 mixer.clipAction(clip).play(); 205 206 // trim 207 208 let start = 0, 209 end = numFrames; 210 211 if (options.trim !== undefined) { 212 start = Math.round(options.trim[0] * options.fps); 213 end = Math.min(Math.round(options.trim[1] * options.fps), numFrames) - start; 214 215 mixer.update(options.trim[0]); 216 } else { 217 mixer.update(0); 218 } 219 220 source.updateMatrixWorld(); 221 222 // 223 224 for (let frame = 0; frame < end; ++frame) { 225 const time = frame * delta; 226 227 retarget(target, source, options); 228 229 for (let j = 0; j < bones.length; ++j) { 230 bone = bones[j]; 231 name = getBoneName(bone, options) || bone.name; 232 boneTo = getBoneByName(name, source.skeleton); 233 234 if (boneTo) { 235 boneData = boneDatas[j] = boneDatas[j] || { bone: bone }; 236 237 if (options.hip === name) { 238 if (!boneData.pos) { 239 boneData.pos = { 240 times: new Float32Array(end), 241 values: new Float32Array(end * 3), 242 }; 243 } 244 245 if (options.useFirstFramePosition) { 246 if (frame === 0) { 247 positionOffset = bone.position.clone(); 248 } 249 250 bone.position.sub(positionOffset); 251 } 252 253 boneData.pos.times[frame] = time; 254 255 bone.position.toArray(boneData.pos.values, frame * 3); 256 } 257 258 if (!boneData.quat) { 259 boneData.quat = { 260 times: new Float32Array(end), 261 values: new Float32Array(end * 4), 262 }; 263 } 264 265 boneData.quat.times[frame] = time; 266 267 bone.quaternion.toArray(boneData.quat.values, frame * 4); 268 } 269 } 270 271 if (frame === end - 2) { 272 // last mixer update before final loop iteration 273 // make sure we do not go over or equal to clip duration 274 mixer.update(delta - 0.0000001); 275 } else { 276 mixer.update(delta); 277 } 278 279 source.updateMatrixWorld(); 280 } 281 282 for (let i = 0; i < boneDatas.length; ++i) { 283 boneData = boneDatas[i]; 284 285 if (boneData) { 286 if (boneData.pos) { 287 convertedTracks.push( 288 new VectorKeyframeTrack( 289 '.bones[' + boneData.bone.name + '].position', 290 boneData.pos.times, 291 boneData.pos.values, 292 ), 293 ); 294 } 295 296 convertedTracks.push( 297 new QuaternionKeyframeTrack( 298 '.bones[' + boneData.bone.name + '].quaternion', 299 boneData.quat.times, 300 boneData.quat.values, 301 ), 302 ); 303 } 304 } 305 306 mixer.uncacheAction(clip); 307 308 return new AnimationClip(clip.name, -1, convertedTracks); 309} 310 311/**
312 * Clones the given 3D object and its descendants, ensuring that any `SkinnedMesh` instances are 313 * correctly associated with their bones. Bones are also cloned, and must be descendants of the 314 * object passed to this method. Other data, like geometries and materials, are reused by reference. 315 * 316 * @param {Object3D} source - The 3D object to clone. 317 * @return {Object3D} The cloned 3D object. 318 */ 319function clone(source) { 320 const sourceLookup = new Map(); 321 const cloneLookup = new Map(); 322 323 const clone = source.clone(); 324 325 parallelTraverse(source, clone, function (sourceNode, clonedNode) { 326 sourceLookup.set(clonedNode, sourceNode); 327 cloneLookup.set(sourceNode, clonedNode); 328 }); 329 330 clone.traverse(function (node) { 331 if (!node.isSkinnedMesh) return; 332 333 const clonedMesh = node; 334 const sourceMesh = sourceLookup.get(node); 335 const sourceBones = sourceMesh.skeleton.bones; 336 337 clonedMesh.skeleton = sourceMesh.skeleton.clone(); 338 clonedMesh.bindMatrix.copy(sourceMesh.bindMatrix); 339 340 clonedMesh.skeleton.bones = sourceBones.map(function (bone) { 341 return cloneLookup.get(bone); 342 }); 343 344 clonedMesh.bind(clonedMesh.skeleton, clonedMesh.bindMatrix); 345 }); 346 347 return clone; 348} 349 350// internal helper 351 352function getBoneByName(name, skeleton) { 353 for (let i = 0, bones = getBones(skeleton); i < bones.length; i++) { 354 if (name === bones[i].name) return bones[i]; 355 } 356} 357 358function getBones(skeleton) { 359 return Array.isArray(skeleton) ? skeleton : skeleton.bones; 360} 361 362function getHelperFromSkeleton(skeleton) { 363 const source = new SkeletonHelper(skeleton.bones[0]); 364 source.skeleton = skeleton; 365 366 return source; 367} 368 369function parallelTraverse(a, b, callback) { 370 callback(a, b); 371 372 for (let i = 0; i < a.children.length; i++) { 373 parallelTraverse(a.children[i], b.children[i], callback); 374 } 375} 376 377/** 378 * Retarget options of `SkeletonUtils`. 379 * 380 * @typedef {Object} module:SkeletonUtils~RetargetOptions 381 * @property {boolean} [useFirstFramePosition=false] - Whether to use the position of the first frame or not. 382 * @property {number} [fps] - The FPS of the clip. 383 * @property {Object<string,string>} [names] - A dictionary for mapping target to source bone names. 384 * @property {function(string):string} [getBoneName] - A function for mapping bone names. Alternative to `names`. 385 * @property {Array<number>} [trim] - Whether to trim the clip or not. If set the array should hold two values for the start and end. 386 * @property {boolean} [preserveBoneMatrix=true] - Whether to preserve bone matrices or not. 387 * @property {boolean} [preserveBonePositions=true] - Whether to preserve bone positions or not. 388 * @property {boolean} [useTargetMatrix=false] - Whether to use the target matrix or not. 389 * @property {string} [hip='hip'] - The name of the source's hip bone. 390 * @property {Vector3} [hipInfluence=(1,1,1)] - The hip influence. 391 * @property {number} [scale=1] - The scale. 392 * @property {Object<string,Matrix4>} [localOffsets] - Per-bone local offset matrices, keyed by bone name. 393 * @property {Vector3} [hipPosition] - An additional position offset applied to the hip bone. 394 **/ 395 396export { retarget, retargetClip, clone };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.