507 lines
12 KiB
JavaScript
507 lines
12 KiB
JavaScript
import { Quaternion } from '../math/Quaternion.js';
|
|
import { AdditiveAnimationBlendMode } from '../constants.js';
|
|
|
|
/**
|
|
* Converts an array to a specific type.
|
|
*
|
|
* @param {TypedArray|Array} array - The array to convert.
|
|
* @param {TypedArray.constructor} type - The constructor of a typed array that defines the new type.
|
|
* @return {TypedArray} The converted array.
|
|
*/
|
|
function convertArray( array, type ) {
|
|
|
|
if ( ! array || array.constructor === type ) return array;
|
|
|
|
if ( typeof type.BYTES_PER_ELEMENT === 'number' ) {
|
|
|
|
return new type( array ); // create typed array
|
|
|
|
}
|
|
|
|
return Array.prototype.slice.call( array ); // create Array
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if the given object is a typed array.
|
|
*
|
|
* @param {any} object - The object to check.
|
|
* @return {boolean} Whether the given object is a typed array.
|
|
*/
|
|
function isTypedArray( object ) {
|
|
|
|
return ArrayBuffer.isView( object ) && ! ( object instanceof DataView );
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns an array by which times and values can be sorted.
|
|
*
|
|
* @param {Array<number>} times - The keyframe time values.
|
|
* @return {Array<number>} The array.
|
|
*/
|
|
function getKeyframeOrder( times ) {
|
|
|
|
function compareTime( i, j ) {
|
|
|
|
return times[ i ] - times[ j ];
|
|
|
|
}
|
|
|
|
const n = times.length;
|
|
const result = new Array( n );
|
|
for ( let i = 0; i !== n; ++ i ) result[ i ] = i;
|
|
|
|
result.sort( compareTime );
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Sorts the given array by the previously computed order via `getKeyframeOrder()`.
|
|
*
|
|
* @param {Array<number>} values - The values to sort.
|
|
* @param {number} stride - The stride.
|
|
* @param {Array<number>} order - The sort order.
|
|
* @return {Array<number>} The sorted values.
|
|
*/
|
|
function sortedArray( values, stride, order ) {
|
|
|
|
const nValues = values.length;
|
|
const result = new values.constructor( nValues );
|
|
|
|
for ( let i = 0, dstOffset = 0; dstOffset !== nValues; ++ i ) {
|
|
|
|
const srcOffset = order[ i ] * stride;
|
|
|
|
for ( let j = 0; j !== stride; ++ j ) {
|
|
|
|
result[ dstOffset ++ ] = values[ srcOffset + j ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Used for parsing AOS keyframe formats.
|
|
*
|
|
* @param {Array<number>} jsonKeys - A list of JSON keyframes.
|
|
* @param {Array<number>} times - This array will be filled with keyframe times by this function.
|
|
* @param {Array<number>} values - This array will be filled with keyframe values by this function.
|
|
* @param {string} valuePropertyName - The name of the property to use.
|
|
*/
|
|
function flattenJSON( jsonKeys, times, values, valuePropertyName ) {
|
|
|
|
let i = 1, key = jsonKeys[ 0 ];
|
|
|
|
while ( key !== undefined && key[ valuePropertyName ] === undefined ) {
|
|
|
|
key = jsonKeys[ i ++ ];
|
|
|
|
}
|
|
|
|
if ( key === undefined ) return; // no data
|
|
|
|
let value = key[ valuePropertyName ];
|
|
if ( value === undefined ) return; // no data
|
|
|
|
if ( Array.isArray( value ) ) {
|
|
|
|
do {
|
|
|
|
value = key[ valuePropertyName ];
|
|
|
|
if ( value !== undefined ) {
|
|
|
|
times.push( key.time );
|
|
values.push( ...value ); // push all elements
|
|
|
|
}
|
|
|
|
key = jsonKeys[ i ++ ];
|
|
|
|
} while ( key !== undefined );
|
|
|
|
} else if ( value.toArray !== undefined ) {
|
|
|
|
// ...assume THREE.Math-ish
|
|
|
|
do {
|
|
|
|
value = key[ valuePropertyName ];
|
|
|
|
if ( value !== undefined ) {
|
|
|
|
times.push( key.time );
|
|
value.toArray( values, values.length );
|
|
|
|
}
|
|
|
|
key = jsonKeys[ i ++ ];
|
|
|
|
} while ( key !== undefined );
|
|
|
|
} else {
|
|
|
|
// otherwise push as-is
|
|
|
|
do {
|
|
|
|
value = key[ valuePropertyName ];
|
|
|
|
if ( value !== undefined ) {
|
|
|
|
times.push( key.time );
|
|
values.push( value );
|
|
|
|
}
|
|
|
|
key = jsonKeys[ i ++ ];
|
|
|
|
} while ( key !== undefined );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Creates a new clip, containing only the segment of the original clip between the given frames.
|
|
*
|
|
* @param {AnimationClip} sourceClip - The values to sort.
|
|
* @param {string} name - The name of the clip.
|
|
* @param {number} startFrame - The start frame.
|
|
* @param {number} endFrame - The end frame.
|
|
* @param {number} [fps=30] - The FPS.
|
|
* @return {AnimationClip} The new sub clip.
|
|
*/
|
|
function subclip( sourceClip, name, startFrame, endFrame, fps = 30 ) {
|
|
|
|
const clip = sourceClip.clone();
|
|
|
|
clip.name = name;
|
|
|
|
const tracks = [];
|
|
|
|
for ( let i = 0; i < clip.tracks.length; ++ i ) {
|
|
|
|
const track = clip.tracks[ i ];
|
|
const valueSize = track.getValueSize();
|
|
|
|
const times = [];
|
|
const values = [];
|
|
|
|
for ( let j = 0; j < track.times.length; ++ j ) {
|
|
|
|
const frame = track.times[ j ] * fps;
|
|
|
|
if ( frame < startFrame || frame >= endFrame ) continue;
|
|
|
|
times.push( track.times[ j ] );
|
|
|
|
for ( let k = 0; k < valueSize; ++ k ) {
|
|
|
|
values.push( track.values[ j * valueSize + k ] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( times.length === 0 ) continue;
|
|
|
|
track.times = convertArray( times, track.times.constructor );
|
|
track.values = convertArray( values, track.values.constructor );
|
|
|
|
tracks.push( track );
|
|
|
|
}
|
|
|
|
clip.tracks = tracks;
|
|
|
|
// find minimum .times value across all tracks in the trimmed clip
|
|
|
|
let minStartTime = Infinity;
|
|
|
|
for ( let i = 0; i < clip.tracks.length; ++ i ) {
|
|
|
|
if ( minStartTime > clip.tracks[ i ].times[ 0 ] ) {
|
|
|
|
minStartTime = clip.tracks[ i ].times[ 0 ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// shift all tracks such that clip begins at t=0
|
|
|
|
for ( let i = 0; i < clip.tracks.length; ++ i ) {
|
|
|
|
clip.tracks[ i ].shift( - 1 * minStartTime );
|
|
|
|
}
|
|
|
|
clip.resetDuration();
|
|
|
|
return clip;
|
|
|
|
}
|
|
|
|
/**
|
|
* Converts the keyframes of the given animation clip to an additive format.
|
|
*
|
|
* @param {AnimationClip} targetClip - The clip to make additive.
|
|
* @param {number} [referenceFrame=0] - The reference frame.
|
|
* @param {AnimationClip} [referenceClip=targetClip] - The reference clip.
|
|
* @param {number} [fps=30] - The FPS.
|
|
* @return {AnimationClip} The updated clip which is now additive.
|
|
*/
|
|
function makeClipAdditive( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 ) {
|
|
|
|
if ( fps <= 0 ) fps = 30;
|
|
|
|
const numTracks = referenceClip.tracks.length;
|
|
const referenceTime = referenceFrame / fps;
|
|
|
|
// Make each track's values relative to the values at the reference frame
|
|
for ( let i = 0; i < numTracks; ++ i ) {
|
|
|
|
const referenceTrack = referenceClip.tracks[ i ];
|
|
const referenceTrackType = referenceTrack.ValueTypeName;
|
|
|
|
// Skip this track if it's non-numeric
|
|
if ( referenceTrackType === 'bool' || referenceTrackType === 'string' ) continue;
|
|
|
|
// Find the track in the target clip whose name and type matches the reference track
|
|
const targetTrack = targetClip.tracks.find( function ( track ) {
|
|
|
|
return track.name === referenceTrack.name
|
|
&& track.ValueTypeName === referenceTrackType;
|
|
|
|
} );
|
|
|
|
if ( targetTrack === undefined ) continue;
|
|
|
|
let referenceOffset = 0;
|
|
const referenceValueSize = referenceTrack.getValueSize();
|
|
|
|
if ( referenceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
|
|
|
|
referenceOffset = referenceValueSize / 3;
|
|
|
|
}
|
|
|
|
let targetOffset = 0;
|
|
const targetValueSize = targetTrack.getValueSize();
|
|
|
|
if ( targetTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
|
|
|
|
targetOffset = targetValueSize / 3;
|
|
|
|
}
|
|
|
|
const lastIndex = referenceTrack.times.length - 1;
|
|
let referenceValue;
|
|
|
|
// Find the value to subtract out of the track
|
|
if ( referenceTime <= referenceTrack.times[ 0 ] ) {
|
|
|
|
// Reference frame is earlier than the first keyframe, so just use the first keyframe
|
|
const startIndex = referenceOffset;
|
|
const endIndex = referenceValueSize - referenceOffset;
|
|
referenceValue = referenceTrack.values.slice( startIndex, endIndex );
|
|
|
|
} else if ( referenceTime >= referenceTrack.times[ lastIndex ] ) {
|
|
|
|
// Reference frame is after the last keyframe, so just use the last keyframe
|
|
const startIndex = lastIndex * referenceValueSize + referenceOffset;
|
|
const endIndex = startIndex + referenceValueSize - referenceOffset;
|
|
referenceValue = referenceTrack.values.slice( startIndex, endIndex );
|
|
|
|
} else {
|
|
|
|
// Interpolate to the reference value
|
|
const interpolant = referenceTrack.createInterpolant();
|
|
const startIndex = referenceOffset;
|
|
const endIndex = referenceValueSize - referenceOffset;
|
|
interpolant.evaluate( referenceTime );
|
|
referenceValue = interpolant.resultBuffer.slice( startIndex, endIndex );
|
|
|
|
}
|
|
|
|
// Conjugate the quaternion
|
|
if ( referenceTrackType === 'quaternion' ) {
|
|
|
|
const referenceQuat = new Quaternion().fromArray( referenceValue ).normalize().conjugate();
|
|
referenceQuat.toArray( referenceValue );
|
|
|
|
}
|
|
|
|
// Subtract the reference value from all of the track values
|
|
|
|
const numTimes = targetTrack.times.length;
|
|
for ( let j = 0; j < numTimes; ++ j ) {
|
|
|
|
const valueStart = j * targetValueSize + targetOffset;
|
|
|
|
if ( referenceTrackType === 'quaternion' ) {
|
|
|
|
// Multiply the conjugate for quaternion track types
|
|
Quaternion.multiplyQuaternionsFlat(
|
|
targetTrack.values,
|
|
valueStart,
|
|
referenceValue,
|
|
0,
|
|
targetTrack.values,
|
|
valueStart
|
|
);
|
|
|
|
} else {
|
|
|
|
const valueEnd = targetValueSize - targetOffset * 2;
|
|
|
|
// Subtract each value for all other numeric track types
|
|
for ( let k = 0; k < valueEnd; ++ k ) {
|
|
|
|
targetTrack.values[ valueStart + k ] -= referenceValue[ k ];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
targetClip.blendMode = AdditiveAnimationBlendMode;
|
|
|
|
return targetClip;
|
|
|
|
}
|
|
|
|
/**
|
|
* A class with various methods to assist with animations.
|
|
*
|
|
* @hideconstructor
|
|
*/
|
|
class AnimationUtils {
|
|
|
|
/**
|
|
* Converts an array to a specific type
|
|
*
|
|
* @static
|
|
* @param {TypedArray|Array} array - The array to convert.
|
|
* @param {TypedArray.constructor} type - The constructor of a type array.
|
|
* @return {TypedArray} The converted array
|
|
*/
|
|
static convertArray( array, type ) {
|
|
|
|
return convertArray( array, type );
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns `true` if the given object is a typed array.
|
|
*
|
|
* @static
|
|
* @param {any} object - The object to check.
|
|
* @return {boolean} Whether the given object is a typed array.
|
|
*/
|
|
static isTypedArray( object ) {
|
|
|
|
return isTypedArray( object );
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns an array by which times and values can be sorted.
|
|
*
|
|
* @static
|
|
* @param {Array<number>} times - The keyframe time values.
|
|
* @return {Array<number>} The array.
|
|
*/
|
|
static getKeyframeOrder( times ) {
|
|
|
|
return getKeyframeOrder( times );
|
|
|
|
}
|
|
|
|
/**
|
|
* Sorts the given array by the previously computed order via `getKeyframeOrder()`.
|
|
*
|
|
* @static
|
|
* @param {Array<number>} values - The values to sort.
|
|
* @param {number} stride - The stride.
|
|
* @param {Array<number>} order - The sort order.
|
|
* @return {Array<number>} The sorted values.
|
|
*/
|
|
static sortedArray( values, stride, order ) {
|
|
|
|
return sortedArray( values, stride, order );
|
|
|
|
}
|
|
|
|
/**
|
|
* Used for parsing AOS keyframe formats.
|
|
*
|
|
* @static
|
|
* @param {Array<number>} jsonKeys - A list of JSON keyframes.
|
|
* @param {Array<number>} times - This array will be filled with keyframe times by this method.
|
|
* @param {Array<number>} values - This array will be filled with keyframe values by this method.
|
|
* @param {string} valuePropertyName - The name of the property to use.
|
|
*/
|
|
static flattenJSON( jsonKeys, times, values, valuePropertyName ) {
|
|
|
|
flattenJSON( jsonKeys, times, values, valuePropertyName );
|
|
|
|
}
|
|
|
|
/**
|
|
* Creates a new clip, containing only the segment of the original clip between the given frames.
|
|
*
|
|
* @static
|
|
* @param {AnimationClip} sourceClip - The values to sort.
|
|
* @param {string} name - The name of the clip.
|
|
* @param {number} startFrame - The start frame.
|
|
* @param {number} endFrame - The end frame.
|
|
* @param {number} [fps=30] - The FPS.
|
|
* @return {AnimationClip} The new sub clip.
|
|
*/
|
|
static subclip( sourceClip, name, startFrame, endFrame, fps = 30 ) {
|
|
|
|
return subclip( sourceClip, name, startFrame, endFrame, fps );
|
|
|
|
}
|
|
|
|
/**
|
|
* Converts the keyframes of the given animation clip to an additive format.
|
|
*
|
|
* @static
|
|
* @param {AnimationClip} targetClip - The clip to make additive.
|
|
* @param {number} [referenceFrame=0] - The reference frame.
|
|
* @param {AnimationClip} [referenceClip=targetClip] - The reference clip.
|
|
* @param {number} [fps=30] - The FPS.
|
|
* @return {AnimationClip} The updated clip which is now additive.
|
|
*/
|
|
static makeClipAdditive( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 ) {
|
|
|
|
return makeClipAdditive( targetClip, referenceFrame, referenceClip, fps );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export {
|
|
convertArray,
|
|
isTypedArray,
|
|
getKeyframeOrder,
|
|
sortedArray,
|
|
flattenJSON,
|
|
subclip,
|
|
makeClipAdditive,
|
|
AnimationUtils
|
|
};
|