Spaces:
Running
Running
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 HIERARCHY 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 }; | |