Dies ist das Repository meines kleinen Portfolios.
Im Hintergrund läuft eine Planetensimulation, geschrieben in JavaScript und Three.js.
Die zu sehenden Texturen stammen von:
https://www.solarsystemscope.com/textures/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
394 lines
9.0 KiB
394 lines
9.0 KiB
( function () { |
|
|
|
/** |
|
* Description: reads BVH files and outputs a single THREE.Skeleton and an THREE.AnimationClip |
|
* |
|
* Currently only supports bvh files containing a single root. |
|
* |
|
*/ |
|
|
|
class BVHLoader extends THREE.Loader { |
|
|
|
constructor( manager ) { |
|
|
|
super( manager ); |
|
this.animateBonePositions = true; |
|
this.animateBoneRotations = true; |
|
|
|
} |
|
|
|
load( url, onLoad, onProgress, onError ) { |
|
|
|
const scope = this; |
|
const loader = new THREE.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 THREE.Vector3(), |
|
rotation: new THREE.Quaternion() |
|
}; |
|
bone.frames.push( keyframe ); |
|
const quat = new THREE.Quaternion(); |
|
const vx = new THREE.Vector3( 1, 0, 0 ); |
|
const vy = new THREE.Vector3( 0, 1, 0 ); |
|
const vz = new THREE.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 THREE.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 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 |
|
*/ |
|
|
|
|
|
function toTHREEBone( source, list ) { |
|
|
|
const bone = new THREE.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 THREE.AnimationClip from the keyframe data saved in each bone. |
|
bone: bvh root node |
|
returns: a THREE.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 THREE.VectorKeyframeTrack( '.bones[' + bone.name + '].position', times, positions ) ); |
|
|
|
} |
|
|
|
if ( scope.animateBoneRotations ) { |
|
|
|
tracks.push( new THREE.QuaternionKeyframeTrack( '.bones[' + bone.name + '].quaternion', times, rotations ) ); |
|
|
|
} |
|
|
|
} |
|
|
|
return new THREE.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 THREE.Skeleton( threeBones ), |
|
clip: threeClip |
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
THREE.BVHLoader = BVHLoader; |
|
|
|
} )();
|
|
|