PageSourceSearch

https://moskus.ai/studio-20260913/vendor/SkeletonUtils.js

js moskus.ai collected 2026-10-05 12:49:54 UTC 11,765 bytes, 396 lines download raw bytes

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.