438 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			438 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 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 };
 |