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( '.bones[' + bone.name + '].position', times, positions ) ); } if ( scope.animateBoneRotations ) { tracks.push( new QuaternionKeyframeTrack( '.bones[' + 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 };