Skip to content

⬅️ Back to Table of Contents

📄 BVHLoader.js

📊 Analysis Summary

Metric Count
🔧 Functions 8
🧱 Classes 1
📦 Imports 9
📊 Variables & Constants 20

📚 Table of Contents

🛠️ File Location:

📂 examples/jsm/loaders/BVHLoader.js

📦 Imports

Name Source
AnimationClip three
Bone three
FileLoader three
Loader three
Quaternion three
QuaternionKeyframeTrack three
Skeleton three
Vector3 three
VectorKeyframeTrack three

Variables & Constants

Name Type Kind Value Exported
scope this let/var this
loader any let/var new FileLoader( scope.manager )
list any[] let/var []
keyframe { time: any; position: any; rotation:... let/var { time: frameTime, position: new Vector3(), rotation: new Quaternion() }
quat any let/var new Quaternion()
vx any let/var new Vector3( 1, 0, 0 )
vy any let/var new Vector3( 0, 1, 0 )
vz any let/var new Vector3( 0, 0, 1 )
node { name: string; type: string; frames:... let/var { name: '', type: '', frames: [] }
offset any let/var new Vector3( parseFloat( tokens[ 1 ] ), parseFloat( tokens[ 2 ] ), parseFloat...
bone any let/var new Bone()
tracks any[] let/var []
bone any let/var bones[ i ]
times any[] let/var []
positions any[] let/var []
rotations any[] let/var []
frame any let/var bone.frames[ j ]
line any let/var *not shown*
scope this let/var this
threeBones any[] let/var []

Functions

BVHLoader.load(url: string, onLoad: (arg0: { skeleton: Skeleton; clip: AnimationClip; }) => any, onProgress: onProgressCallback, onError: onErrorCallback): void

JSDoc:

/**
     * 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.
     */

Parameters:

  • url string
  • onLoad (arg0: { skeleton: Skeleton; clip: AnimationClip; }) => any
  • onProgress onProgressCallback
  • onError onErrorCallback

Returns: void

Calls:

  • loader.setPath
  • loader.setRequestHeader
  • loader.setWithCredentials
  • loader.load
  • onLoad
  • scope.parse
  • onError
  • console.error
  • scope.manager.itemError
Code
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 );

    }

BVHLoader.parse(text: string): { skeleton: Skeleton; clip: AnimationClip; }

JSDoc:

/**
     * 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.
     */

Parameters:

  • text string

Returns: { skeleton: Skeleton; clip: AnimationClip; }

Calls:

  • nextLine
  • console.error
  • readNode
  • nextLine( lines ).split
  • parseInt
  • isNaN
  • parseFloat
  • readFrameData
  • bone.frames.push
  • data.shift().trim
  • quat.setFromAxisAngle
  • keyframe.rotation.multiply
  • console.warn
  • list.push
  • firstline.split
  • tokens[ 0 ].toUpperCase
  • tokens[ 1 ].toUpperCase
  • tokens.splice
  • node.children.push
  • bone.position.add
  • bone.add
  • toTHREEBone
  • times.push
  • positions.push
  • rotations.push
  • tracks.push
  • lines.shift().trim
  • text.split
  • readBvh
  • toTHREEAnimation

Internal Comments:

// reads a string array (lines) from a BVH file
// and outputs a skeleton structure including motion data
// returns thee root node:
// { name: '', channels: [], children: [] }
// read model structure
// read motion data
// number of frames (x2)
// frame time (x3)
// read frame data line by line
/*
            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.
        */
// end sites have no motion data
// add keyframe (x2)
// parse values for each channel in node
// parse child nodes
/*
         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
        */
// parse node type and name (x2)
// parse OFFSET (x3)
// parse CHANNELS definitions
// read children
/*
            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
        */
/*
            builds an AnimationClip from the keyframe data saved in each bone.

            bone: bvh root node

            returns: an AnimationClip containing position and quaternion tracks
        */
// create a position and quaternion animation track for each node
// track data (x2)
// the animation system animates the position property, (x4)
// so we have to add the joint offset to all values (x4)
/*
            returns the next non-empty line in lines
        */
// skip empty lines

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

    }

readBvh(lines: any): any[]

Parameters:

  • lines any

Returns: any[]

Calls:

  • nextLine
  • console.error
  • readNode
  • nextLine( lines ).split
  • parseInt
  • isNaN
  • parseFloat
  • readFrameData

Internal Comments:

// read model structure
// read motion data
// number of frames (x2)
// frame time (x3)
// read frame data line by line

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

        }

readFrameData(data: any, frameTime: any, bone: any): void

Parameters:

  • data any
  • frameTime any
  • bone any

Returns: void

Calls:

  • bone.frames.push
  • parseFloat
  • data.shift().trim
  • quat.setFromAxisAngle
  • keyframe.rotation.multiply
  • console.warn
  • readFrameData

Internal Comments:

// end sites have no motion data
// add keyframe (x2)
// parse values for each channel in node
// parse child nodes

Code
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 ] );

            }

        }

readNode(lines: any, firstline: any, list: any): { name: string; type: string; frames: any[]; }

Parameters:

  • lines any
  • firstline any
  • list any

Returns: { name: string; type: string; frames: any[]; }

Calls:

  • list.push
  • firstline.split
  • tokens[ 0 ].toUpperCase
  • tokens[ 1 ].toUpperCase
  • nextLine
  • console.error
  • nextLine( lines ).split
  • parseFloat
  • isNaN
  • parseInt
  • tokens.splice
  • node.children.push
  • readNode

Internal Comments:

// parse node type and name (x2)
// parse OFFSET (x3)
// parse CHANNELS definitions
// read children

Code
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 ) );

                }

            }

        }

toTHREEBone(source: any, list: any): any

Parameters:

  • source any
  • list any

Returns: any

Calls:

  • list.push
  • bone.position.add
  • bone.add
  • toTHREEBone
Code
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;

        }

toTHREEAnimation(bones: any): any

Parameters:

  • bones any

Returns: any

Calls:

  • times.push
  • positions.push
  • rotations.push
  • tracks.push

Internal Comments:

// create a position and quaternion animation track for each node
// track data (x2)
// the animation system animates the position property, (x4)
// so we have to add the joint offset to all values (x4)

Code
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 );

        }

nextLine(lines: any): any

Parameters:

  • lines any

Returns: any

Calls:

  • lines.shift().trim

Internal Comments:

// skip empty lines

Code
function nextLine( lines ) {

            let line;
            // skip empty lines
            while ( ( line = lines.shift().trim() ).length === 0 ) { }

            return line;

        }

Classes

BVHLoader

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

    }

}

Methods

load(url: string, onLoad: (arg0: { skeleton: Skeleton; clip: AnimationClip; }) => any, onProgress: onProgressCallback, onError: onErrorCallback): void
Code
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: string): { skeleton: Skeleton; clip: AnimationClip; }
Code
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
        };

    }