425 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			425 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import {
 | 
						|
	AnimationClip,
 | 
						|
	AnimationMixer,
 | 
						|
	Matrix4,
 | 
						|
	Quaternion,
 | 
						|
	QuaternionKeyframeTrack,
 | 
						|
	SkeletonHelper,
 | 
						|
	Vector3,
 | 
						|
	VectorKeyframeTrack
 | 
						|
} from 'three';
 | 
						|
 | 
						|
 | 
						|
function retarget( target, source, options = {} ) {
 | 
						|
 | 
						|
	const pos = new Vector3(),
 | 
						|
		quat = new Quaternion(),
 | 
						|
		scale = new Vector3(),
 | 
						|
		bindBoneMatrix = new Matrix4(),
 | 
						|
		relativeMatrix = new Matrix4(),
 | 
						|
		globalMatrix = new Matrix4();
 | 
						|
 | 
						|
	options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true;
 | 
						|
	options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true;
 | 
						|
	options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false;
 | 
						|
	options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
 | 
						|
	options.hip = options.hip !== undefined ? options.hip : 'hip';
 | 
						|
	options.names = options.names || {};
 | 
						|
 | 
						|
	const sourceBones = source.isObject3D ? source.skeleton.bones : getBones( source ),
 | 
						|
		bones = target.isObject3D ? target.skeleton.bones : getBones( target );
 | 
						|
 | 
						|
	let bindBones,
 | 
						|
		bone, name, boneTo,
 | 
						|
		bonesPosition;
 | 
						|
 | 
						|
	// reset bones
 | 
						|
 | 
						|
	if ( target.isObject3D ) {
 | 
						|
 | 
						|
		target.skeleton.pose();
 | 
						|
 | 
						|
	} else {
 | 
						|
 | 
						|
		options.useTargetMatrix = true;
 | 
						|
		options.preserveMatrix = false;
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	if ( options.preservePosition ) {
 | 
						|
 | 
						|
		bonesPosition = [];
 | 
						|
 | 
						|
		for ( let i = 0; i < bones.length; i ++ ) {
 | 
						|
 | 
						|
			bonesPosition.push( bones[ i ].position.clone() );
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	if ( options.preserveMatrix ) {
 | 
						|
 | 
						|
		// reset matrix
 | 
						|
 | 
						|
		target.updateMatrixWorld();
 | 
						|
 | 
						|
		target.matrixWorld.identity();
 | 
						|
 | 
						|
		// reset children matrix
 | 
						|
 | 
						|
		for ( let i = 0; i < target.children.length; ++ i ) {
 | 
						|
 | 
						|
			target.children[ i ].updateMatrixWorld( true );
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	if ( options.offsets ) {
 | 
						|
 | 
						|
		bindBones = [];
 | 
						|
 | 
						|
		for ( let i = 0; i < bones.length; ++ i ) {
 | 
						|
 | 
						|
			bone = bones[ i ];
 | 
						|
			name = options.names[ bone.name ] || bone.name;
 | 
						|
 | 
						|
			if ( options.offsets[ name ] ) {
 | 
						|
 | 
						|
				bone.matrix.multiply( options.offsets[ name ] );
 | 
						|
 | 
						|
				bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
 | 
						|
 | 
						|
				bone.updateMatrixWorld();
 | 
						|
 | 
						|
			}
 | 
						|
 | 
						|
			bindBones.push( bone.matrixWorld.clone() );
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	for ( let i = 0; i < bones.length; ++ i ) {
 | 
						|
 | 
						|
		bone = bones[ i ];
 | 
						|
		name = options.names[ bone.name ] || bone.name;
 | 
						|
 | 
						|
		boneTo = getBoneByName( name, sourceBones );
 | 
						|
 | 
						|
		globalMatrix.copy( bone.matrixWorld );
 | 
						|
 | 
						|
		if ( boneTo ) {
 | 
						|
 | 
						|
			boneTo.updateMatrixWorld();
 | 
						|
 | 
						|
			if ( options.useTargetMatrix ) {
 | 
						|
 | 
						|
				relativeMatrix.copy( boneTo.matrixWorld );
 | 
						|
 | 
						|
			} else {
 | 
						|
 | 
						|
				relativeMatrix.copy( target.matrixWorld ).invert();
 | 
						|
				relativeMatrix.multiply( boneTo.matrixWorld );
 | 
						|
 | 
						|
			}
 | 
						|
 | 
						|
			// ignore scale to extract rotation
 | 
						|
 | 
						|
			scale.setFromMatrixScale( relativeMatrix );
 | 
						|
			relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) );
 | 
						|
 | 
						|
			// apply to global matrix
 | 
						|
 | 
						|
			globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) );
 | 
						|
 | 
						|
			if ( target.isObject3D ) {
 | 
						|
 | 
						|
				const boneIndex = bones.indexOf( bone ),
 | 
						|
					wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.copy( target.skeleton.boneInverses[ boneIndex ] ).invert();
 | 
						|
 | 
						|
				globalMatrix.multiply( wBindMatrix );
 | 
						|
 | 
						|
			}
 | 
						|
 | 
						|
			globalMatrix.copyPosition( relativeMatrix );
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		if ( bone.parent && bone.parent.isBone ) {
 | 
						|
 | 
						|
			bone.matrix.copy( bone.parent.matrixWorld ).invert();
 | 
						|
			bone.matrix.multiply( globalMatrix );
 | 
						|
 | 
						|
		} else {
 | 
						|
 | 
						|
			bone.matrix.copy( globalMatrix );
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		if ( options.preserveHipPosition && name === options.hip ) {
 | 
						|
 | 
						|
			bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) );
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
 | 
						|
 | 
						|
		bone.updateMatrixWorld();
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	if ( options.preservePosition ) {
 | 
						|
 | 
						|
		for ( let i = 0; i < bones.length; ++ i ) {
 | 
						|
 | 
						|
			bone = bones[ i ];
 | 
						|
			name = options.names[ bone.name ] || bone.name;
 | 
						|
 | 
						|
			if ( name !== options.hip ) {
 | 
						|
 | 
						|
				bone.position.copy( bonesPosition[ i ] );
 | 
						|
 | 
						|
			}
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	if ( options.preserveMatrix ) {
 | 
						|
 | 
						|
		// restore matrix
 | 
						|
 | 
						|
		target.updateMatrixWorld( true );
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
function retargetClip( target, source, clip, options = {} ) {
 | 
						|
 | 
						|
	options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
 | 
						|
	// Calculate the fps from the source clip based on the track with the most frames, unless fps is already provided.
 | 
						|
	options.fps = options.fps !== undefined ? options.fps : ( Math.max( ...clip.tracks.map( track => track.times.length ) ) / clip.duration );
 | 
						|
	options.names = options.names || [];
 | 
						|
 | 
						|
	if ( ! source.isObject3D ) {
 | 
						|
 | 
						|
		source = getHelperFromSkeleton( source );
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	const numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
 | 
						|
		delta = clip.duration / ( numFrames - 1 ),
 | 
						|
		convertedTracks = [],
 | 
						|
		mixer = new AnimationMixer( source ),
 | 
						|
		bones = getBones( target.skeleton ),
 | 
						|
		boneDatas = [];
 | 
						|
	let positionOffset,
 | 
						|
		bone, boneTo, boneData,
 | 
						|
		name;
 | 
						|
 | 
						|
	mixer.clipAction( clip ).play();
 | 
						|
	mixer.update( 0 );
 | 
						|
 | 
						|
	source.updateMatrixWorld();
 | 
						|
 | 
						|
	for ( let i = 0; i < numFrames; ++ i ) {
 | 
						|
 | 
						|
		const time = i * delta;
 | 
						|
 | 
						|
		retarget( target, source, options );
 | 
						|
 | 
						|
		for ( let j = 0; j < bones.length; ++ j ) {
 | 
						|
 | 
						|
			name = options.names[ bones[ j ].name ] || bones[ j ].name;
 | 
						|
 | 
						|
			boneTo = getBoneByName( name, source.skeleton );
 | 
						|
 | 
						|
			if ( boneTo ) {
 | 
						|
 | 
						|
				bone = bones[ j ];
 | 
						|
				boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };
 | 
						|
 | 
						|
				if ( options.hip === name ) {
 | 
						|
 | 
						|
					if ( ! boneData.pos ) {
 | 
						|
 | 
						|
						boneData.pos = {
 | 
						|
							times: new Float32Array( numFrames ),
 | 
						|
							values: new Float32Array( numFrames * 3 )
 | 
						|
						};
 | 
						|
 | 
						|
					}
 | 
						|
 | 
						|
					if ( options.useFirstFramePosition ) {
 | 
						|
 | 
						|
						if ( i === 0 ) {
 | 
						|
 | 
						|
							positionOffset = bone.position.clone();
 | 
						|
 | 
						|
						}
 | 
						|
 | 
						|
						bone.position.sub( positionOffset );
 | 
						|
 | 
						|
					}
 | 
						|
 | 
						|
					boneData.pos.times[ i ] = time;
 | 
						|
 | 
						|
					bone.position.toArray( boneData.pos.values, i * 3 );
 | 
						|
 | 
						|
				}
 | 
						|
 | 
						|
				if ( ! boneData.quat ) {
 | 
						|
 | 
						|
					boneData.quat = {
 | 
						|
						times: new Float32Array( numFrames ),
 | 
						|
						values: new Float32Array( numFrames * 4 )
 | 
						|
					};
 | 
						|
 | 
						|
				}
 | 
						|
 | 
						|
				boneData.quat.times[ i ] = time;
 | 
						|
 | 
						|
				bone.quaternion.toArray( boneData.quat.values, i * 4 );
 | 
						|
 | 
						|
			}
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		if ( i === numFrames - 2 ) {
 | 
						|
 | 
						|
			// last mixer update before final loop iteration
 | 
						|
			// make sure we do not go over or equal to clip duration
 | 
						|
			mixer.update( delta - 0.0000001 );
 | 
						|
 | 
						|
		} else {
 | 
						|
 | 
						|
			mixer.update( delta );
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		source.updateMatrixWorld();
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	for ( let i = 0; i < boneDatas.length; ++ i ) {
 | 
						|
 | 
						|
		boneData = boneDatas[ i ];
 | 
						|
 | 
						|
		if ( boneData ) {
 | 
						|
 | 
						|
			if ( boneData.pos ) {
 | 
						|
 | 
						|
				convertedTracks.push( new VectorKeyframeTrack(
 | 
						|
					'.bones[' + boneData.bone.name + '].position',
 | 
						|
					boneData.pos.times,
 | 
						|
					boneData.pos.values
 | 
						|
				) );
 | 
						|
 | 
						|
			}
 | 
						|
 | 
						|
			convertedTracks.push( new QuaternionKeyframeTrack(
 | 
						|
				'.bones[' + boneData.bone.name + '].quaternion',
 | 
						|
				boneData.quat.times,
 | 
						|
				boneData.quat.values
 | 
						|
			) );
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	mixer.uncacheAction( clip );
 | 
						|
 | 
						|
	return new AnimationClip( clip.name, - 1, convertedTracks );
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
function clone( source ) {
 | 
						|
 | 
						|
	const sourceLookup = new Map();
 | 
						|
	const cloneLookup = new Map();
 | 
						|
 | 
						|
	const clone = source.clone();
 | 
						|
 | 
						|
	parallelTraverse( source, clone, function ( sourceNode, clonedNode ) {
 | 
						|
 | 
						|
		sourceLookup.set( clonedNode, sourceNode );
 | 
						|
		cloneLookup.set( sourceNode, clonedNode );
 | 
						|
 | 
						|
	} );
 | 
						|
 | 
						|
	clone.traverse( function ( node ) {
 | 
						|
 | 
						|
		if ( ! node.isSkinnedMesh ) return;
 | 
						|
 | 
						|
		const clonedMesh = node;
 | 
						|
		const sourceMesh = sourceLookup.get( node );
 | 
						|
		const sourceBones = sourceMesh.skeleton.bones;
 | 
						|
 | 
						|
		clonedMesh.skeleton = sourceMesh.skeleton.clone();
 | 
						|
		clonedMesh.bindMatrix.copy( sourceMesh.bindMatrix );
 | 
						|
 | 
						|
		clonedMesh.skeleton.bones = sourceBones.map( function ( bone ) {
 | 
						|
 | 
						|
			return cloneLookup.get( bone );
 | 
						|
 | 
						|
		} );
 | 
						|
 | 
						|
		clonedMesh.bind( clonedMesh.skeleton, clonedMesh.bindMatrix );
 | 
						|
 | 
						|
	} );
 | 
						|
 | 
						|
	return clone;
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
// internal helper
 | 
						|
 | 
						|
function getBoneByName( name, skeleton ) {
 | 
						|
 | 
						|
	for ( let i = 0, bones = getBones( skeleton ); i < bones.length; i ++ ) {
 | 
						|
 | 
						|
		if ( name === bones[ i ].name )
 | 
						|
 | 
						|
			return bones[ i ];
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
function getBones( skeleton ) {
 | 
						|
 | 
						|
	return Array.isArray( skeleton ) ? skeleton : skeleton.bones;
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
function getHelperFromSkeleton( skeleton ) {
 | 
						|
 | 
						|
	const source = new SkeletonHelper( skeleton.bones[ 0 ] );
 | 
						|
	source.skeleton = skeleton;
 | 
						|
 | 
						|
	return source;
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
function parallelTraverse( a, b, callback ) {
 | 
						|
 | 
						|
	callback( a, b );
 | 
						|
 | 
						|
	for ( let i = 0; i < a.children.length; i ++ ) {
 | 
						|
 | 
						|
		parallelTraverse( a.children[ i ], b.children[ i ], callback );
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
export {
 | 
						|
	retarget,
 | 
						|
	retargetClip,
 | 
						|
	clone,
 | 
						|
};
 |