BVHLoader.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. import {
  2. AnimationClip,
  3. Bone,
  4. FileLoader,
  5. Loader,
  6. Quaternion,
  7. QuaternionKeyframeTrack,
  8. Skeleton,
  9. Vector3,
  10. VectorKeyframeTrack
  11. } from 'three';
  12. /**
  13. * A loader for the BVH format.
  14. *
  15. * Imports BVH files and outputs a single {@link Skeleton} and {@link AnimationClip}.
  16. * The loader only supports BVH files containing a single root right now.
  17. *
  18. * ```js
  19. * const loader = new BVHLoader();
  20. * const result = await loader.loadAsync( 'models/bvh/pirouette.bvh' );
  21. *
  22. * // visualize skeleton
  23. * const skeletonHelper = new THREE.SkeletonHelper( result.skeleton.bones[ 0 ] );
  24. * scene.add( result.skeleton.bones[ 0 ] );
  25. * scene.add( skeletonHelper );
  26. *
  27. * // play animation clip
  28. * mixer = new THREE.AnimationMixer( result.skeleton.bones[ 0 ] );
  29. * mixer.clipAction( result.clip ).play();
  30. * ```
  31. *
  32. * @augments Loader
  33. * @three_import import { BVHLoader } from 'three/addons/loaders/BVHLoader.js';
  34. */
  35. class BVHLoader extends Loader {
  36. /**
  37. * Constructs a new BVH loader.
  38. *
  39. * @param {LoadingManager} [manager] - The loading manager.
  40. */
  41. constructor( manager ) {
  42. super( manager );
  43. /**
  44. * Whether to animate bone positions or not.
  45. *
  46. * @type {boolean}
  47. * @default true
  48. */
  49. this.animateBonePositions = true;
  50. /**
  51. * Whether to animate bone rotations or not.
  52. *
  53. * @type {boolean}
  54. * @default true
  55. */
  56. this.animateBoneRotations = true;
  57. }
  58. /**
  59. * Starts loading from the given URL and passes the loaded BVH asset
  60. * to the `onLoad()` callback.
  61. *
  62. * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
  63. * @param {function({skeleton:Skeleton,clip:AnimationClip})} onLoad - Executed when the loading process has been finished.
  64. * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
  65. * @param {onErrorCallback} onError - Executed when errors occur.
  66. */
  67. load( url, onLoad, onProgress, onError ) {
  68. const scope = this;
  69. const loader = new FileLoader( scope.manager );
  70. loader.setPath( scope.path );
  71. loader.setRequestHeader( scope.requestHeader );
  72. loader.setWithCredentials( scope.withCredentials );
  73. loader.load( url, function ( text ) {
  74. try {
  75. onLoad( scope.parse( text ) );
  76. } catch ( e ) {
  77. if ( onError ) {
  78. onError( e );
  79. } else {
  80. console.error( e );
  81. }
  82. scope.manager.itemError( url );
  83. }
  84. }, onProgress, onError );
  85. }
  86. /**
  87. * Parses the given BVH data and returns the resulting data.
  88. *
  89. * @param {string} text - The raw BVH data as a string.
  90. * @return {{skeleton:Skeleton,clip:AnimationClip}} An object representing the parsed asset.
  91. */
  92. parse( text ) {
  93. // reads a string array (lines) from a BVH file
  94. // and outputs a skeleton structure including motion data
  95. // returns thee root node:
  96. // { name: '', channels: [], children: [] }
  97. function readBvh( lines ) {
  98. // read model structure
  99. if ( nextLine( lines ) !== 'HIERARCHY' ) {
  100. console.error( 'THREE.BVHLoader: HIERARCHY expected.' );
  101. }
  102. const list = []; // collects flat array of all bones
  103. const root = readNode( lines, nextLine( lines ), list );
  104. // read motion data
  105. if ( nextLine( lines ) !== 'MOTION' ) {
  106. console.error( 'THREE.BVHLoader: MOTION expected.' );
  107. }
  108. // number of frames
  109. let tokens = nextLine( lines ).split( /[\s]+/ );
  110. const numFrames = parseInt( tokens[ 1 ] );
  111. if ( isNaN( numFrames ) ) {
  112. console.error( 'THREE.BVHLoader: Failed to read number of frames.' );
  113. }
  114. // frame time
  115. tokens = nextLine( lines ).split( /[\s]+/ );
  116. const frameTime = parseFloat( tokens[ 2 ] );
  117. if ( isNaN( frameTime ) ) {
  118. console.error( 'THREE.BVHLoader: Failed to read frame time.' );
  119. }
  120. // read frame data line by line
  121. for ( let i = 0; i < numFrames; i ++ ) {
  122. tokens = nextLine( lines ).split( /[\s]+/ );
  123. readFrameData( tokens, i * frameTime, root );
  124. }
  125. return list;
  126. }
  127. /*
  128. Recursively reads data from a single frame into the bone hierarchy.
  129. The passed bone hierarchy has to be structured in the same order as the BVH file.
  130. keyframe data is stored in bone.frames.
  131. - data: splitted string array (frame values), values are shift()ed so
  132. this should be empty after parsing the whole hierarchy.
  133. - frameTime: playback time for this keyframe.
  134. - bone: the bone to read frame data from.
  135. */
  136. function readFrameData( data, frameTime, bone ) {
  137. // end sites have no motion data
  138. if ( bone.type === 'ENDSITE' ) return;
  139. // add keyframe
  140. const keyframe = {
  141. time: frameTime,
  142. position: new Vector3(),
  143. rotation: new Quaternion()
  144. };
  145. bone.frames.push( keyframe );
  146. const quat = new Quaternion();
  147. const vx = new Vector3( 1, 0, 0 );
  148. const vy = new Vector3( 0, 1, 0 );
  149. const vz = new Vector3( 0, 0, 1 );
  150. // parse values for each channel in node
  151. for ( let i = 0; i < bone.channels.length; i ++ ) {
  152. switch ( bone.channels[ i ] ) {
  153. case 'Xposition':
  154. keyframe.position.x = parseFloat( data.shift().trim() );
  155. break;
  156. case 'Yposition':
  157. keyframe.position.y = parseFloat( data.shift().trim() );
  158. break;
  159. case 'Zposition':
  160. keyframe.position.z = parseFloat( data.shift().trim() );
  161. break;
  162. case 'Xrotation':
  163. quat.setFromAxisAngle( vx, parseFloat( data.shift().trim() ) * Math.PI / 180 );
  164. keyframe.rotation.multiply( quat );
  165. break;
  166. case 'Yrotation':
  167. quat.setFromAxisAngle( vy, parseFloat( data.shift().trim() ) * Math.PI / 180 );
  168. keyframe.rotation.multiply( quat );
  169. break;
  170. case 'Zrotation':
  171. quat.setFromAxisAngle( vz, parseFloat( data.shift().trim() ) * Math.PI / 180 );
  172. keyframe.rotation.multiply( quat );
  173. break;
  174. default:
  175. console.warn( 'THREE.BVHLoader: Invalid channel type.' );
  176. }
  177. }
  178. // parse child nodes
  179. for ( let i = 0; i < bone.children.length; i ++ ) {
  180. readFrameData( data, frameTime, bone.children[ i ] );
  181. }
  182. }
  183. /*
  184. Recursively parses the HIERARCHY section of the BVH file
  185. - lines: all lines of the file. lines are consumed as we go along.
  186. - firstline: line containing the node type and name e.g. 'JOINT hip'
  187. - list: collects a flat list of nodes
  188. returns: a BVH node including children
  189. */
  190. function readNode( lines, firstline, list ) {
  191. const node = { name: '', type: '', frames: [] };
  192. list.push( node );
  193. // parse node type and name
  194. let tokens = firstline.split( /[\s]+/ );
  195. if ( tokens[ 0 ].toUpperCase() === 'END' && tokens[ 1 ].toUpperCase() === 'SITE' ) {
  196. node.type = 'ENDSITE';
  197. node.name = 'ENDSITE'; // bvh end sites have no name
  198. } else {
  199. node.name = tokens[ 1 ];
  200. node.type = tokens[ 0 ].toUpperCase();
  201. }
  202. if ( nextLine( lines ) !== '{' ) {
  203. console.error( 'THREE.BVHLoader: Expected opening { after type & name' );
  204. }
  205. // parse OFFSET
  206. tokens = nextLine( lines ).split( /[\s]+/ );
  207. if ( tokens[ 0 ] !== 'OFFSET' ) {
  208. console.error( 'THREE.BVHLoader: Expected OFFSET but got: ' + tokens[ 0 ] );
  209. }
  210. if ( tokens.length !== 4 ) {
  211. console.error( 'THREE.BVHLoader: Invalid number of values for OFFSET.' );
  212. }
  213. const offset = new Vector3(
  214. parseFloat( tokens[ 1 ] ),
  215. parseFloat( tokens[ 2 ] ),
  216. parseFloat( tokens[ 3 ] )
  217. );
  218. if ( isNaN( offset.x ) || isNaN( offset.y ) || isNaN( offset.z ) ) {
  219. console.error( 'THREE.BVHLoader: Invalid values of OFFSET.' );
  220. }
  221. node.offset = offset;
  222. // parse CHANNELS definitions
  223. if ( node.type !== 'ENDSITE' ) {
  224. tokens = nextLine( lines ).split( /[\s]+/ );
  225. if ( tokens[ 0 ] !== 'CHANNELS' ) {
  226. console.error( 'THREE.BVHLoader: Expected CHANNELS definition.' );
  227. }
  228. const numChannels = parseInt( tokens[ 1 ] );
  229. node.channels = tokens.splice( 2, numChannels );
  230. node.children = [];
  231. }
  232. // read children
  233. while ( true ) {
  234. const line = nextLine( lines );
  235. if ( line === '}' ) {
  236. return node;
  237. } else {
  238. node.children.push( readNode( lines, line, list ) );
  239. }
  240. }
  241. }
  242. /*
  243. recursively converts the internal bvh node structure to a Bone hierarchy
  244. source: the bvh root node
  245. list: pass an empty array, collects a flat list of all converted THREE.Bones
  246. returns the root Bone
  247. */
  248. function toTHREEBone( source, list ) {
  249. const bone = new Bone();
  250. list.push( bone );
  251. bone.position.add( source.offset );
  252. bone.name = source.name;
  253. if ( source.type !== 'ENDSITE' ) {
  254. for ( let i = 0; i < source.children.length; i ++ ) {
  255. bone.add( toTHREEBone( source.children[ i ], list ) );
  256. }
  257. }
  258. return bone;
  259. }
  260. /*
  261. builds an AnimationClip from the keyframe data saved in each bone.
  262. bone: bvh root node
  263. returns: an AnimationClip containing position and quaternion tracks
  264. */
  265. function toTHREEAnimation( bones ) {
  266. const tracks = [];
  267. // create a position and quaternion animation track for each node
  268. for ( let i = 0; i < bones.length; i ++ ) {
  269. const bone = bones[ i ];
  270. if ( bone.type === 'ENDSITE' )
  271. continue;
  272. // track data
  273. const times = [];
  274. const positions = [];
  275. const rotations = [];
  276. for ( let j = 0; j < bone.frames.length; j ++ ) {
  277. const frame = bone.frames[ j ];
  278. times.push( frame.time );
  279. // the animation system animates the position property,
  280. // so we have to add the joint offset to all values
  281. positions.push( frame.position.x + bone.offset.x );
  282. positions.push( frame.position.y + bone.offset.y );
  283. positions.push( frame.position.z + bone.offset.z );
  284. rotations.push( frame.rotation.x );
  285. rotations.push( frame.rotation.y );
  286. rotations.push( frame.rotation.z );
  287. rotations.push( frame.rotation.w );
  288. }
  289. if ( scope.animateBonePositions ) {
  290. tracks.push( new VectorKeyframeTrack( bone.name + '.position', times, positions ) );
  291. }
  292. if ( scope.animateBoneRotations ) {
  293. tracks.push( new QuaternionKeyframeTrack( bone.name + '.quaternion', times, rotations ) );
  294. }
  295. }
  296. return new AnimationClip( 'animation', - 1, tracks );
  297. }
  298. /*
  299. returns the next non-empty line in lines
  300. */
  301. function nextLine( lines ) {
  302. let line;
  303. // skip empty lines
  304. while ( ( line = lines.shift().trim() ).length === 0 ) { }
  305. return line;
  306. }
  307. const scope = this;
  308. const lines = text.split( /[\r\n]+/g );
  309. const bones = readBvh( lines );
  310. const threeBones = [];
  311. toTHREEBone( bones[ 0 ], threeBones );
  312. const threeClip = toTHREEAnimation( bones );
  313. return {
  314. skeleton: new Skeleton( threeBones ),
  315. clip: threeClip
  316. };
  317. }
  318. }
  319. export { BVHLoader };