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 }; |