/** * @author herzig / http://github.com/herzig * * Description: reads BVH files and outputs a single THREE.Skeleton and an THREE.AnimationClip * * Currently only supports bvh files containing a single root. * */ THREE.BVHLoader = function( manager ) { this.animateBonePositions = true; this.animateBoneRotations = true; this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; } THREE.BVHLoader.prototype.load = function( url, onLoad, onProgress, onError ) { var scope = this; var loader = new THREE.XHRLoader( scope.manager ); loader.setResponseType( 'arraybuffer' ); loader.load( url, function( buffer ) { onLoad( scope.parse( buffer ) ); }, onProgress, onError ); } THREE.BVHLoader.prototype.parse = function( buffer ) { // convert buffer to ASCII string var text = ""; var raw = new Uint8Array( buffer ); for ( var i = 0; i < raw.length; ++ i ) { text += String.fromCharCode( raw[ i ] ); } var lines = text.split( /[\r\n]+/g ); var bones = this._readBvh( lines ); var threeBones = []; this._toTHREEBone( bones[ 0 ], threeBones ); var threeClip = this._toTHREEAnimation( bones ); return { skeleton: new THREE.Skeleton( threeBones ), clip: threeClip }; } /* reads a string array (lines) from a BVH file and outputs a skeleton structure including motion data returns thee root node: { name: "", channels: [], children: [] } */ THREE.BVHLoader.prototype._readBvh = function( lines ) { // read model structure if ( this._nextLine( lines ) !== "HIERARCHY" ) { throw "HIERARCHY expected"; } var list = []; // collects flat array of all bones var root = this._readNode( lines, this._nextLine( lines ), list ); // read motion data if ( this._nextLine( lines ) != "MOTION" ) { throw "MOTION expected"; } // number of frames var tokens = this._nextLine( lines ).split( /[\s]+/ ); var numFrames = parseInt( tokens[ 1 ] ); if ( isNaN( numFrames ) ) { throw "Failed to read number of frames."; } // frame time tokens = this._nextLine( lines ).split( /[\s]+/ ); var frameTime = parseFloat( tokens[ 2 ] ); if ( isNaN( frameTime ) ) { throw "Failed to read frame time."; } // read frame data line by line for ( var i = 0; i < numFrames; ++ i ) { tokens = this._nextLine( lines ).split( /[\s]+/ ); this._readFrameData( tokens, i * frameTime, root, list ); } 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. */ THREE.BVHLoader.prototype._readFrameData = function( data, frameTime, bone ) { // end sites have no motion data if ( bone.type === "ENDSITE" ) { return; } // add keyframe var keyframe = { time: frameTime, position: { x: 0, y: 0, z: 0 }, rotation: new THREE.Quaternion(), }; bone.frames.push( keyframe ); var vx = new THREE.Vector3( 1, 0, 0 ); var vy = new THREE.Vector3( 0, 1, 0 ); var vz = new THREE.Vector3( 0, 0, 1 ); // parse values for each channel in node for ( var 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": var quat = new THREE.Quaternion(); quat.setFromAxisAngle( vx, parseFloat( data.shift().trim() ) * Math.PI / 180 ); keyframe.rotation.multiply( quat ); break; case "Yrotation": var quat = new THREE.Quaternion(); quat.setFromAxisAngle( vy, parseFloat( data.shift().trim() ) * Math.PI / 180 ); keyframe.rotation.multiply( quat ); break; case "Zrotation": var quat = new THREE.Quaternion(); quat.setFromAxisAngle( vz, parseFloat( data.shift().trim() ) * Math.PI / 180 ); keyframe.rotation.multiply( quat ); break; default: throw "invalid channel type"; break; } } // parse child nodes for ( var i = 0; i < bone.children.length; ++ i ) { this._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 */ THREE.BVHLoader.prototype._readNode = function( lines, firstline, list ) { var node = { name: "", type: "", frames: [] }; list.push( node ); // parse node type and name. var 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 ( this._nextLine( lines ) != "{" ) { throw "Expected opening { after type & name"; } // parse OFFSET tokens = this._nextLine( lines ).split( /[\s]+/ ); if ( tokens[ 0 ] !== "OFFSET" ) { throw "Expected OFFSET, but got: " + tokens[ 0 ]; } if ( tokens.length != 4 ) { throw "OFFSET: Invalid number of values"; } var offset = { x: parseFloat( tokens[ 1 ] ), y: parseFloat( tokens[ 2 ] ), z: parseFloat( tokens[ 3 ] ) }; if ( isNaN( offset.x ) || isNaN( offset.y ) || isNaN( offset.z ) ) { throw "OFFSET: Invalid values"; } node.offset = offset; // parse CHANNELS definitions if ( node.type != "ENDSITE" ) { tokens = this._nextLine( lines ).split( /[\s]+/ ); if ( tokens[ 0 ] != "CHANNELS" ) { throw "Expected CHANNELS definition"; } var numChannels = parseInt( tokens[ 1 ] ); node.channels = tokens.splice( 2, numChannels ); node.children = []; } // read children while ( true ) { var line = this._nextLine( lines ); if ( line === "}" ) { return node; } else { node.children.push( this._readNode( lines, line, list ) ); } } } /* recursively converts the internal bvh node structure to a THREE.Bone hierarchy source: the bvh root node list: pass an empty array, collects a flat list of all converted THREE.Bones returns the root THREE.Bone */ THREE.BVHLoader.prototype._toTHREEBone = function( source, list ) { var bone = new THREE.Bone(); list.push( bone ); bone.position.add( source.offset ); bone.name = source.name; if ( source.type != "ENDSITE" ) { for ( var i = 0; i < source.children.length; ++ i ) { bone.add( this._toTHREEBone( source.children[ i ], list ) ); } } return bone; } /* builds a THREE.AnimationClip from the keyframe data saved in each bone. bone: bvh root node returns: a THREE.AnimationClip containing position and quaternion tracks */ THREE.BVHLoader.prototype._toTHREEAnimation = function( bones ) { var tracks = []; // create a position and quaternion animation track for each node for ( var i = 0; i < bones.length; ++ i ) { var bone = bones[ i ]; if ( bone.type == "ENDSITE" ) continue; // track data var times = []; var positions = []; var rotations = []; for ( var j = 0; j < bone.frames.length; ++ j ) { var 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 ( this.animateBonePositions ) { tracks.push( new THREE.VectorKeyframeTrack( ".bones[" + bone.name + "].position", times, positions ) ); } if ( this.animateBoneRotations ) { tracks.push( new THREE.QuaternionKeyframeTrack( ".bones[" + bone.name + "].quaternion", times, rotations ) ); } } var clip = new THREE.AnimationClip( "animation", - 1, tracks ); return clip; } /* returns the next non-empty line in lines */ THREE.BVHLoader.prototype._nextLine = function( lines ) { var line; // skip empty lines while ( ( line = lines.shift().trim() ).length === 0 ) { } return line; }