You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
438 lines
8.8 KiB
438 lines
8.8 KiB
6 months ago
|
import {
|
||
|
AnimationClip,
|
||
|
Bone,
|
||
|
FileLoader,
|
||
|
Loader,
|
||
|
Quaternion,
|
||
|
QuaternionKeyframeTrack,
|
||
|
Skeleton,
|
||
|
Vector3,
|
||
|
VectorKeyframeTrack
|
||
|
} from 'three';
|
||
|
|
||
|
/**
|
||
|
* Description: reads BVH files and outputs a single Skeleton and an AnimationClip
|
||
|
*
|
||
|
* Currently only supports bvh files containing a single root.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
class BVHLoader extends Loader {
|
||
|
|
||
|
constructor( manager ) {
|
||
|
|
||
|
super( manager );
|
||
|
|
||
|
this.animateBonePositions = true;
|
||
|
this.animateBoneRotations = true;
|
||
|
|
||
|
}
|
||
|
|
||
|
load( url, onLoad, onProgress, onError ) {
|
||
|
|
||
|
const scope = this;
|
||
|
|
||
|
const loader = new FileLoader( scope.manager );
|
||
|
loader.setPath( scope.path );
|
||
|
loader.setRequestHeader( scope.requestHeader );
|
||
|
loader.setWithCredentials( scope.withCredentials );
|
||
|
loader.load( url, function ( text ) {
|
||
|
|
||
|
try {
|
||
|
|
||
|
onLoad( scope.parse( text ) );
|
||
|
|
||
|
} catch ( e ) {
|
||
|
|
||
|
if ( onError ) {
|
||
|
|
||
|
onError( e );
|
||
|
|
||
|
} else {
|
||
|
|
||
|
console.error( e );
|
||
|
|
||
|
}
|
||
|
|
||
|
scope.manager.itemError( url );
|
||
|
|
||
|
}
|
||
|
|
||
|
}, onProgress, onError );
|
||
|
|
||
|
}
|
||
|
|
||
|
parse( text ) {
|
||
|
|
||
|
/*
|
||
|
reads a string array (lines) from a BVH file
|
||
|
and outputs a skeleton structure including motion data
|
||
|
|
||
|
returns thee root node:
|
||
|
{ name: '', channels: [], children: [] }
|
||
|
*/
|
||
|
function readBvh( lines ) {
|
||
|
|
||
|
// read model structure
|
||
|
|
||
|
if ( nextLine( lines ) !== 'HIERARCHY' ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: HIERARCHY expected.' );
|
||
|
|
||
|
}
|
||
|
|
||
|
const list = []; // collects flat array of all bones
|
||
|
const root = readNode( lines, nextLine( lines ), list );
|
||
|
|
||
|
// read motion data
|
||
|
|
||
|
if ( nextLine( lines ) !== 'MOTION' ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: MOTION expected.' );
|
||
|
|
||
|
}
|
||
|
|
||
|
// number of frames
|
||
|
|
||
|
let tokens = nextLine( lines ).split( /[\s]+/ );
|
||
|
const numFrames = parseInt( tokens[ 1 ] );
|
||
|
|
||
|
if ( isNaN( numFrames ) ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: Failed to read number of frames.' );
|
||
|
|
||
|
}
|
||
|
|
||
|
// frame time
|
||
|
|
||
|
tokens = nextLine( lines ).split( /[\s]+/ );
|
||
|
const frameTime = parseFloat( tokens[ 2 ] );
|
||
|
|
||
|
if ( isNaN( frameTime ) ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: Failed to read frame time.' );
|
||
|
|
||
|
}
|
||
|
|
||
|
// read frame data line by line
|
||
|
|
||
|
for ( let i = 0; i < numFrames; i ++ ) {
|
||
|
|
||
|
tokens = nextLine( lines ).split( /[\s]+/ );
|
||
|
readFrameData( tokens, i * frameTime, root );
|
||
|
|
||
|
}
|
||
|
|
||
|
return list;
|
||
|
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
Recursively reads data from a single frame into the bone hierarchy.
|
||
|
The passed bone hierarchy has to be structured in the same order as the BVH file.
|
||
|
keyframe data is stored in bone.frames.
|
||
|
|
||
|
- data: splitted string array (frame values), values are shift()ed so
|
||
|
this should be empty after parsing the whole hierarchy.
|
||
|
- frameTime: playback time for this keyframe.
|
||
|
- bone: the bone to read frame data from.
|
||
|
*/
|
||
|
function readFrameData( data, frameTime, bone ) {
|
||
|
|
||
|
// end sites have no motion data
|
||
|
|
||
|
if ( bone.type === 'ENDSITE' ) return;
|
||
|
|
||
|
// add keyframe
|
||
|
|
||
|
const keyframe = {
|
||
|
time: frameTime,
|
||
|
position: new Vector3(),
|
||
|
rotation: new Quaternion()
|
||
|
};
|
||
|
|
||
|
bone.frames.push( keyframe );
|
||
|
|
||
|
const quat = new Quaternion();
|
||
|
|
||
|
const vx = new Vector3( 1, 0, 0 );
|
||
|
const vy = new Vector3( 0, 1, 0 );
|
||
|
const vz = new Vector3( 0, 0, 1 );
|
||
|
|
||
|
// parse values for each channel in node
|
||
|
|
||
|
for ( let i = 0; i < bone.channels.length; i ++ ) {
|
||
|
|
||
|
switch ( bone.channels[ i ] ) {
|
||
|
|
||
|
case 'Xposition':
|
||
|
keyframe.position.x = parseFloat( data.shift().trim() );
|
||
|
break;
|
||
|
case 'Yposition':
|
||
|
keyframe.position.y = parseFloat( data.shift().trim() );
|
||
|
break;
|
||
|
case 'Zposition':
|
||
|
keyframe.position.z = parseFloat( data.shift().trim() );
|
||
|
break;
|
||
|
case 'Xrotation':
|
||
|
quat.setFromAxisAngle( vx, parseFloat( data.shift().trim() ) * Math.PI / 180 );
|
||
|
keyframe.rotation.multiply( quat );
|
||
|
break;
|
||
|
case 'Yrotation':
|
||
|
quat.setFromAxisAngle( vy, parseFloat( data.shift().trim() ) * Math.PI / 180 );
|
||
|
keyframe.rotation.multiply( quat );
|
||
|
break;
|
||
|
case 'Zrotation':
|
||
|
quat.setFromAxisAngle( vz, parseFloat( data.shift().trim() ) * Math.PI / 180 );
|
||
|
keyframe.rotation.multiply( quat );
|
||
|
break;
|
||
|
default:
|
||
|
console.warn( 'THREE.BVHLoader: Invalid channel type.' );
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
// parse child nodes
|
||
|
|
||
|
for ( let i = 0; i < bone.children.length; i ++ ) {
|
||
|
|
||
|
readFrameData( data, frameTime, bone.children[ i ] );
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
Recursively parses the HIERACHY section of the BVH file
|
||
|
|
||
|
- lines: all lines of the file. lines are consumed as we go along.
|
||
|
- firstline: line containing the node type and name e.g. 'JOINT hip'
|
||
|
- list: collects a flat list of nodes
|
||
|
|
||
|
returns: a BVH node including children
|
||
|
*/
|
||
|
function readNode( lines, firstline, list ) {
|
||
|
|
||
|
const node = { name: '', type: '', frames: [] };
|
||
|
list.push( node );
|
||
|
|
||
|
// parse node type and name
|
||
|
|
||
|
let tokens = firstline.split( /[\s]+/ );
|
||
|
|
||
|
if ( tokens[ 0 ].toUpperCase() === 'END' && tokens[ 1 ].toUpperCase() === 'SITE' ) {
|
||
|
|
||
|
node.type = 'ENDSITE';
|
||
|
node.name = 'ENDSITE'; // bvh end sites have no name
|
||
|
|
||
|
} else {
|
||
|
|
||
|
node.name = tokens[ 1 ];
|
||
|
node.type = tokens[ 0 ].toUpperCase();
|
||
|
|
||
|
}
|
||
|
|
||
|
if ( nextLine( lines ) !== '{' ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: Expected opening { after type & name' );
|
||
|
|
||
|
}
|
||
|
|
||
|
// parse OFFSET
|
||
|
|
||
|
tokens = nextLine( lines ).split( /[\s]+/ );
|
||
|
|
||
|
if ( tokens[ 0 ] !== 'OFFSET' ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: Expected OFFSET but got: ' + tokens[ 0 ] );
|
||
|
|
||
|
}
|
||
|
|
||
|
if ( tokens.length !== 4 ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: Invalid number of values for OFFSET.' );
|
||
|
|
||
|
}
|
||
|
|
||
|
const offset = new Vector3(
|
||
|
parseFloat( tokens[ 1 ] ),
|
||
|
parseFloat( tokens[ 2 ] ),
|
||
|
parseFloat( tokens[ 3 ] )
|
||
|
);
|
||
|
|
||
|
if ( isNaN( offset.x ) || isNaN( offset.y ) || isNaN( offset.z ) ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: Invalid values of OFFSET.' );
|
||
|
|
||
|
}
|
||
|
|
||
|
node.offset = offset;
|
||
|
|
||
|
// parse CHANNELS definitions
|
||
|
|
||
|
if ( node.type !== 'ENDSITE' ) {
|
||
|
|
||
|
tokens = nextLine( lines ).split( /[\s]+/ );
|
||
|
|
||
|
if ( tokens[ 0 ] !== 'CHANNELS' ) {
|
||
|
|
||
|
console.error( 'THREE.BVHLoader: Expected CHANNELS definition.' );
|
||
|
|
||
|
}
|
||
|
|
||
|
const numChannels = parseInt( tokens[ 1 ] );
|
||
|
node.channels = tokens.splice( 2, numChannels );
|
||
|
node.children = [];
|
||
|
|
||
|
}
|
||
|
|
||
|
// read children
|
||
|
|
||
|
while ( true ) {
|
||
|
|
||
|
const line = nextLine( lines );
|
||
|
|
||
|
if ( line === '}' ) {
|
||
|
|
||
|
return node;
|
||
|
|
||
|
} else {
|
||
|
|
||
|
node.children.push( readNode( lines, line, list ) );
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
recursively converts the internal bvh node structure to a Bone hierarchy
|
||
|
|
||
|
source: the bvh root node
|
||
|
list: pass an empty array, collects a flat list of all converted THREE.Bones
|
||
|
|
||
|
returns the root Bone
|
||
|
*/
|
||
|
function toTHREEBone( source, list ) {
|
||
|
|
||
|
const bone = new Bone();
|
||
|
list.push( bone );
|
||
|
|
||
|
bone.position.add( source.offset );
|
||
|
bone.name = source.name;
|
||
|
|
||
|
if ( source.type !== 'ENDSITE' ) {
|
||
|
|
||
|
for ( let i = 0; i < source.children.length; i ++ ) {
|
||
|
|
||
|
bone.add( toTHREEBone( source.children[ i ], list ) );
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
return bone;
|
||
|
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
builds a AnimationClip from the keyframe data saved in each bone.
|
||
|
|
||
|
bone: bvh root node
|
||
|
|
||
|
returns: a AnimationClip containing position and quaternion tracks
|
||
|
*/
|
||
|
function toTHREEAnimation( bones ) {
|
||
|
|
||
|
const tracks = [];
|
||
|
|
||
|
// create a position and quaternion animation track for each node
|
||
|
|
||
|
for ( let i = 0; i < bones.length; i ++ ) {
|
||
|
|
||
|
const bone = bones[ i ];
|
||
|
|
||
|
if ( bone.type === 'ENDSITE' )
|
||
|
continue;
|
||
|
|
||
|
// track data
|
||
|
|
||
|
const times = [];
|
||
|
const positions = [];
|
||
|
const rotations = [];
|
||
|
|
||
|
for ( let j = 0; j < bone.frames.length; j ++ ) {
|
||
|
|
||
|
const frame = bone.frames[ j ];
|
||
|
|
||
|
times.push( frame.time );
|
||
|
|
||
|
// the animation system animates the position property,
|
||
|
// so we have to add the joint offset to all values
|
||
|
|
||
|
positions.push( frame.position.x + bone.offset.x );
|
||
|
positions.push( frame.position.y + bone.offset.y );
|
||
|
positions.push( frame.position.z + bone.offset.z );
|
||
|
|
||
|
rotations.push( frame.rotation.x );
|
||
|
rotations.push( frame.rotation.y );
|
||
|
rotations.push( frame.rotation.z );
|
||
|
rotations.push( frame.rotation.w );
|
||
|
|
||
|
}
|
||
|
|
||
|
if ( scope.animateBonePositions ) {
|
||
|
|
||
|
tracks.push( new VectorKeyframeTrack( bone.name + '.position', times, positions ) );
|
||
|
|
||
|
}
|
||
|
|
||
|
if ( scope.animateBoneRotations ) {
|
||
|
|
||
|
tracks.push( new QuaternionKeyframeTrack( bone.name + '.quaternion', times, rotations ) );
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
return new AnimationClip( 'animation', - 1, tracks );
|
||
|
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
returns the next non-empty line in lines
|
||
|
*/
|
||
|
function nextLine( lines ) {
|
||
|
|
||
|
let line;
|
||
|
// skip empty lines
|
||
|
while ( ( line = lines.shift().trim() ).length === 0 ) { }
|
||
|
|
||
|
return line;
|
||
|
|
||
|
}
|
||
|
|
||
|
const scope = this;
|
||
|
|
||
|
const lines = text.split( /[\r\n]+/g );
|
||
|
|
||
|
const bones = readBvh( lines );
|
||
|
|
||
|
const threeBones = [];
|
||
|
toTHREEBone( bones[ 0 ], threeBones );
|
||
|
|
||
|
const threeClip = toTHREEAnimation( bones );
|
||
|
|
||
|
return {
|
||
|
skeleton: new Skeleton( threeBones ),
|
||
|
clip: threeClip
|
||
|
};
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
export { BVHLoader };
|