import { AnimationUtils } from './AnimationUtils.js'; import { KeyframeTrack } from './KeyframeTrack.js'; import { BooleanKeyframeTrack } from './tracks/BooleanKeyframeTrack.js'; import { ColorKeyframeTrack } from './tracks/ColorKeyframeTrack.js'; import { NumberKeyframeTrack } from './tracks/NumberKeyframeTrack.js'; import { QuaternionKeyframeTrack } from './tracks/QuaternionKeyframeTrack.js'; import { StringKeyframeTrack } from './tracks/StringKeyframeTrack.js'; import { VectorKeyframeTrack } from './tracks/VectorKeyframeTrack.js'; import * as MathUtils from '../math/MathUtils.js'; import { NormalAnimationBlendMode } from '../constants.js'; class AnimationClip { constructor(name, duration = -1, tracks, blendMode = NormalAnimationBlendMode) { this.name = name; this.tracks = tracks; this.duration = duration; this.blendMode = blendMode; this.uuid = MathUtils.generateUUID(); // this means it should figure out its duration by scanning the tracks if (this.duration < 0) { this.resetDuration(); } } static parse(json) { const tracks = [], jsonTracks = json.tracks, frameTime = 1.0 / (json.fps || 1.0); for (let i = 0, n = jsonTracks.length; i !== n; ++i) { tracks.push(parseKeyframeTrack(jsonTracks[i]).scale(frameTime)); } const clip = new this(json.name, json.duration, tracks, json.blendMode); clip.uuid = json.uuid; return clip; } static toJSON(clip) { const tracks = [], clipTracks = clip.tracks; const json = { name: clip.name, duration: clip.duration, tracks: tracks, uuid: clip.uuid, blendMode: clip.blendMode, }; for (let i = 0, n = clipTracks.length; i !== n; ++i) { tracks.push(KeyframeTrack.toJSON(clipTracks[i])); } return json; } static CreateFromMorphTargetSequence(name, morphTargetSequence, fps, noLoop) { const numMorphTargets = morphTargetSequence.length; const tracks = []; for (let i = 0; i < numMorphTargets; i++) { let times = []; let values = []; times.push((i + numMorphTargets - 1) % numMorphTargets, i, (i + 1) % numMorphTargets); values.push(0, 1, 0); const order = AnimationUtils.getKeyframeOrder(times); times = AnimationUtils.sortedArray(times, 1, order); values = AnimationUtils.sortedArray(values, 1, order); // if there is a key at the first frame, duplicate it as the // last frame as well for perfect loop. if (!noLoop && times[0] === 0) { times.push(numMorphTargets); values.push(values[0]); } tracks.push(new NumberKeyframeTrack('.morphTargetInfluences[' + morphTargetSequence[i].name + ']', times, values).scale(1.0 / fps)); } return new this(name, -1, tracks); } static findByName(objectOrClipArray, name) { let clipArray = objectOrClipArray; if (!Array.isArray(objectOrClipArray)) { const o = objectOrClipArray; clipArray = (o.geometry && o.geometry.animations) || o.animations; } for (let i = 0; i < clipArray.length; i++) { if (clipArray[i].name === name) { return clipArray[i]; } } return null; } static CreateClipsFromMorphTargetSequences(morphTargets, fps, noLoop) { const animationToMorphTargets = {}; // tested with https://regex101.com/ on trick sequences // such flamingo_flyA_003, flamingo_run1_003, crdeath0059 const pattern = /^([\w-]*?)([\d]+)$/; // sort morph target names into animation groups based // patterns like Walk_001, Walk_002, Run_001, Run_002 for (let i = 0, il = morphTargets.length; i < il; i++) { const morphTarget = morphTargets[i]; const parts = morphTarget.name.match(pattern); if (parts && parts.length > 1) { const name = parts[1]; let animationMorphTargets = animationToMorphTargets[name]; if (!animationMorphTargets) { animationToMorphTargets[name] = animationMorphTargets = []; } animationMorphTargets.push(morphTarget); } } const clips = []; for (const name in animationToMorphTargets) { clips.push(this.CreateFromMorphTargetSequence(name, animationToMorphTargets[name], fps, noLoop)); } return clips; } // parse the animation.hierarchy format static parseAnimation(animation, bones) { if (!animation) { console.error('THREE.AnimationClip: No animation in JSONLoader data.'); return null; } const addNonemptyTrack = function (trackType, trackName, animationKeys, propertyName, destTracks) { // only return track if there are actually keys. if (animationKeys.length !== 0) { const times = []; const values = []; AnimationUtils.flattenJSON(animationKeys, times, values, propertyName); // empty keys are filtered out, so check again if (times.length !== 0) { destTracks.push(new trackType(trackName, times, values)); } } }; const tracks = []; const clipName = animation.name || 'default'; const fps = animation.fps || 30; const blendMode = animation.blendMode; // automatic length determination in AnimationClip. let duration = animation.length || -1; const hierarchyTracks = animation.hierarchy || []; for (let h = 0; h < hierarchyTracks.length; h++) { const animationKeys = hierarchyTracks[h].keys; // skip empty tracks if (!animationKeys || animationKeys.length === 0) continue; // process morph targets if (animationKeys[0].morphTargets) { // figure out all morph targets used in this track const morphTargetNames = {}; let k; for (k = 0; k < animationKeys.length; k++) { if (animationKeys[k].morphTargets) { for (let m = 0; m < animationKeys[k].morphTargets.length; m++) { morphTargetNames[animationKeys[k].morphTargets[m]] = -1; } } } // create a track for each morph target with all zero // morphTargetInfluences except for the keys in which // the morphTarget is named. for (const morphTargetName in morphTargetNames) { const times = []; const values = []; for (let m = 0; m !== animationKeys[k].morphTargets.length; ++m) { const animationKey = animationKeys[k]; times.push(animationKey.time); values.push(animationKey.morphTarget === morphTargetName ? 1 : 0); } tracks.push(new NumberKeyframeTrack('.morphTargetInfluence[' + morphTargetName + ']', times, values)); } duration = morphTargetNames.length * (fps || 1.0); } else { // ...assume skeletal animation const boneName = '.bones[' + bones[h].name + ']'; addNonemptyTrack(VectorKeyframeTrack, boneName + '.position', animationKeys, 'pos', tracks); addNonemptyTrack(QuaternionKeyframeTrack, boneName + '.quaternion', animationKeys, 'rot', tracks); addNonemptyTrack(VectorKeyframeTrack, boneName + '.scale', animationKeys, 'scl', tracks); } } if (tracks.length === 0) { return null; } const clip = new this(clipName, duration, tracks, blendMode); return clip; } resetDuration() { const tracks = this.tracks; let duration = 0; for (let i = 0, n = tracks.length; i !== n; ++i) { const track = this.tracks[i]; duration = Math.max(duration, track.times[track.times.length - 1]); } this.duration = duration; return this; } trim() { for (let i = 0; i < this.tracks.length; i++) { this.tracks[i].trim(0, this.duration); } return this; } validate() { let valid = true; for (let i = 0; i < this.tracks.length; i++) { valid = valid && this.tracks[i].validate(); } return valid; } optimize() { for (let i = 0; i < this.tracks.length; i++) { this.tracks[i].optimize(); } return this; } clone() { const tracks = []; for (let i = 0; i < this.tracks.length; i++) { tracks.push(this.tracks[i].clone()); } return new this.constructor(this.name, this.duration, tracks, this.blendMode); } toJSON() { return this.constructor.toJSON(this); } } function getTrackTypeForValueTypeName(typeName) { switch (typeName.toLowerCase()) { case 'scalar': case 'double': case 'float': case 'number': case 'integer': return NumberKeyframeTrack; case 'vector': case 'vector2': case 'vector3': case 'vector4': return VectorKeyframeTrack; case 'color': return ColorKeyframeTrack; case 'quaternion': return QuaternionKeyframeTrack; case 'bool': case 'boolean': return BooleanKeyframeTrack; case 'string': return StringKeyframeTrack; } throw new Error('THREE.KeyframeTrack: Unsupported typeName: ' + typeName); } function parseKeyframeTrack(json) { if (json.type === undefined) { throw new Error('THREE.KeyframeTrack: track type undefined, can not parse'); } const trackType = getTrackTypeForValueTypeName(json.type); if (json.times === undefined) { const times = [], values = []; AnimationUtils.flattenJSON(json.keys, times, values, 'value'); json.times = times; json.values = values; } // derived classes can define a static parse method if (trackType.parse !== undefined) { return trackType.parse(json); } else { // by default, we assume a constructor compatible with the base return new trackType(json.name, json.times, json.values, json.interpolation); } } export { AnimationClip };