Buckets:
| import { | |
| AnimationClip, | |
| Bone, | |
| FileLoader, | |
| Loader, | |
| Quaternion, | |
| QuaternionKeyframeTrack, | |
| Skeleton, | |
| Vector3, | |
| VectorKeyframeTrack | |
| } from 'three'; | |
| /** | |
| * A loader for the BVH format. | |
| * | |
| * Imports BVH files and outputs a single {@link Skeleton} and {@link AnimationClip}. | |
| * The loader only supports BVH files containing a single root right now. | |
| * | |
| * ```js | |
| * const loader = new BVHLoader(); | |
| * const result = await loader.loadAsync( 'models/bvh/pirouette.bvh' ); | |
| * | |
| * // visualize skeleton | |
| * const skeletonHelper = new THREE.SkeletonHelper( result.skeleton.bones[ 0 ] ); | |
| * scene.add( result.skeleton.bones[ 0 ] ); | |
| * scene.add( skeletonHelper ); | |
| * | |
| * // play animation clip | |
| * mixer = new THREE.AnimationMixer( result.skeleton.bones[ 0 ] ); | |
| * mixer.clipAction( result.clip ).play(); | |
| * ``` | |
| * | |
| * @augments Loader | |
| * @three_import import { BVHLoader } from 'three/addons/loaders/BVHLoader.js'; | |
| */ | |
| class BVHLoader extends Loader { | |
| /** | |
| * Constructs a new BVH loader. | |
| * | |
| * @param {LoadingManager} [manager] - The loading manager. | |
| */ | |
| constructor( manager ) { | |
| super( manager ); | |
| /** | |
| * Whether to animate bone positions or not. | |
| * | |
| * @type {boolean} | |
| * @default true | |
| */ | |
| this.animateBonePositions = true; | |
| /** | |
| * Whether to animate bone rotations or not. | |
| * | |
| * @type {boolean} | |
| * @default true | |
| */ | |
| this.animateBoneRotations = true; | |
| } | |
| /** | |
| * Starts loading from the given URL and passes the loaded BVH asset | |
| * to the `onLoad()` callback. | |
| * | |
| * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI. | |
| * @param {function({skeleton:Skeleton,clip:AnimationClip})} onLoad - Executed when the loading process has been finished. | |
| * @param {onProgressCallback} onProgress - Executed while the loading is in progress. | |
| * @param {onErrorCallback} onError - Executed when errors occur. | |
| */ | |
| 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 ); | |
| } | |
| /** | |
| * Parses the given BVH data and returns the resulting data. | |
| * | |
| * @param {string} text - The raw BVH data as a string. | |
| * @return {{skeleton:Skeleton,clip:AnimationClip}} An object representing the parsed asset. | |
| */ | |
| 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 an AnimationClip from the keyframe data saved in each bone. | |
| bone: bvh root node | |
| returns: an 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 }; | |
Xet Storage Details
- Size:
- 10.6 kB
- Xet hash:
- 3349eb547f1e45650ad3ce6530472d334f5b90aa3acdc28b360ecc4795ed7006
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.