starry / backend /libs /three /animation /AnimationClip.js
k-l-lambda's picture
feat: add Python ML services (CPU mode) with model download
2b7aae2
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 };