1/** 2 * @author Kyle-Larson https://github.com/Kyle-Larson 3 * @author Takahiro https://github.com/takahirox 4 * @author Lewy Blue https://github.com/looeee 5 * 6 * Loader loads FBX file and generates Group representing FBX scene. 7 * Requires FBX file to be >= 7.0 and in ASCII or >= 6400 in Binary format 8 * Versions lower than this may load but will probably have errors 9 * 10 * Needs Support: 11 * Morph normals / blend shape normals 12 * 13 * FBX format references: 14 * https://wiki.blender.org/index.php/User:Mont29/Foundation/FBX_File_Structure 15 * http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_index_html (C++ SDK reference) 16 * 17 * Binary format specification: 18 * https://code.blender.org/2013/08/fbx-binary-file-format-specification/ 19 */ 20 21 22THREE.FBXLoader = ( function () { 23 24 var FBXTree; 25 var connections; 26 var sceneGraph; 27 28 function FBXLoader( manager ) { 29 30 this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; 31 32 } 33 34 FBXLoader.prototype = { 35 36 constructor: FBXLoader, 37 38 crossOrigin: 'anonymous', 39 40 load: function ( url, onLoad, onProgress, onError ) { 41 42 var self = this; 43 44 var resourceDirectory = THREE.LoaderUtils.extractUrlBase( url ); 45 46 var loader = new THREE.FileLoader( this.manager ); 47 loader.setResponseType( 'arraybuffer' ); 48 loader.load( url, function ( buffer ) { 49 50 try { 51 52 var scene = self.parse( buffer, resourceDirectory ); 53 onLoad( scene ); 54 55 } catch ( error ) { 56 57 setTimeout( function () { 58 59 if ( onError ) onError( error ); 60 61 self.manager.itemError( url ); 62 63 }, 0 ); 64 65 } 66 67 }, onProgress, onError ); 68 69 }, 70 71 setCrossOrigin: function ( value ) { 72 73 this.crossOrigin = value; 74 return this; 75 76 }, 77 78 parse: function ( FBXBuffer, resourceDirectory ) { 79 80 if ( isFbxFormatBinary( FBXBuffer ) ) { 81 82 FBXTree = new BinaryParser().parse( FBXBuffer ); 83 84 } else { 85 86 var FBXText = convertArrayBufferToString( FBXBuffer ); 87 88 if ( ! isFbxFormatASCII( FBXText ) ) { 89 90 throw new Error( 'THREE.FBXLoader: Unknown format.' ); 91 92 } 93 94 if ( getFbxVersion( FBXText ) < 7000 ) { 95 96 throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion( FBXText ) ); 97 98 } 99 100 FBXTree = new TextParser().parse( FBXText ); 101 102 } 103 104 //console.log( FBXTree ); 105 106 var textureLoader = new THREE.TextureLoader( this.manager ).setPath( resourceDirectory ).setCrossOrigin( this.crossOrigin ); 107 108 return new FBXTreeParser( textureLoader ).parse( FBXTree ); 109 110 } 111 112 }; 113 114 // Parse the FBXTree object returned by the BinaryParser or TextParser and return a THREE.Group 115 function FBXTreeParser( textureLoader ) { 116 117 this.textureLoader = textureLoader; 118 119 } 120 121 FBXTreeParser.prototype = { 122 123 constructor: FBXTreeParser, 124 125 parse: function () { 126 127 connections = this.parseConnections(); 128 129 var images = this.parseImages(); 130 var textures = this.parseTextures( images ); 131 var materials = this.parseMaterials( textures ); 132 var deformers = this.parseDeformers(); 133 var geometryMap = new GeometryParser().parse( deformers ); 134 135 this.parseScene( deformers, geometryMap, materials ); 136 137 return sceneGraph; 138 139 }, 140 141 // Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry ) 142 // and details the connection type 143 parseConnections: function () { 144 145 var connectionMap = new Map(); 146 147 if ( 'Connections' in FBXTree ) { 148 149 var rawConnections = FBXTree.Connections.connections; 150 151 rawConnections.forEach( function ( rawConnection ) { 152 153 var fromID = rawConnection[ 0 ]; 154 var toID = rawConnection[ 1 ]; 155 var relationship = rawConnection[ 2 ]; 156 157 if ( ! connectionMap.has( fromID ) ) { 158 159 connectionMap.set( fromID, { 160 parents: [], 161 children: [] 162 } ); 163 164 } 165 166 var parentRelationship = { ID: toID, relationship: relationship }; 167 connectionMap.get( fromID ).parents.push( parentRelationship ); 168 169 if ( ! connectionMap.has( toID ) ) { 170 171 connectionMap.set( toID, { 172 parents: [], 173 children: [] 174 } ); 175 176 } 177 178 var childRelationship = { ID: fromID, relationship: relationship }; 179 connectionMap.get( toID ).children.push( childRelationship ); 180 181 } ); 182 183 } 184 185 return connectionMap; 186 187 }, 188 189 // Parse FBXTree.Objects.Video for embedded image data 190 // These images are connected to textures in FBXTree.Objects.Textures 191 // via FBXTree.Connections. 192 parseImages: function () { 193 194 var images = {}; 195 var blobs = {}; 196 197 if ( 'Video' in FBXTree.Objects ) { 198 199 var videoNodes = FBXTree.Objects.Video; 200 201 for ( var nodeID in videoNodes ) { 202 203 var videoNode = videoNodes[ nodeID ]; 204 205 var id = parseInt( nodeID ); 206 207 images[ id ] = videoNode.RelativeFilename || videoNode.Filename; 208 209 // raw image data is in videoNode.Content 210 if ( 'Content' in videoNode ) { 211 212 var arrayBufferContent = ( videoNode.Content instanceof ArrayBuffer ) && ( videoNode.Content.byteLength > 0 ); 213 var base64Content = ( typeof videoNode.Content === 'string' ) && ( videoNode.Content !== '' ); 214 215 if ( arrayBufferContent || base64Content ) { 216 217 var image = this.parseImage( videoNodes[ nodeID ] ); 218 219 blobs[ videoNode.RelativeFilename || videoNode.Filename ] = image; 220 221 } 222 223 } 224 225 } 226 227 } 228 229 for ( var id in images ) { 230 231 var filename = images[ id ]; 232 233 if ( blobs[ filename ] !== undefined ) images[ id ] = blobs[ filename ]; 234 else images[ id ] = images[ id ].split( '\\' ).pop(); 235 236 } 237 238 return images; 239 240 }, 241 242 // Parse embedded image data in FBXTree.Video.Content 243 parseImage: function ( videoNode ) { 244 245 var content = videoNode.Content; 246 var fileName = videoNode.RelativeFilename || videoNode.Filename; 247 var extension = fileName.slice( fileName.lastIndexOf( '.' ) + 1 ).toLowerCase(); 248 249 var type; 250 251 switch ( extension ) { 252 253 case 'bmp': 254 255 type = 'image/bmp'; 256 break; 257
258 case 'jpg': 259 case 'jpeg': 260 261 type = 'image/jpeg'; 262 break; 263 264 case 'png': 265 266 type = 'image/png'; 267 break; 268 269 case 'tif': 270 271 type = 'image/tiff'; 272 break; 273 274 case 'tga': 275 276 if ( typeof THREE.TGALoader !== 'function' ) { 277 278 console.warn( 'FBXLoader: THREE.TGALoader is required to load TGA textures' ); 279 return; 280 281 } else { 282 283 if ( THREE.Loader.Handlers.get( '.tga' ) === null ) { 284 285 THREE.Loader.Handlers.add( /\.tga$/i, new THREE.TGALoader() ); 286 287 } 288 289 type = 'image/tga'; 290 break; 291 292 } 293 294 default: 295 296 console.warn( 'FBXLoader: Image type "' + extension + '" is not supported.' ); 297 return; 298 299 } 300 301 if ( typeof content === 'string' ) { // ASCII format 302 303 return 'data:' + type + ';base64,' + content; 304 305 } else { // Binary Format 306 307 var array = new Uint8Array( content ); 308 return window.URL.createObjectURL( new Blob( [ array ], { type: type } ) ); 309 310 } 311 312 }, 313 314 // Parse nodes in FBXTree.Objects.Texture 315 // These contain details such as UV scaling, cropping, rotation etc and are connected 316 // to images in FBXTree.Objects.Video 317 parseTextures: function ( images ) { 318 319 var textureMap = new Map(); 320 321 if ( 'Texture' in FBXTree.Objects ) { 322 323 var textureNodes = FBXTree.Objects.Texture; 324 for ( var nodeID in textureNodes ) { 325 326 var texture = this.parseTexture( textureNodes[ nodeID ], images ); 327 textureMap.set( parseInt( nodeID ), texture ); 328 329 } 330 331 } 332 333 return textureMap; 334 335 }, 336 337 // Parse individual node in FBXTree.Objects.Texture 338 parseTexture: function ( textureNode, images ) { 339 340 var texture = this.loadTexture( textureNode, images ); 341 342 texture.ID = textureNode.id; 343 344 texture.name = textureNode.attrName; 345 346 var wrapModeU = textureNode.WrapModeU; 347 var wrapModeV = textureNode.WrapModeV; 348 349 var valueU = wrapModeU !== undefined ? wrapModeU.value : 0; 350 var valueV = wrapModeV !== undefined ? wrapModeV.value : 0; 351 352 // http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a 353 // 0: repeat(default), 1: clamp 354 355 texture.wrapS = valueU === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping; 356 texture.wrapT = valueV === 0 ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping; 357 358 if ( 'Scaling' in textureNode ) { 359 360 var values = textureNode.Scaling.value; 361 362 texture.repeat.x = values[ 0 ]; 363 texture.repeat.y = values[ 1 ]; 364 365 } 366 367 return texture; 368 369 }, 370 371 // load a texture specified as a blob or data URI, or via an external URL using THREE.TextureLoader 372 loadTexture: function ( textureNode, images ) { 373 374 var fileName; 375 376 var currentPath = this.textureLoader.path; 377 378 var children = connections.get( textureNode.id ).children; 379 380 if ( children !== undefined && children.length > 0 && images[ children[ 0 ].ID ] !== undefined ) { 381 382 fileName = images[ children[ 0 ].ID ]; 383 384 if ( fileName.indexOf( 'blob:' ) === 0 || fileName.indexOf( 'data:' ) === 0 ) { 385 386 this.textureLoader.setPath( undefined ); 387 388 } 389 390 } 391 392 var texture; 393 394 if ( textureNode.FileName.slice( - 3 ).toLowerCase() === 'tga' ) { 395 396 texture = THREE.Loader.Handlers.get( '.tga' ).load( fileName ); 397 398 } else { 399 400 texture = this.textureLoader.load( fileName ); 401 402 } 403 404 this.textureLoader.setPath( currentPath ); 405 406 return texture; 407 408 }, 409 410 // Parse nodes in FBXTree.Objects.Material 411 parseMaterials: function ( textureMap ) { 412 413 var materialMap = new Map(); 414 415 if ( 'Material' in FBXTree.Objects ) { 416 417 var materialNodes = FBXTree.Objects.Material; 418 419 for ( var nodeID in materialNodes ) { 420 421 var material = this.parseMaterial( materialNodes[ nodeID ], textureMap ); 422 423 if ( material !== null ) materialMap.set( parseInt( nodeID ), material ); 424 425 } 426 427 } 428 429 return materialMap; 430 431 }, 432 433 // Parse single node in FBXTree.Objects.Material 434 // Materials are connected to texture maps in FBXTree.Objects.Textures 435 // FBX format currently only supports Lambert and Phong shading models 436 parseMaterial: function ( materialNode, textureMap ) { 437 438 var ID = materialNode.id; 439 var name = materialNode.attrName; 440 var type = materialNode.ShadingModel; 441 442 // Case where FBX wraps shading model in property object. 443 if ( typeof type === 'object' ) { 444 445 type = type.value; 446 447 } 448 449 // Ignore unused materials which don't have any connections. 450 if ( ! connections.has( ID ) ) return null; 451 452 var parameters = this.parseParameters( materialNode, textureMap, ID ); 453 454 var material; 455 456 switch ( type.toLowerCase() ) { 457 458 case 'phong': 459 material = new THREE.MeshPhongMaterial(); 460 break; 461 case 'lambert': 462 material = new THREE.MeshLambertMaterial(); 463 break; 464 default: 465 console.warn( 'THREE.FBXLoader: unknown material type "%s". Defaulting to MeshPhongMaterial.', type ); 466 material = new THREE.MeshPhongMaterial( { color: 0x3300ff } ); 467 break; 468 469 } 470 471 material.setValues( parameters ); 472 material.name = name; 473 474 return material; 475 476 }, 477 478 // Parse FBX material and return parameters suitable for a three.js material 479 // Also parse the texture map and return any textures associated with the material 480 parseParameters: function ( materialNode, textureMap, ID ) { 481 482 var parameters = {}; 483 484 if ( materialNode.BumpFactor ) { 485 486 parameters.bumpScale = materialNode.BumpFactor.value; 487 488 } 489 if ( materialNode.Diffuse ) { 490 491 parameters.color = new THREE.Color().fromArray( materialNode.Diffuse.value ); 492 493 } else if ( materialNode.DiffuseColor && materialNode.DiffuseColor.type === 'Color' ) { 494 495 // The blender exporter exports diffuse here instead of in materialNode.Diffuse 496 parameters.color = new THREE.Color().fromArray( materialNode.DiffuseColor.value ); 497 498 } 499 if ( materialNode.DisplacementFactor ) { 500 501 parameters.displacementScale = materialNode.DisplacementFactor.value; 502 503 } 504 if ( materialNode.Emissive ) { 505 506 parameters.emissive = new THREE.Color().fromArray( materialNode.Emissive.value ); 507 508 } else if ( materialNode.EmissiveColor && materialNode.EmissiveColor.type === 'Color' ) { 509 510 // The blender exporter exports emissive color here instead of in materialNode.Emissive 511 parameters.emissive = new THREE.Color().fromArray( materialNode.EmissiveColor.value ); 512 513 } 514 if ( materialNode.EmissiveFactor ) { 515 516 parameters.emissiveIntensity = parseFloat( materialNode.EmissiveFactor.value ); 517 518 } 519 if ( materialNode.Opacity ) { 520 521 parameters.opacity = parseFloat( materialNode.Opacity.value ); 522 523 } 524 if ( parameters.opacity < 1.0 ) { 525 526 parameters.transparent = true; 527 528 } 529 if ( materialNode.ReflectionFactor ) { 530 531 parameters.reflectivity = materialNode.ReflectionFactor.value; 532 533 } 534 if ( materialNode.Shininess ) { 535 536 parameters.shininess = materialNode.Shininess.value; 537 538 } 539 if ( materialNode.Specular ) { 540 541 parameters.specular = new THREE.Color().fromArray( materialNode.Specular.value ); 542 543 } else if ( materialNode.SpecularColor && materialNode.SpecularColor.type === 'Color' ) { 544 545 // The blender exporter exports specular color here instead of in materialNode.Specular 546 parameters.specular = new THREE.Color().fromArray( materialNode.SpecularColor.value ); 547 548 } 549 550 var self = this;
551 connections.get( ID ).children.forEach( function ( child ) { 552 553 var type = child.relationship; 554 555 switch ( type ) { 556 557 case 'Bump': 558 parameters.bumpMap = self.getTexture( textureMap, child.ID ); 559 break; 560 561 case 'DiffuseColor': 562 parameters.map = self.getTexture( textureMap, child.ID ); 563 break; 564 565 case 'DisplacementColor': 566 parameters.displacementMap = self.getTexture( textureMap, child.ID ); 567 break; 568 569 570 case 'EmissiveColor': 571 parameters.emissiveMap = self.getTexture( textureMap, child.ID ); 572 break; 573 574 case 'NormalMap': 575 parameters.normalMap = self.getTexture( textureMap, child.ID ); 576 break; 577 578 case 'ReflectionColor': 579 parameters.envMap = self.getTexture( textureMap, child.ID ); 580 parameters.envMap.mapping = THREE.EquirectangularReflectionMapping; 581 break; 582 583 case 'SpecularColor': 584 parameters.specularMap = self.getTexture( textureMap, child.ID ); 585 break; 586 587 case 'TransparentColor': 588 parameters.alphaMap = self.getTexture( textureMap, child.ID ); 589 parameters.transparent = true; 590 break; 591 592 case 'AmbientColor': 593 case 'ShininessExponent': // AKA glossiness map 594 case 'SpecularFactor': // AKA specularLevel 595 case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor 596 default: 597 console.warn( 'THREE.FBXLoader: %s map is not supported in three.js, skipping texture.', type ); 598 break; 599 600 } 601 602 } ); 603 604 return parameters; 605 606 }, 607 608 // get a texture from the textureMap for use by a material. 609 getTexture: function ( textureMap, id ) { 610 611 // if the texture is a layered texture, just use the first layer and issue a warning 612 if ( 'LayeredTexture' in FBXTree.Objects && id in FBXTree.Objects.LayeredTexture ) { 613 614 console.warn( 'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.' ); 615 id = connections.get( id ).children[ 0 ].ID; 616 617 } 618 619 return textureMap.get( id ); 620 621 }, 622 623 // Parse nodes in FBXTree.Objects.Deformer 624 // Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here 625 // Generates map of Skeleton-like objects for use later when generating and binding skeletons. 626 parseDeformers: function () { 627 628 var skeletons = {}; 629 var morphTargets = {}; 630 631 if ( 'Deformer' in FBXTree.Objects ) { 632 633 var DeformerNodes = FBXTree.Objects.Deformer; 634 635 for ( var nodeID in DeformerNodes ) { 636 637 var deformerNode = DeformerNodes[ nodeID ]; 638 639 var relationships = connections.get( parseInt( nodeID ) ); 640 641 if ( deformerNode.attrType === 'Skin' ) { 642 643 var skeleton = this.parseSkeleton( relationships, DeformerNodes ); 644 skeleton.ID = nodeID; 645 646 if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: skeleton attached to more than one geometry is not supported.' ); 647 skeleton.geometryID = relationships.parents[ 0 ].ID; 648 649 skeletons[ nodeID ] = skeleton; 650 651 } else if ( deformerNode.attrType === 'BlendShape' ) { 652 653 var morphTarget = { 654 id: nodeID, 655 }; 656 657 morphTarget.rawTargets = this.parseMorphTargets( relationships, DeformerNodes ); 658 morphTarget.id = nodeID; 659 660 if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: morph target attached to more than one geometry is not supported.' ); 661 662 morphTargets[ nodeID ] = morphTarget; 663 664 } 665 666 } 667 668 } 669 670 return { 671 672 skeletons: skeletons, 673 morphTargets: morphTargets, 674 675 }; 676 677 }, 678 679 // Parse single nodes in FBXTree.Objects.Deformer 680 // The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster' 681 // Each skin node represents a skeleton and each cluster node represents a bone 682 parseSkeleton: function ( relationships, deformerNodes ) { 683 684 var rawBones = []; 685 686 relationships.children.forEach( function ( child ) { 687 688 var boneNode = deformerNodes[ child.ID ]; 689 690 if ( boneNode.attrType !== 'Cluster' ) return; 691 692 var rawBone = { 693 694 ID: child.ID, 695 indices: [], 696 weights: [], 697 transform: new THREE.Matrix4().fromArray( boneNode.Transform.a ), 698 transformLink: new THREE.Matrix4().fromArray( boneNode.TransformLink.a ), 699 linkMode: boneNode.Mode, 700 701 }; 702
703 if ( 'Indexes' in boneNode ) { 704 705 rawBone.indices = boneNode.Indexes.a; 706 rawBone.weights = boneNode.Weights.a; 707 708 } 709 710 rawBones.push( rawBone ); 711 712 } ); 713 714 return { 715 716 rawBones: rawBones, 717 bones: [] 718 719 }; 720 721 }, 722 723 // The top level morph deformer node has type "BlendShape" and sub nodes have type "BlendShapeChannel" 724 parseMorphTargets: function ( relationships, deformerNodes ) { 725 726 var rawMorphTargets = []; 727 728 for ( var i = 0; i < relationships.children.length; i ++ ) { 729 730 if ( i === 8 ) { 731 732 console.warn( 'FBXLoader: maximum of 8 morph targets supported. Ignoring additional targets.' ); 733 734 break; 735 736 } 737 738 var child = relationships.children[ i ]; 739 740 var morphTargetNode = deformerNodes[ child.ID ]; 741 742 var rawMorphTarget = { 743 744 name: morphTargetNode.attrName, 745 initialWeight: morphTargetNode.DeformPercent, 746 id: morphTargetNode.id, 747 fullWeights: morphTargetNode.FullWeights.a 748 749 }; 750 751 if ( morphTargetNode.attrType !== 'BlendShapeChannel' ) return; 752 753 var targetRelationships = connections.get( parseInt( child.ID ) ); 754 755 targetRelationships.children.forEach( function ( child ) { 756 757 if ( child.relationship === undefined ) rawMorphTarget.geoID = child.ID; 758 759 } ); 760 761 rawMorphTargets.push( rawMorphTarget ); 762 763 } 764 765 return rawMorphTargets; 766 767 }, 768 769 // create the main THREE.Group() to be returned by the loader 770 parseScene: function ( deformers, geometryMap, materialMap ) { 771 772 sceneGraph = new THREE.Group(); 773 774 var modelMap = this.parseModels( deformers.skeletons, geometryMap, materialMap ); 775 776 var modelNodes = FBXTree.Objects.Model; 777 778 var self = this; 779 modelMap.forEach( function ( model ) { 780 781 var modelNode = modelNodes[ model.ID ]; 782 self.setLookAtProperties( model, modelNode ); 783 784 var parentConnections = connections.get( model.ID ).parents; 785 786 parentConnections.forEach( function ( connection ) { 787 788 var parent = modelMap.get( connection.ID ); 789 if ( parent !== undefined ) parent.add( model ); 790 791 } ); 792 793 if ( model.parent === null ) { 794 795 sceneGraph.add( model ); 796 797 } 798 799 800 } ); 801 802 this.bindSkeleton( deformers.skeletons, geometryMap, modelMap ); 803 804 this.createAmbientLight(); 805 806 this.setupMorphMaterials(); 807 808 var animations = new AnimationParser().parse(); 809 810 // if all the models where already combined in a single group, just return that 811 if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) { 812 813 sceneGraph.children[ 0 ].animations = animations; 814 sceneGraph = sceneGraph.children[ 0 ]; 815 816 } 817 818 sceneGraph.animations = animations; 819 820 }, 821 822 // parse nodes in FBXTree.Objects.Model 823 parseModels: function ( skeletons, geometryMap, materialMap ) { 824 825 var modelMap = new Map(); 826 var modelNodes = FBXTree.Objects.Model; 827 828 for ( var nodeID in modelNodes ) { 829 830 var id = parseInt( nodeID ); 831 var node = modelNodes[ nodeID ]; 832 var relationships = connections.get( id ); 833 834 var model = this.buildSkeleton( relationships, skeletons, id, node.attrName ); 835 836 if ( ! model ) { 837 838 switch ( node.attrType ) { 839 840 case 'Camera': 841 model = this.createCamera( relationships ); 842 break; 843 case 'Light': 844 model = this.createLight( relationships ); 845 break; 846 case 'Mesh': 847 model = this.createMesh( relationships, geometryMap, materialMap ); 848 break; 849 case 'NurbsCurve': 850 model = this.createCurve( relationships, geometryMap ); 851 break; 852 case 'LimbNode': // usually associated with a Bone, however if a Bone was not created we'll make a Group instead 853 case 'Null': 854 default: 855 model = new THREE.Group(); 856 break; 857 858 } 859 860 model.name = THREE.PropertyBinding.sanitizeNodeName( node.attrName ); 861 model.ID = id; 862 863 } 864 865 this.setModelTransforms( model, node ); 866 modelMap.set( id, model ); 867 868 } 869 870 return modelMap; 871 872 }, 873 874 buildSkeleton: function ( relationships, skeletons, id, name ) { 875 876 var bone = null; 877 878 relationships.parents.forEach( function ( parent ) { 879 880 for ( var ID in skeletons ) { 881 882 var skeleton = skeletons[ ID ]; 883
884 skeleton.rawBones.forEach( function ( rawBone, i ) { 885 886 if ( rawBone.ID === parent.ID ) { 887 888 var subBone = bone; 889 bone = new THREE.Bone(); 890 bone.matrixWorld.copy( rawBone.transformLink ); 891 892 // set name and id here - otherwise in cases where "subBone" is created it will not have a name / id 893 bone.name = THREE.PropertyBinding.sanitizeNodeName( name ); 894 bone.ID = id; 895 896 skeleton.bones[ i ] = bone; 897 898 // In cases where a bone is shared between multiple meshes 899 // duplicate the bone here and and it as a child of the first bone 900 if ( subBone !== null ) { 901 902 bone.add( subBone ); 903 904 } 905 906 } 907 908 } ); 909 910 } 911 912 } ); 913 914 return bone; 915 916 }, 917 918 // create a THREE.PerspectiveCamera or THREE.OrthographicCamera 919 createCamera: function ( relationships ) { 920 921 var model; 922 var cameraAttribute; 923 924 relationships.children.forEach( function ( child ) { 925 926 var attr = FBXTree.Objects.NodeAttribute[ child.ID ]; 927 928 if ( attr !== undefined ) { 929 930 cameraAttribute = attr; 931 932 } 933 934 } ); 935 936 if ( cameraAttribute === undefined ) { 937 938 model = new THREE.Object3D(); 939 940 } else { 941 942 var type = 0; 943 if ( cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1 ) { 944 945 type = 1; 946 947 } 948 949 var nearClippingPlane = 1; 950 if ( cameraAttribute.NearPlane !== undefined ) { 951 952 nearClippingPlane = cameraAttribute.NearPlane.value / 1000; 953 954 } 955 956 var farClippingPlane = 1000; 957 if ( cameraAttribute.FarPlane !== undefined ) { 958 959 farClippingPlane = cameraAttribute.FarPlane.value / 1000; 960 961 } 962 963 964 var width = window.innerWidth; 965 var height = window.innerHeight; 966 967 if ( cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined ) { 968 969 width = cameraAttribute.AspectWidth.value; 970 height = cameraAttribute.AspectHeight.value; 971 972 } 973 974 var aspect = width / height; 975 976 var fov = 45; 977 if ( cameraAttribute.FieldOfView !== undefined ) { 978 979 fov = cameraAttribute.FieldOfView.value; 980 981 } 982 983 var focalLength = cameraAttribute.FocalLength ? cameraAttribute.FocalLength.value : null; 984 985 switch ( type ) { 986 987 case 0: // Perspective 988 model = new THREE.PerspectiveCamera( fov, aspect, nearClippingPlane, farClippingPlane ); 989 if ( focalLength !== null ) model.setFocalLength( focalLength ); 990 break; 991 992 case 1: // Orthographic 993 model = new THREE.OrthographicCamera( - width / 2, width / 2, height / 2, - height / 2, nearClippingPlane, farClippingPlane ); 994 break; 995 996 default: 997 console.warn( 'THREE.FBXLoader: Unknown camera type ' + type + '.' ); 998 model = new THREE.Object3D(); 999 break; 1000 1001 } 1002 1003 } 1004 1005 return model; 1006 1007 }, 1008 1009 // Create a THREE.DirectionalLight, THREE.PointLight or THREE.SpotLight 1010 createLight: function ( relationships ) { 1011 1012 var model; 1013 var lightAttribute; 1014 1015 relationships.children.forEach( function ( child ) { 1016 1017 var attr = FBXTree.Objects.NodeAttribute[ child.ID ]; 1018 1019 if ( attr !== undefined ) { 1020 1021 lightAttribute = attr; 1022 1023 } 1024 1025 } ); 1026 1027 if ( lightAttribute === undefined ) { 1028 1029 model = new THREE.Object3D(); 1030 1031 } else { 1032 1033 var type; 1034 1035 // LightType can be undefined for Point lights 1036 if ( lightAttribute.LightType === undefined ) { 1037 1038 type = 0; 1039 1040 } else { 1041 1042 type = lightAttribute.LightType.value; 1043 1044 } 1045 1046 var color = 0xffffff; 1047 1048 if ( lightAttribute.Color !== undefined ) { 1049 1050 color = new THREE.Color().fromArray( lightAttribute.Color.value ); 1051 1052 } 1053 1054 var intensity = ( lightAttribute.Intensity === undefined ) ? 1 : lightAttribute.Intensity.value / 100; 1055 1056 // light disabled 1057 if ( lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0 ) { 1058 1059 intensity = 0; 1060 1061 } 1062 1063 var distance = 0; 1064 if ( lightAttribute.FarAttenuationEnd !== undefined ) { 1065 1066 if ( lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0 ) { 1067 1068 distance = 0; 1069 1070 } else { 1071 1072 distance = lightAttribute.FarAttenuationEnd.value; 1073 1074 } 1075 1076 } 1077 1078 // TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd? 1079 var decay = 1; 1080 1081 switch ( type ) { 1082 1083 case 0: // Point 1084 model = new THREE.PointLight( color, intensity, distance, decay ); 1085 break; 1086 1087 case 1: // Directional 1088 model = new THREE.DirectionalLight( color, intensity ); 1089 break; 1090 1091 case 2: // Spot 1092 var angle = Math.PI / 3; 1093 1094 if ( lightAttribute.InnerAngle !== undefined ) { 1095 1096 angle = THREE.Math.degToRad( lightAttribute.InnerAngle.value ); 1097 1098 } 1099 1100 var penumbra = 0; 1101 if ( lightAttribute.OuterAngle !== undefined ) { 1102 1103 // TODO: this is not correct - FBX calculates outer and inner angle in degrees 1104 // with OuterAngle > InnerAngle && OuterAngle <= Math.PI 1105 // while three.js uses a penumbra between (0, 1) to attenuate the inner angle 1106 penumbra = THREE.Math.degToRad( lightAttribute.OuterAngle.value ); 1107 penumbra = Math.max( penumbra, 1 ); 1108 1109 } 1110 1111 model = new THREE.SpotLight( color, intensity, distance, angle, penumbra, decay ); 1112 break; 1113 1114 default: 1115 console.warn( 'THREE.FBXLoader: Unknown light type ' + lightAttribute.LightType.value + ', defaulting to a THREE.PointLight.' ); 1116 model = new THREE.PointLight( color, intensity ); 1117 break; 1118 1119 } 1120 1121 if ( lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1 ) { 1122 1123 model.castShadow = true; 1124 1125 } 1126 1127 } 1128 1129 return model; 1130 1131 }, 1132 1133 createMesh: function ( relationships, geometryMap, materialMap ) { 1134 1135 var model; 1136 var geometry = null; 1137 var material = null; 1138 var materials = []; 1139 1140 // get geometry and materials(s) from connections
1141 relationships.children.forEach( function ( child ) { 1142 1143 if ( geometryMap.has( child.ID ) ) { 1144 1145 geometry = geometryMap.get( child.ID ); 1146 1147 } 1148 1149 if ( materialMap.has( child.ID ) ) { 1150 1151 materials.push( materialMap.get( child.ID ) ); 1152 1153 } 1154 1155 } ); 1156 1157 if ( materials.length > 1 ) { 1158 1159 material = materials; 1160 1161 } else if ( materials.length > 0 ) { 1162 1163 material = materials[ 0 ]; 1164 1165 } else { 1166 1167 material = new THREE.MeshPhongMaterial( { color: 0xcccccc } ); 1168 materials.push( material ); 1169 1170 } 1171 1172 if ( 'color' in geometry.attributes ) { 1173 1174 materials.forEach( function ( material ) { 1175 1176 material.vertexColors = THREE.VertexColors; 1177 1178 } ); 1179 1180 } 1181 1182 if ( geometry.FBX_Deformer ) { 1183 1184 materials.forEach( function ( material ) { 1185 1186 material.skinning = true; 1187 1188 } ); 1189 1190 model = new THREE.SkinnedMesh( geometry, material ); 1191 1192 } else { 1193 1194 model = new THREE.Mesh( geometry, material ); 1195 1196 } 1197 1198 return model; 1199 1200 }, 1201 1202 createCurve: function ( relationships, geometryMap ) { 1203 1204 var geometry = relationships.children.reduce( function ( geo, child ) { 1205 1206 if ( geometryMap.has( child.ID ) ) geo = geometryMap.get( child.ID ); 1207 1208 return geo; 1209 1210 }, null ); 1211 1212 // FBX does not list materials for Nurbs lines, so we'll just put our own in here. 1213 var material = new THREE.LineBasicMaterial( { color: 0x3300ff, linewidth: 1 } ); 1214 return new THREE.Line( geometry, material ); 1215 1216 }, 1217 1218 // parse the model node for transform details and apply them to the model 1219 setModelTransforms: function ( model, modelNode ) { 1220 1221 var transformData = {}; 1222 1223 if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = parseInt( modelNode.RotationOrder.value ); 1224 if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value; 1225 if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value; 1226 if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value; 1227 if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value; 1228 if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value; 1229 if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value; 1230 1231 var transform = generateTransform( transformData ); 1232 1233 model.applyMatrix( transform ); 1234 1235 }, 1236 1237 setLookAtProperties: function ( model, modelNode ) { 1238 1239 if ( 'LookAtProperty' in modelNode ) { 1240 1241 var children = connections.get( model.ID ).children; 1242 1243 children.forEach( function ( child ) { 1244 1245 if ( child.relationship === 'LookAtProperty' ) { 1246 1247 var lookAtTarget = FBXTree.Objects.Model[ child.ID ]; 1248 1249 if ( 'Lcl_Translation' in lookAtTarget ) { 1250 1251 var pos = lookAtTarget.Lcl_Translation.value; 1252 1253 // DirectionalLight, SpotLight 1254 if ( model.target !== undefined ) { 1255 1256 model.target.position.fromArray( pos ); 1257 sceneGraph.add( model.target ); 1258 1259 } else { // Cameras and other Object3Ds 1260 1261 model.lookAt( new THREE.Vector3().fromArray( pos ) ); 1262 1263 } 1264 1265 } 1266 1267 } 1268 1269 } ); 1270 1271 } 1272 1273 }, 1274 1275 bindSkeleton: function ( skeletons, geometryMap, modelMap ) { 1276 1277 var bindMatrices = this.parsePoseNodes(); 1278 1279 for ( var ID in skeletons ) { 1280 1281 var skeleton = skeletons[ ID ]; 1282 1283 var parents = connections.get( parseInt( skeleton.ID ) ).parents; 1284 1285 parents.forEach( function ( parent ) { 1286 1287 if ( geometryMap.has( parent.ID ) ) { 1288 1289 var geoID = parent.ID; 1290 var geoRelationships = connections.get( geoID ); 1291 1292 geoRelationships.parents.forEach( function ( geoConnParent ) { 1293 1294 if ( modelMap.has( geoConnParent.ID ) ) { 1295 1296 var model = modelMap.get( geoConnParent.ID ); 1297 1298 model.bind( new THREE.Skeleton( skeleton.bones ), bindMatrices[ geoConnParent.ID ] ); 1299 1300 } 1301 1302 } ); 1303 1304 } 1305 1306 } ); 1307 1308 } 1309 1310 }, 1311 1312 parsePoseNodes: function () { 1313 1314 var bindMatrices = {}; 1315 1316 if ( 'Pose' in FBXTree.Objects ) { 1317 1318 var BindPoseNode = FBXTree.Objects.Pose; 1319 1320 for ( var nodeID in BindPoseNode ) { 1321 1322 if ( BindPoseNode[ nodeID ].attrType === 'BindPose' ) { 1323 1324 var poseNodes = BindPoseNode[ nodeID ].PoseNode; 1325 1326 if ( Array.isArray( poseNodes ) ) { 1327
1328 poseNodes.forEach( function ( poseNode ) { 1329 1330 bindMatrices[ poseNode.Node ] = new THREE.Matrix4().fromArray( poseNode.Matrix.a ); 1331 1332 } ); 1333 1334 } else { 1335 1336 bindMatrices[ poseNodes.Node ] = new THREE.Matrix4().fromArray( poseNodes.Matrix.a ); 1337 1338 } 1339 1340 } 1341 1342 } 1343 1344 } 1345 1346 return bindMatrices; 1347 1348 }, 1349 1350 // Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light 1351 createAmbientLight: function () { 1352 1353 if ( 'GlobalSettings' in FBXTree && 'AmbientColor' in FBXTree.GlobalSettings ) { 1354 1355 var ambientColor = FBXTree.GlobalSettings.AmbientColor.value; 1356 var r = ambientColor[ 0 ]; 1357 var g = ambientColor[ 1 ]; 1358 var b = ambientColor[ 2 ]; 1359 1360 if ( r !== 0 || g !== 0 || b !== 0 ) { 1361 1362 var color = new THREE.Color( r, g, b ); 1363 sceneGraph.add( new THREE.AmbientLight( color, 1 ) ); 1364 1365 } 1366 1367 } 1368 1369 }, 1370 1371 setupMorphMaterials: function () { 1372 1373 sceneGraph.traverse( function ( child ) { 1374 1375 if ( child.isMesh ) { 1376 1377 if ( child.geometry.morphAttributes.position || child.geometry.morphAttributes.normal ) { 1378 1379 var uuid = child.uuid; 1380 var matUuid = child.material.uuid; 1381 1382 // if a geometry has morph targets, it cannot share the material with other geometries 1383 var sharedMat = false; 1384 1385 sceneGraph.traverse( function ( child ) { 1386 1387 if ( child.isMesh ) { 1388 1389 if ( child.material.uuid === matUuid && child.uuid !== uuid ) sharedMat = true; 1390 1391 } 1392 1393 } ); 1394 1395 if ( sharedMat === true ) child.material = child.material.clone(); 1396 1397 child.material.morphTargets = true; 1398 1399 } 1400 1401 } 1402 1403 } ); 1404 1405 }, 1406 1407 }; 1408 1409 // parse Geometry data from FBXTree and return map of BufferGeometries 1410 function GeometryParser() {} 1411 1412 GeometryParser.prototype = { 1413 1414 constructor: GeometryParser, 1415 1416 // Parse nodes in FBXTree.Objects.Geometry 1417 parse: function ( deformers ) { 1418 1419 var geometryMap = new Map(); 1420 1421 if ( 'Geometry' in FBXTree.Objects ) { 1422 1423 var geoNodes = FBXTree.Objects.Geometry; 1424 1425 for ( var nodeID in geoNodes ) { 1426 1427 var relationships = connections.get( parseInt( nodeID ) ); 1428 var geo = this.parseGeometry( relationships, geoNodes[ nodeID ], deformers ); 1429 1430 geometryMap.set( parseInt( nodeID ), geo ); 1431 1432 } 1433 1434 } 1435 1436 return geometryMap; 1437 1438 }, 1439 1440 // Parse single node in FBXTree.Objects.Geometry 1441 parseGeometry: function ( relationships, geoNode, deformers ) { 1442 1443 switch ( geoNode.attrType ) { 1444 1445 case 'Mesh': 1446 return this.parseMeshGeometry( relationships, geoNode, deformers ); 1447 break; 1448 1449 case 'NurbsCurve': 1450 return this.parseNurbsGeometry( geoNode ); 1451 break; 1452 1453 } 1454 1455 }, 1456 1457 // Parse single node mesh geometry in FBXTree.Objects.Geometry 1458 parseMeshGeometry: function ( relationships, geoNode, deformers ) { 1459 1460 var skeletons = deformers.skeletons; 1461 var morphTargets = deformers.morphTargets; 1462 1463 var modelNodes = relationships.parents.map( function ( parent ) { 1464 1465 return FBXTree.Objects.Model[ parent.ID ]; 1466 1467 } ); 1468 1469 // don't create geometry if it is not associated with any models 1470 if ( modelNodes.length === 0 ) return; 1471 1472 var skeleton = relationships.children.reduce( function ( skeleton, child ) { 1473 1474 if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ]; 1475 1476 return skeleton; 1477 1478 }, null ); 1479 1480 var morphTarget = relationships.children.reduce( function ( morphTarget, child ) { 1481 1482 if ( morphTargets[ child.ID ] !== undefined ) morphTarget = morphTargets[ child.ID ]; 1483 1484 return morphTarget; 1485 1486 }, null ); 1487 1488 // TODO: if there is more than one model associated with the geometry, AND the models have 1489 // different geometric transforms, then this will cause problems 1490 // if ( modelNodes.length > 1 ) { } 1491 1492 // For now just assume one model and get the preRotations from that 1493 var modelNode = modelNodes[ 0 ]; 1494 1495 var transformData = {}; 1496 1497 if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = modelNode.RotationOrder.value; 1498 if ( 'GeometricTranslation' in modelNode ) transformData.translation = modelNode.GeometricTranslation.value; 1499 if ( 'GeometricRotation' in modelNode ) transformData.rotation = modelNode.GeometricRotation.value; 1500 if ( 'GeometricScaling' in modelNode ) transformData.scale = modelNode.GeometricScaling.value; 1501 1502 var transform = generateTransform( transformData ); 1503 1504 return this.genGeometry( geoNode, skeleton, morphTarget, transform ); 1505 1506 }, 1507 1508 // Generate a THREE.BufferGeometry from a node in FBXTree.Objects.Geometry 1509 genGeometry: function ( geoNode, skeleton, morphTarget, preTransform ) { 1510 1511 var geo = new THREE.BufferGeometry(); 1512 if ( geoNode.attrName ) geo.name = geoNode.attrName; 1513 1514 var geoInfo = this.parseGeoNode( geoNode, skeleton ); 1515 var buffers = this.genBuffers( geoInfo ); 1516 1517 var positionAttribute = new THREE.Float32BufferAttribute( buffers.vertex, 3 ); 1518 1519 preTransform.applyToBufferAttribute( positionAttribute ); 1520 1521 geo.addAttribute( 'position', positionAttribute ); 1522 1523 if ( buffers.colors.length > 0 ) { 1524 1525 geo.addAttribute( 'color', new THREE.Float32BufferAttribute( buffers.colors, 3 ) ); 1526 1527 } 1528 1529 if ( skeleton ) { 1530 1531 geo.addAttribute( 'skinIndex', new THREE.Uint16BufferAttribute( buffers.weightsIndices, 4 ) ); 1532 1533 geo.addAttribute( 'skinWeight', new THREE.Float32BufferAttribute( buffers.vertexWeights, 4 ) ); 1534 1535 // used later to bind the skeleton to the model 1536 geo.FBX_Deformer = skeleton; 1537 1538 } 1539 1540 if ( buffers.normal.length > 0 ) { 1541 1542 var normalAttribute = new THREE.Float32BufferAttribute( buffers.normal, 3 ); 1543 1544 var normalMatrix = new THREE.Matrix3().getNormalMatrix( preTransform ); 1545 normalMatrix.applyToBufferAttribute( normalAttribute ); 1546
1547 geo.addAttribute( 'normal', normalAttribute ); 1548 1549 } 1550 1551 buffers.uvs.forEach( function ( uvBuffer, i ) { 1552 1553 // subsequent uv buffers are called 'uv1', 'uv2', ... 1554 var name = 'uv' + ( i + 1 ).toString(); 1555 1556 // the first uv buffer is just called 'uv' 1557 if ( i === 0 ) { 1558 1559 name = 'uv'; 1560 1561 } 1562 1563 geo.addAttribute( name, new THREE.Float32BufferAttribute( buffers.uvs[ i ], 2 ) ); 1564 1565 } ); 1566 1567 if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { 1568 1569 // Convert the material indices of each vertex into rendering groups on the geometry. 1570 var prevMaterialIndex = buffers.materialIndex[ 0 ]; 1571 var startIndex = 0; 1572 1573 buffers.materialIndex.forEach( function ( currentIndex, i ) { 1574 1575 if ( currentIndex !== prevMaterialIndex ) { 1576 1577 geo.addGroup( startIndex, i - startIndex, prevMaterialIndex ); 1578 1579 prevMaterialIndex = currentIndex; 1580 startIndex = i; 1581 1582 } 1583 1584 } ); 1585 1586 // the loop above doesn't add the last group, do that here. 1587 if ( geo.groups.length > 0 ) { 1588 1589 var lastGroup = geo.groups[ geo.groups.length - 1 ]; 1590 var lastIndex = lastGroup.start + lastGroup.count; 1591 1592 if ( lastIndex !== buffers.materialIndex.length ) { 1593 1594 geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex ); 1595 1596 } 1597 1598 } 1599 1600 // case where there are multiple materials but the whole geometry is only 1601 // using one of them 1602 if ( geo.groups.length === 0 ) { 1603 1604 geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] ); 1605 1606 } 1607 1608 } 1609 1610 this.addMorphTargets( geo, geoNode, morphTarget, preTransform ); 1611 1612 return geo; 1613 1614 }, 1615 1616 parseGeoNode: function ( geoNode, skeleton ) { 1617 1618 var geoInfo = {}; 1619 1620 geoInfo.vertexPositions = ( geoNode.Vertices !== undefined ) ? geoNode.Vertices.a : []; 1621 geoInfo.vertexIndices = ( geoNode.PolygonVertexIndex !== undefined ) ? geoNode.PolygonVertexIndex.a : []; 1622 1623 if ( geoNode.LayerElementColor ) { 1624 1625 geoInfo.color = this.parseVertexColors( geoNode.LayerElementColor[ 0 ] ); 1626 1627 } 1628 1629 if ( geoNode.LayerElementMaterial ) { 1630 1631 geoInfo.material = this.parseMaterialIndices( geoNode.LayerElementMaterial[ 0 ] ); 1632 1633 } 1634 1635 if ( geoNode.LayerElementNormal ) { 1636 1637 geoInfo.normal = this.parseNormals( geoNode.LayerElementNormal[ 0 ] ); 1638 1639 } 1640 1641 if ( geoNode.LayerElementUV ) { 1642 1643 geoInfo.uv = []; 1644 1645 var i = 0; 1646 while ( geoNode.LayerElementUV[ i ] ) { 1647 1648 geoInfo.uv.push( this.parseUVs( geoNode.LayerElementUV[ i ] ) ); 1649 i ++; 1650 1651 } 1652 1653 } 1654 1655 geoInfo.weightTable = {}; 1656 1657 if ( skeleton !== null ) { 1658 1659 geoInfo.skeleton = skeleton; 1660 1661 skeleton.rawBones.forEach( function ( rawBone, i ) { 1662 1663 // loop over the bone's vertex indices and weights 1664 rawBone.indices.forEach( function ( index, j ) { 1665 1666 if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = []; 1667 1668 geoInfo.weightTable[ index ].push( { 1669 1670 id: i, 1671 weight: rawBone.weights[ j ], 1672 1673 } ); 1674 1675 } ); 1676 1677 } ); 1678 1679 } 1680 1681 return geoInfo; 1682 1683 }, 1684 1685 genBuffers: function ( geoInfo ) { 1686 1687 var buffers = { 1688 vertex: [], 1689 normal: [], 1690 colors: [], 1691 uvs: [], 1692 materialIndex: [], 1693 vertexWeights: [], 1694 weightsIndices: [], 1695 }; 1696 1697 var polygonIndex = 0; 1698 var faceLength = 0; 1699 var displayedWeightsWarning = false; 1700 1701 // these will hold data for a single face 1702 var facePositionIndexes = []; 1703 var faceNormals = []; 1704 var faceColors = []; 1705 var faceUVs = []; 1706 var faceWeights = []; 1707 var faceWeightIndices = []; 1708 1709 var self = this; 1710 geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) { 1711 1712 var endOfFace = false; 1713 1714 // Face index and vertex index arrays are combined in a single array 1715 // A cube with quad faces looks like this: 1716 // PolygonVertexIndex: *24 { 1717 // a: 0, 1, 3, -3, 2, 3, 5, -5, 4, 5, 7, -7, 6, 7, 1, -1, 1, 7, 5, -4, 6, 0, 2, -5 1718 // } 1719 // Negative numbers mark the end of a face - first face here is 0, 1, 3, -3 1720 // to find index of last vertex bit shift the index: ^ - 1 1721 if ( vertexIndex < 0 ) { 1722 1723 vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1 1724 endOfFace = true; 1725 1726 } 1727 1728 var weightIndices = []; 1729 var weights = []; 1730 1731 facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 ); 1732 1733 if ( geoInfo.color ) { 1734 1735 var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color ); 1736 1737 faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] ); 1738 1739 } 1740 1741 if ( geoInfo.skeleton ) { 1742 1743 if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) { 1744 1745 geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) { 1746 1747 weights.push( wt.weight ); 1748 weightIndices.push( wt.id ); 1749 1750 } ); 1751 1752 1753 } 1754 1755 if ( weights.length > 4 ) { 1756 1757 if ( ! displayedWeightsWarning ) { 1758 1759 console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' ); 1760 displayedWeightsWarning = true; 1761 1762 } 1763 1764 var wIndex = [ 0, 0, 0, 0 ]; 1765 var Weight = [ 0, 0, 0, 0 ]; 1766
1767 weights.forEach( function ( weight, weightIndex ) { 1768 1769 var currentWeight = weight; 1770 var currentIndex = weightIndices[ weightIndex ]; 1771 1772 Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) { 1773 1774 if ( currentWeight > comparedWeight ) { 1775 1776 comparedWeightArray[ comparedWeightIndex ] = currentWeight; 1777 currentWeight = comparedWeight; 1778 1779 var tmp = wIndex[ comparedWeightIndex ]; 1780 wIndex[ comparedWeightIndex ] = currentIndex; 1781 currentIndex = tmp; 1782 1783 } 1784 1785 } ); 1786 1787 } ); 1788 1789 weightIndices = wIndex; 1790 weights = Weight; 1791 1792 } 1793 1794 // if the weight array is shorter than 4 pad with 0s 1795 while ( weights.length < 4 ) { 1796 1797 weights.push( 0 ); 1798 weightIndices.push( 0 ); 1799 1800 } 1801 1802 for ( var i = 0; i < 4; ++ i ) { 1803 1804 faceWeights.push( weights[ i ] ); 1805 faceWeightIndices.push( weightIndices[ i ] ); 1806 1807 } 1808 1809 } 1810 1811 if ( geoInfo.normal ) { 1812 1813 var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal ); 1814 1815 faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] ); 1816 1817 } 1818 1819 if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { 1820 1821 var materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ]; 1822 1823 } 1824 1825 if ( geoInfo.uv ) { 1826 1827 geoInfo.uv.forEach( function ( uv, i ) { 1828 1829 var data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv ); 1830 1831 if ( faceUVs[ i ] === undefined ) { 1832 1833 faceUVs[ i ] = []; 1834 1835 } 1836 1837 faceUVs[ i ].push( data[ 0 ] ); 1838 faceUVs[ i ].push( data[ 1 ] ); 1839 1840 } ); 1841 1842 } 1843 1844 faceLength ++; 1845 1846 if ( endOfFace ) { 1847 1848 self.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ); 1849 1850 polygonIndex ++; 1851 faceLength = 0; 1852 1853 // reset arrays for the next face 1854 facePositionIndexes = []; 1855 faceNormals = []; 1856 faceColors = []; 1857 faceUVs = []; 1858 faceWeights = []; 1859 faceWeightIndices = []; 1860 1861 } 1862 1863 } ); 1864 1865 return buffers; 1866 1867 }, 1868 1869 // Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris 1870 genFace: function ( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) { 1871 1872 for ( var i = 2; i < faceLength; i ++ ) { 1873 1874 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] ); 1875 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] ); 1876 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] ); 1877 1878 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] ); 1879 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] ); 1880 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] ); 1881 1882 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] ); 1883 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] ); 1884 buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] ); 1885 1886 if ( geoInfo.skeleton ) { 1887 1888 buffers.vertexWeights.push( faceWeights[ 0 ] ); 1889 buffers.vertexWeights.push( faceWeights[ 1 ] ); 1890 buffers.vertexWeights.push( faceWeights[ 2 ] ); 1891 buffers.vertexWeights.push( faceWeights[ 3 ] ); 1892 1893 buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] ); 1894 buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] ); 1895 buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] ); 1896 buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] ); 1897 1898 buffers.vertexWeights.push( faceWeights[ i * 4 ] ); 1899 buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] ); 1900 buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] ); 1901 buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] ); 1902 1903 buffers.weightsIndices.push( faceWeightIndices[ 0 ] ); 1904 buffers.weightsIndices.push( faceWeightIndices[ 1 ] ); 1905 buffers.weightsIndices.push( faceWeightIndices[ 2 ] ); 1906 buffers.weightsIndices.push( faceWeightIndices[ 3 ] ); 1907 1908 buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] ); 1909 buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] ); 1910 buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] ); 1911 buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] ); 1912 1913 buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] ); 1914 buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] ); 1915 buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] ); 1916 buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] ); 1917 1918 } 1919 1920 if ( geoInfo.color ) { 1921 1922 buffers.colors.push( faceColors[ 0 ] ); 1923 buffers.colors.push( faceColors[ 1 ] ); 1924 buffers.colors.push( faceColors[ 2 ] ); 1925 1926 buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] ); 1927 buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] ); 1928 buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] ); 1929 1930 buffers.colors.push( faceColors[ i * 3 ] ); 1931 buffers.colors.push( faceColors[ i * 3 + 1 ] ); 1932 buffers.colors.push( faceColors[ i * 3 + 2 ] ); 1933 1934 } 1935 1936 if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) { 1937 1938 buffers.materialIndex.push( materialIndex ); 1939 buffers.materialIndex.push( materialIndex ); 1940 buffers.materialIndex.push( materialIndex ); 1941 1942 } 1943 1944 if ( geoInfo.normal ) { 1945 1946 buffers.normal.push( faceNormals[ 0 ] ); 1947 buffers.normal.push( faceNormals[ 1 ] ); 1948 buffers.normal.push( faceNormals[ 2 ] ); 1949 1950 buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] ); 1951 buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] ); 1952 buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] ); 1953 1954 buffers.normal.push( faceNormals[ i * 3 ] ); 1955 buffers.normal.push( faceNormals[ i * 3 + 1 ] ); 1956 buffers.normal.push( faceNormals[ i * 3 + 2 ] ); 1957 1958 } 1959 1960 if ( geoInfo.uv ) { 1961 1962 geoInfo.uv.forEach( function ( uv, j ) { 1963 1964 if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = []; 1965 1966 buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] ); 1967 buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] ); 1968
1969 buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] ); 1970 buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] ); 1971 1972 buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] ); 1973 buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] ); 1974 1975 } ); 1976 1977 } 1978 1979 } 1980 1981 }, 1982 1983 addMorphTargets: function ( parentGeo, parentGeoNode, morphTarget, preTransform ) { 1984 1985 if ( morphTarget === null ) return; 1986 1987 parentGeo.morphAttributes.position = []; 1988 parentGeo.morphAttributes.normal = []; 1989 1990 var self = this; 1991 morphTarget.rawTargets.forEach( function ( rawTarget ) { 1992 1993 var morphGeoNode = FBXTree.Objects.Geometry[ rawTarget.geoID ]; 1994 1995 if ( morphGeoNode !== undefined ) { 1996 1997 self.genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform ); 1998 1999 } 2000 2001 } ); 2002 2003 }, 2004 2005 // a morph geometry node is similar to a standard node, and the node is also contained 2006 // in FBXTree.Objects.Geometry, however it can only have attributes for position, normal 2007 // and a special attribute Index defining which vertices of the original geometry are affected 2008 // Normal and position attributes only have data for the vertices that are affected by the morph 2009 genMorphGeometry: function ( parentGeo, parentGeoNode, morphGeoNode, preTransform ) { 2010 2011 var morphGeo = new THREE.BufferGeometry(); 2012 if ( morphGeoNode.attrName ) morphGeo.name = morphGeoNode.attrName; 2013 2014 var vertexIndices = ( parentGeoNode.PolygonVertexIndex !== undefined ) ? parentGeoNode.PolygonVertexIndex.a : []; 2015 2016 // make a copy of the parent's vertex positions 2017 var vertexPositions = ( parentGeoNode.Vertices !== undefined ) ? parentGeoNode.Vertices.a.slice() : []; 2018 2019 var morphPositions = ( morphGeoNode.Vertices !== undefined ) ? morphGeoNode.Vertices.a : []; 2020 var indices = ( morphGeoNode.Indexes !== undefined ) ? morphGeoNode.Indexes.a : []; 2021 2022 for ( var i = 0; i < indices.length; i ++ ) { 2023 2024 var morphIndex = indices[ i ] * 3; 2025 2026 // FBX format uses blend shapes rather than morph targets. This can be converted 2027 // by additively combining the blend shape positions with the original geometry's positions 2028 vertexPositions[ morphIndex ] += morphPositions[ i * 3 ]; 2029 vertexPositions[ morphIndex + 1 ] += morphPositions[ i * 3 + 1 ]; 2030 vertexPositions[ morphIndex + 2 ] += morphPositions[ i * 3 + 2 ]; 2031 2032 } 2033 2034 // TODO: add morph normal support 2035 var morphGeoInfo = { 2036 vertexIndices: vertexIndices, 2037 vertexPositions: vertexPositions, 2038 }; 2039 2040 var morphBuffers = this.genBuffers( morphGeoInfo ); 2041 2042 var positionAttribute = new THREE.Float32BufferAttribute( morphBuffers.vertex, 3 ); 2043 positionAttribute.name = morphGeoNode.attrName; 2044 2045 preTransform.applyToBufferAttribute( positionAttribute ); 2046 2047 parentGeo.morphAttributes.position.push( positionAttribute ); 2048 2049 }, 2050 2051 // Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists 2052 parseNormals: function ( NormalNode ) { 2053 2054 var mappingType = NormalNode.MappingInformationType; 2055 var referenceType = NormalNode.ReferenceInformationType; 2056 var buffer = NormalNode.Normals.a; 2057 var indexBuffer = []; 2058 if ( referenceType === 'IndexToDirect' ) { 2059 2060 if ( 'NormalIndex' in NormalNode ) { 2061 2062 indexBuffer = NormalNode.NormalIndex.a; 2063 2064 } else if ( 'NormalsIndex' in NormalNode ) { 2065 2066 indexBuffer = NormalNode.NormalsIndex.a; 2067 2068 } 2069 2070 } 2071 2072 return { 2073 dataSize: 3, 2074 buffer: buffer, 2075 indices: indexBuffer, 2076 mappingType: mappingType, 2077 referenceType: referenceType 2078 }; 2079 2080 }, 2081 2082 // Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists 2083 parseUVs: function ( UVNode ) { 2084 2085 var mappingType = UVNode.MappingInformationType; 2086 var referenceType = UVNode.ReferenceInformationType; 2087 var buffer = UVNode.UV.a; 2088 var indexBuffer = []; 2089 if ( referenceType === 'IndexToDirect' ) { 2090 2091 indexBuffer = UVNode.UVIndex.a; 2092 2093 } 2094 2095 return { 2096 dataSize: 2, 2097 buffer: buffer, 2098 indices: indexBuffer, 2099 mappingType: mappingType, 2100 referenceType: referenceType 2101 }; 2102 2103 }, 2104 2105 // Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists 2106 parseVertexColors: function ( ColorNode ) { 2107 2108 var mappingType = ColorNode.MappingInformationType; 2109 var referenceType = ColorNode.ReferenceInformationType; 2110 var buffer = ColorNode.Colors.a; 2111 var indexBuffer = []; 2112 if ( referenceType === 'IndexToDirect' ) { 2113 2114 indexBuffer = ColorNode.ColorIndex.a; 2115 2116 } 2117 2118 return { 2119 dataSize: 4, 2120 buffer: buffer, 2121 indices: indexBuffer, 2122 mappingType: mappingType, 2123 referenceType: referenceType 2124 }; 2125 2126 }, 2127 2128 // Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists 2129 parseMaterialIndices: function ( MaterialNode ) { 2130 2131 var mappingType = MaterialNode.MappingInformationType; 2132 var referenceType = MaterialNode.ReferenceInformationType; 2133 2134 if ( mappingType === 'NoMappingInformation' ) { 2135 2136 return { 2137 dataSize: 1, 2138 buffer: [ 0 ], 2139 indices: [ 0 ], 2140 mappingType: 'AllSame', 2141 referenceType: referenceType 2142 }; 2143 2144 } 2145 2146 var materialIndexBuffer = MaterialNode.Materials.a; 2147 2148 // Since materials are stored as indices, there's a bit of a mismatch between FBX and what 2149 // we expect.So we create an intermediate buffer that points to the index in the buffer, 2150 // for conforming with the other functions we've written for other data. 2151 var materialIndices = []; 2152 2153 for ( var i = 0; i < materialIndexBuffer.length; ++ i ) { 2154 2155 materialIndices.push( i ); 2156 2157 } 2158 2159 return { 2160 dataSize: 1, 2161 buffer: materialIndexBuffer, 2162 indices: materialIndices, 2163 mappingType: mappingType, 2164 referenceType: referenceType 2165 }; 2166 2167 }, 2168 2169 // Generate a NurbGeometry from a node in FBXTree.Objects.Geometry 2170 parseNurbsGeometry: function ( geoNode ) { 2171 2172 if ( THREE.NURBSCurve === undefined ) { 2173 2174 console.error( 'THREE.FBXLoader: The loader relies on THREE.NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.' ); 2175 return new THREE.BufferGeometry(); 2176 2177 } 2178 2179 var order = parseInt( geoNode.Order ); 2180 2181 if ( isNaN( order ) ) { 2182 2183 console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id ); 2184 return new THREE.BufferGeometry(); 2185 2186 } 2187 2188 var degree = order - 1; 2189 2190 var knots = geoNode.KnotVector.a; 2191 var controlPoints = []; 2192 var pointsValues = geoNode.Points.a; 2193 2194 for ( var i = 0, l = pointsValues.length; i < l; i += 4 ) { 2195 2196 controlPoints.push( new THREE.Vector4().fromArray( pointsValues, i ) ); 2197 2198 } 2199 2200 var startKnot, endKnot; 2201 2202 if ( geoNode.Form === 'Closed' ) { 2203 2204 controlPoints.push( controlPoints[ 0 ] ); 2205 2206 } else if ( geoNode.Form === 'Periodic' ) { 2207 2208 startKnot = degree; 2209 endKnot = knots.length - 1 - startKnot; 2210 2211 for ( var i = 0; i < degree; ++ i ) { 2212 2213 controlPoints.push( controlPoints[ i ] ); 2214 2215 } 2216 2217 } 2218 2219 var curve = new THREE.NURBSCurve( degree, knots, controlPoints, startKnot, endKnot ); 2220 var vertices = curve.getPoints( controlPoints.length * 7 ); 2221 2222 var positions = new Float32Array( vertices.length * 3 ); 2223
2224 vertices.forEach( function ( vertex, i ) { 2225 2226 vertex.toArray( positions, i * 3 ); 2227 2228 } ); 2229 2230 var geometry = new THREE.BufferGeometry(); 2231 geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); 2232 2233 return geometry; 2234 2235 }, 2236 2237 }; 2238 2239 // parse animation data from FBXTree 2240 function AnimationParser() {} 2241 2242 AnimationParser.prototype = { 2243 2244 constructor: AnimationParser, 2245 2246 // take raw animation clips and turn them into three.js animation clips 2247 parse: function () { 2248 2249 var animationClips = []; 2250 2251 2252 var rawClips = this.parseClips(); 2253 2254 if ( rawClips === undefined ) return; 2255 2256 for ( var key in rawClips ) { 2257 2258 var rawClip = rawClips[ key ]; 2259 2260 var clip = this.addClip( rawClip ); 2261 2262 animationClips.push( clip ); 2263 2264 } 2265 2266 return animationClips; 2267 2268 }, 2269 2270 parseClips: function () { 2271 2272 // since the actual transformation data is stored in FBXTree.Objects.AnimationCurve, 2273 // if this is undefined we can safely assume there are no animations 2274 if ( FBXTree.Objects.AnimationCurve === undefined ) return undefined; 2275 2276 var curveNodesMap = this.parseAnimationCurveNodes(); 2277 2278 this.parseAnimationCurves( curveNodesMap ); 2279 2280 var layersMap = this.parseAnimationLayers( curveNodesMap ); 2281 var rawClips = this.parseAnimStacks( layersMap ); 2282 2283 return rawClips; 2284 2285 }, 2286 2287 // parse nodes in FBXTree.Objects.AnimationCurveNode 2288 // each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation ) 2289 // and is referenced by an AnimationLayer 2290 parseAnimationCurveNodes: function () { 2291 2292 var rawCurveNodes = FBXTree.Objects.AnimationCurveNode; 2293 2294 var curveNodesMap = new Map(); 2295 2296 for ( var nodeID in rawCurveNodes ) { 2297 2298 var rawCurveNode = rawCurveNodes[ nodeID ]; 2299 2300 if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) { 2301 2302 var curveNode = { 2303 2304 id: rawCurveNode.id, 2305 attr: rawCurveNode.attrName, 2306 curves: {}, 2307 2308 }; 2309 2310 curveNodesMap.set( curveNode.id, curveNode ); 2311 2312 } 2313 2314 } 2315 2316 return curveNodesMap; 2317 2318 }, 2319 2320 // parse nodes in FBXTree.Objects.AnimationCurve and connect them up to 2321 // previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated 2322 // axis ( e.g. times and values of x rotation) 2323 parseAnimationCurves: function ( curveNodesMap ) { 2324 2325 var rawCurves = FBXTree.Objects.AnimationCurve; 2326 2327 // TODO: Many values are identical up to roundoff error, but won't be optimised 2328 // e.g. position times: [0, 0.4, 0. 8] 2329 // position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809] 2330 // clearly, this should be optimised to 2331 // times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809] 2332 // this shows up in nearly every FBX file, and generally time array is length > 100 2333 2334 for ( var nodeID in rawCurves ) { 2335 2336 var animationCurve = { 2337 2338 id: rawCurves[ nodeID ].id, 2339 times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ), 2340 values: rawCurves[ nodeID ].KeyValueFloat.a, 2341 2342 }; 2343 2344 var relationships = connections.get( animationCurve.id ); 2345 2346 if ( relationships !== undefined ) { 2347 2348 var animationCurveID = relationships.parents[ 0 ].ID; 2349 var animationCurveRelationship = relationships.parents[ 0 ].relationship; 2350 2351 if ( animationCurveRelationship.match( /X/ ) ) { 2352 2353 curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve; 2354 2355 } else if ( animationCurveRelationship.match( /Y/ ) ) { 2356 2357 curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve; 2358 2359 } else if ( animationCurveRelationship.match( /Z/ ) ) { 2360 2361 curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve; 2362 2363 } else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) { 2364 2365 curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve; 2366 2367 } 2368 2369 } 2370 2371 } 2372 2373 }, 2374 2375 // parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references 2376 // to various AnimationCurveNodes and is referenced by an AnimationStack node 2377 // note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack 2378 parseAnimationLayers: function ( curveNodesMap ) { 2379 2380 var rawLayers = FBXTree.Objects.AnimationLayer; 2381 2382 var layersMap = new Map(); 2383 2384 for ( var nodeID in rawLayers ) { 2385 2386 var layerCurveNodes = []; 2387 2388 var connection = connections.get( parseInt( nodeID ) ); 2389 2390 if ( connection !== undefined ) { 2391 2392 // all the animationCurveNodes used in the layer 2393 var children = connection.children; 2394 2395 var self = this;
2396 children.forEach( function ( child, i ) { 2397 2398 if ( curveNodesMap.has( child.ID ) ) { 2399 2400 var curveNode = curveNodesMap.get( child.ID ); 2401 2402 // check that the curves are defined for at least one axis, otherwise ignore the curveNode 2403 if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) { 2404 2405 if ( layerCurveNodes[ i ] === undefined ) { 2406 2407 var modelID; 2408 2409 connections.get( child.ID ).parents.forEach( function ( parent ) { 2410 2411 if ( parent.relationship !== undefined ) modelID = parent.ID; 2412 2413 } ); 2414 2415 var rawModel = FBXTree.Objects.Model[ modelID.toString() ]; 2416 2417 var node = { 2418 2419 modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ), 2420 initialPosition: [ 0, 0, 0 ], 2421 initialRotation: [ 0, 0, 0 ], 2422 initialScale: [ 1, 1, 1 ], 2423 transform: self.getModelAnimTransform( rawModel ), 2424 2425 }; 2426 2427 // if the animated model is pre rotated, we'll have to apply the pre rotations to every 2428 // animation value as well 2429 if ( 'PreRotation' in rawModel ) node.preRotations = rawModel.PreRotation.value; 2430 if ( 'PostRotation' in rawModel ) node.postRotations = rawModel.PostRotation.value; 2431 2432 layerCurveNodes[ i ] = node; 2433 2434 } 2435 2436 layerCurveNodes[ i ][ curveNode.attr ] = curveNode; 2437 2438 } else if ( curveNode.curves.morph !== undefined ) { 2439 2440 if ( layerCurveNodes[ i ] === undefined ) { 2441 2442 var deformerID; 2443 2444 connections.get( child.ID ).parents.forEach( function ( parent ) { 2445 2446 if ( parent.relationship !== undefined ) deformerID = parent.ID; 2447 2448 } ); 2449 2450 var morpherID = connections.get( deformerID ).parents[ 0 ].ID; 2451 var geoID = connections.get( morpherID ).parents[ 0 ].ID; 2452 2453 // assuming geometry is not used in more than one model 2454 var modelID = connections.get( geoID ).parents[ 0 ].ID; 2455 2456 var rawModel = FBXTree.Objects.Model[ modelID ]; 2457 2458 var node = { 2459 2460 modelName: THREE.PropertyBinding.sanitizeNodeName( rawModel.attrName ), 2461 morphName: FBXTree.Objects.Deformer[ deformerID ].attrName, 2462 2463 }; 2464 2465 layerCurveNodes[ i ] = node; 2466 2467 } 2468 2469 layerCurveNodes[ i ][ curveNode.attr ] = curveNode; 2470 2471 } 2472 2473 } 2474 2475 } ); 2476 2477 layersMap.set( parseInt( nodeID ), layerCurveNodes ); 2478 2479 } 2480 2481 } 2482 2483 return layersMap; 2484 2485 }, 2486 2487 getModelAnimTransform: function ( modelNode ) { 2488 2489 var transformData = {}; 2490 2491 if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = parseInt( modelNode.RotationOrder.value ); 2492 2493 if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value; 2494 if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value; 2495 2496 if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value; 2497 if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value; 2498 2499 if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value; 2500 2501 if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value; 2502 2503 return generateTransform( transformData ); 2504 2505 }, 2506 2507 // parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation 2508 // hierarchy. Each Stack node will be used to create a THREE.AnimationClip 2509 parseAnimStacks: function ( layersMap ) { 2510 2511 var rawStacks = FBXTree.Objects.AnimationStack; 2512 2513 // connect the stacks (clips) up to the layers 2514 var rawClips = {}; 2515 2516 for ( var nodeID in rawStacks ) { 2517 2518 var children = connections.get( parseInt( nodeID ) ).children; 2519 2520 if ( children.length > 1 ) { 2521 2522 // it seems like stacks will always be associated with a single layer. But just in case there are files 2523 // where there are multiple layers per stack, we'll display a warning 2524 console.warn( 'THREE.FBXLoader: Encountered an animation stack with multiple layers, this is currently not supported. Ignoring subsequent layers.' ); 2525 2526 } 2527 2528 var layer = layersMap.get( children[ 0 ].ID ); 2529 2530 rawClips[ nodeID ] = { 2531 2532 name: rawStacks[ nodeID ].attrName, 2533 layer: layer, 2534 2535 }; 2536 2537 } 2538 2539 return rawClips; 2540 2541 }, 2542 2543 addClip: function ( rawClip ) { 2544 2545 var tracks = []; 2546 2547 var self = this; 2548 rawClip.layer.forEach( function ( rawTracks ) { 2549 2550 tracks = tracks.concat( self.generateTracks( rawTracks ) ); 2551 2552 } ); 2553 2554 return new THREE.AnimationClip( rawClip.name, - 1, tracks ); 2555 2556 }, 2557 2558 generateTracks: function ( rawTracks ) { 2559 2560 var tracks = []; 2561 2562 var initialPosition = new THREE.Vector3(); 2563 var initialRotation = new THREE.Quaternion(); 2564 var initialScale = new THREE.Vector3(); 2565 2566 if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale ); 2567 2568 initialPosition = initialPosition.toArray(); 2569 initialRotation = new THREE.Euler().setFromQuaternion( initialRotation ).toArray(); // todo: euler order 2570 initialScale = initialScale.toArray(); 2571 2572 if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) { 2573 2574 var positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' ); 2575 if ( positionTrack !== undefined ) tracks.push( positionTrack ); 2576 2577 } 2578 2579 if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) { 2580 2581 var rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, initialRotation, rawTracks.preRotations, rawTracks.postRotations ); 2582 if ( rotationTrack !== undefined ) tracks.push( rotationTrack ); 2583 2584 } 2585 2586 if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) { 2587 2588 var scaleTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, initialScale, 'scale' ); 2589 if ( scaleTrack !== undefined ) tracks.push( scaleTrack ); 2590 2591 } 2592 2593 if ( rawTracks.DeformPercent !== undefined ) { 2594 2595 var morphTrack = this.generateMorphTrack( rawTracks ); 2596 if ( morphTrack !== undefined ) tracks.push( morphTrack ); 2597 2598 } 2599 2600 return tracks; 2601 2602 }, 2603 2604 generateVectorTrack: function ( modelName, curves, initialValue, type ) { 2605 2606 var times = this.getTimesForAllAxes( curves ); 2607 var values = this.getKeyframeTrackValues( times, curves, initialValue ); 2608 2609 return new THREE.VectorKeyframeTrack( modelName + '.' + type, times, values ); 2610 2611 }, 2612 2613 generateRotationTrack: function ( modelName, curves, initialValue, preRotations, postRotations ) { 2614 2615 if ( curves.x !== undefined ) { 2616 2617 this.interpolateRotations( curves.x ); 2618 curves.x.values = curves.x.values.map( THREE.Math.degToRad ); 2619 2620 } 2621 if ( curves.y !== undefined ) { 2622 2623 this.interpolateRotations( curves.y ); 2624 curves.y.values = curves.y.values.map( THREE.Math.degToRad ); 2625 2626 } 2627 if ( curves.z !== undefined ) { 2628 2629 this.interpolateRotations( curves.z ); 2630 curves.z.values = curves.z.values.map( THREE.Math.degToRad ); 2631 2632 } 2633 2634 var times = this.getTimesForAllAxes( curves ); 2635 var values = this.getKeyframeTrackValues( times, curves, initialValue ); 2636 2637 if ( preRotations !== undefined ) { 2638 2639 preRotations = preRotations.map( THREE.Math.degToRad ); 2640 preRotations.push( 'ZYX' ); 2641 2642 preRotations = new THREE.Euler().fromArray( preRotations ); 2643 preRotations = new THREE.Quaternion().setFromEuler( preRotations ); 2644 2645 } 2646 2647 if ( postRotations !== undefined ) { 2648 2649 postRotations = postRotations.map( THREE.Math.degToRad ); 2650 postRotations.push( 'ZYX' ); 2651 2652 postRotations = new THREE.Euler().fromArray( postRotations ); 2653 postRotations = new THREE.Quaternion().setFromEuler( postRotations ).inverse(); 2654 2655 } 2656 2657 var quaternion = new THREE.Quaternion(); 2658 var euler = new THREE.Euler(); 2659 2660 var quaternionValues = []; 2661 2662 for ( var i = 0; i < values.length; i += 3 ) { 2663 2664 euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], 'ZYX' ); 2665 2666 quaternion.setFromEuler( euler ); 2667 2668 if ( preRotations !== undefined ) quaternion.premultiply( preRotations ); 2669 if ( postRotations !== undefined ) quaternion.multiply( postRotations ); 2670 2671 quaternion.toArray( quaternionValues, ( i / 3 ) * 4 ); 2672 2673 } 2674 2675 return new THREE.QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues ); 2676 2677 }, 2678 2679 generateMorphTrack: function ( rawTracks ) { 2680 2681 var curves = rawTracks.DeformPercent.curves.morph; 2682 var values = curves.values.map( function ( val ) { 2683 2684 return val / 100; 2685 2686 } ); 2687 2688 var morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ]; 2689 2690 return new THREE.NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values ); 2691 2692 }, 2693 2694 // For all animated objects, times are defined separately for each axis 2695 // Here we'll combine the times into one sorted array without duplicates 2696 getTimesForAllAxes: function ( curves ) { 2697 2698 var times = []; 2699 2700 // first join together the times for each axis, if defined 2701 if ( curves.x !== undefined ) times = times.concat( curves.x.times ); 2702 if ( curves.y !== undefined ) times = times.concat( curves.y.times ); 2703 if ( curves.z !== undefined ) times = times.concat( curves.z.times ); 2704 2705 // then sort them and remove duplicates 2706 times = times.sort( function ( a, b ) { 2707 2708 return a - b; 2709 2710 } ).filter( function ( elem, index, array ) { 2711 2712 return array.indexOf( elem ) == index; 2713 2714 } ); 2715 2716 return times; 2717 2718 }, 2719 2720 getKeyframeTrackValues: function ( times, curves, initialValue ) { 2721 2722 var prevValue = initialValue; 2723 2724 var values = []; 2725 2726 var xIndex = - 1; 2727 var yIndex = - 1; 2728 var zIndex = - 1; 2729
2730 times.forEach( function ( time ) { 2731 2732 if ( curves.x ) xIndex = curves.x.times.indexOf( time ); 2733 if ( curves.y ) yIndex = curves.y.times.indexOf( time ); 2734 if ( curves.z ) zIndex = curves.z.times.indexOf( time ); 2735 2736 // if there is an x value defined for this frame, use that 2737 if ( xIndex !== - 1 ) { 2738 2739 var xValue = curves.x.values[ xIndex ]; 2740 values.push( xValue ); 2741 prevValue[ 0 ] = xValue; 2742 2743 } else { 2744 2745 // otherwise use the x value from the previous frame 2746 values.push( prevValue[ 0 ] ); 2747 2748 } 2749 2750 if ( yIndex !== - 1 ) { 2751 2752 var yValue = curves.y.values[ yIndex ]; 2753 values.push( yValue ); 2754 prevValue[ 1 ] = yValue; 2755 2756 } else { 2757 2758 values.push( prevValue[ 1 ] ); 2759 2760 } 2761 2762 if ( zIndex !== - 1 ) { 2763 2764 var zValue = curves.z.values[ zIndex ]; 2765 values.push( zValue ); 2766 prevValue[ 2 ] = zValue; 2767 2768 } else { 2769 2770 values.push( prevValue[ 2 ] ); 2771 2772 } 2773 2774 } ); 2775 2776 return values; 2777 2778 }, 2779 2780 // Rotations are defined as Euler angles which can have values of any size 2781 // These will be converted to quaternions which don't support values greater than 2782 // PI, so we'll interpolate large rotations 2783 interpolateRotations: function ( curve ) { 2784 2785 for ( var i = 1; i < curve.values.length; i ++ ) { 2786 2787 var initialValue = curve.values[ i - 1 ]; 2788 var valuesSpan = curve.values[ i ] - initialValue; 2789 2790 var absoluteSpan = Math.abs( valuesSpan ); 2791 2792 if ( absoluteSpan >= 180 ) { 2793 2794 var numSubIntervals = absoluteSpan / 180; 2795 2796 var step = valuesSpan / numSubIntervals; 2797 var nextValue = initialValue + step; 2798 2799 var initialTime = curve.times[ i - 1 ]; 2800 var timeSpan = curve.times[ i ] - initialTime; 2801 var interval = timeSpan / numSubIntervals; 2802 var nextTime = initialTime + interval; 2803 2804 var interpolatedTimes = []; 2805 var interpolatedValues = []; 2806 2807 while ( nextTime < curve.times[ i ] ) { 2808 2809 interpolatedTimes.push( nextTime ); 2810 nextTime += interval; 2811 2812 interpolatedValues.push( nextValue ); 2813 nextValue += step; 2814 2815 } 2816 2817 curve.times = inject( curve.times, i, interpolatedTimes ); 2818 curve.values = inject( curve.values, i, interpolatedValues ); 2819 2820 } 2821 2822 } 2823 2824 }, 2825 2826 }; 2827 2828 // parse an FBX file in ASCII format 2829 function TextParser() {} 2830 2831 TextParser.prototype = { 2832 2833 constructor: TextParser, 2834 2835 getPrevNode: function () { 2836 2837 return this.nodeStack[ this.currentIndent - 2 ]; 2838 2839 }, 2840 2841 getCurrentNode: function () { 2842 2843 return this.nodeStack[ this.currentIndent - 1 ]; 2844 2845 }, 2846 2847 getCurrentProp: function () { 2848 2849 return this.currentProp; 2850 2851 }, 2852 2853 pushStack: function ( node ) { 2854 2855 this.nodeStack.push( node ); 2856 this.currentIndent += 1; 2857 2858 }, 2859 2860 popStack: function () { 2861 2862 this.nodeStack.pop(); 2863 this.currentIndent -= 1; 2864 2865 }, 2866 2867 setCurrentProp: function ( val, name ) { 2868 2869 this.currentProp = val; 2870 this.currentPropName = name; 2871 2872 }, 2873 2874 parse: function ( text ) { 2875 2876 this.currentIndent = 0; 2877 this.allNodes = new FBXTree(); 2878 this.nodeStack = []; 2879 this.currentProp = []; 2880 this.currentPropName = ''; 2881 2882 var self = this; 2883 2884 var split = text.split( /[\r\n]+/ ); 2885 2886 split.forEach( function ( line, i ) { 2887 2888 var matchComment = line.match( /^[\s\t]*;/ ); 2889 var matchEmpty = line.match( /^[\s\t]*$/ ); 2890 2891 if ( matchComment || matchEmpty ) return; 2892 2893 var matchBeginning = line.match( '^\\t{' + self.currentIndent + '}(\\w+):(.*){', '' ); 2894 var matchProperty = line.match( '^\\t{' + ( self.currentIndent ) + '}(\\w+):[\\s\\t\\r\\n](.*)' ); 2895 var matchEnd = line.match( '^\\t{' + ( self.currentIndent - 1 ) + '}}' ); 2896 2897 if ( matchBeginning ) { 2898 2899 self.parseNodeBegin( line, matchBeginning ); 2900 2901 } else if ( matchProperty ) { 2902 2903 self.parseNodeProperty( line, matchProperty, split[ ++ i ] ); 2904 2905 } else if ( matchEnd ) { 2906 2907 self.popStack(); 2908 2909 } else if ( line.match( /^[^\s\t}]/ ) ) { 2910 2911 // large arrays are split over multiple lines terminated with a ',' character 2912 // if this is encountered the line needs to be joined to the previous line 2913 self.parseNodePropertyContinued( line ); 2914 2915 } 2916 2917 } ); 2918 2919 return this.allNodes; 2920 2921 }, 2922 2923 parseNodeBegin: function ( line, property ) { 2924 2925 var nodeName = property[ 1 ].trim().replace( /^"/, '' ).replace( /"$/, '' ); 2926 2927 var nodeAttrs = property[ 2 ].split( ',' ).map( function ( attr ) { 2928 2929 return attr.trim().replace( /^"/, '' ).replace( /"$/, '' ); 2930 2931 } ); 2932 2933 var node = { name: nodeName }; 2934 var attrs = this.parseNodeAttr( nodeAttrs ); 2935 2936 var currentNode = this.getCurrentNode(); 2937 2938 // a top node 2939 if ( this.currentIndent === 0 ) { 2940 2941 this.allNodes.add( nodeName, node ); 2942 2943 } else { // a subnode 2944 2945 // if the subnode already exists, append it 2946 if ( nodeName in currentNode ) { 2947 2948 // special case Pose needs PoseNodes as an array 2949 if ( nodeName === 'PoseNode' ) { 2950
2951 currentNode.PoseNode.push( node ); 2952 2953 } else if ( currentNode[ nodeName ].id !== undefined ) { 2954 2955 currentNode[ nodeName ] = {}; 2956 currentNode[ nodeName ][ currentNode[ nodeName ].id ] = currentNode[ nodeName ]; 2957 2958 } 2959 2960 if ( attrs.id !== '' ) currentNode[ nodeName ][ attrs.id ] = node; 2961 2962 } else if ( typeof attrs.id === 'number' ) { 2963 2964 currentNode[ nodeName ] = {}; 2965 currentNode[ nodeName ][ attrs.id ] = node; 2966 2967 } else if ( nodeName !== 'Properties70' ) { 2968 2969 if ( nodeName === 'PoseNode' ) currentNode[ nodeName ] = [ node ]; 2970 else currentNode[ nodeName ] = node; 2971 2972 } 2973 2974 } 2975 2976 if ( typeof attrs.id === 'number' ) node.id = attrs.id; 2977 if ( attrs.name !== '' ) node.attrName = attrs.name; 2978 if ( attrs.type !== '' ) node.attrType = attrs.type; 2979 2980 this.pushStack( node ); 2981 2982 }, 2983 2984 parseNodeAttr: function ( attrs ) { 2985 2986 var id = attrs[ 0 ]; 2987 2988 if ( attrs[ 0 ] !== '' ) { 2989 2990 id = parseInt( attrs[ 0 ] ); 2991 2992 if ( isNaN( id ) ) { 2993 2994 id = attrs[ 0 ]; 2995 2996 } 2997 2998 } 2999 3000 var name = '', type = ''; 3001 3002 if ( attrs.length > 1 ) { 3003 3004 name = attrs[ 1 ].replace( /^(\w+)::/, '' ); 3005 type = attrs[ 2 ]; 3006 3007 } 3008 3009 return { id: id, name: name, type: type }; 3010 3011 }, 3012 3013 parseNodeProperty: function ( line, property, contentLine ) { 3014 3015 var propName = property[ 1 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); 3016 var propValue = property[ 2 ].replace( /^"/, '' ).replace( /"$/, '' ).trim(); 3017 3018 // for special case: base64 image data follows "Content: ," line 3019 // Content: , 3020 // "/9j/4RDaRXhpZgAATU0A..." 3021 if ( propName === 'Content' && propValue === ',' ) { 3022 3023 propValue = contentLine.replace( /"/g, '' ).replace( /,$/, '' ).trim(); 3024 3025 } 3026 3027 var currentNode = this.getCurrentNode(); 3028 var parentName = currentNode.name; 3029 3030 if ( parentName === 'Properties70' ) { 3031 3032 this.parseNodeSpecialProperty( line, propName, propValue ); 3033 return; 3034 3035 } 3036 3037 // Connections 3038 if ( propName === 'C' ) { 3039 3040 var connProps = propValue.split( ',' ).slice( 1 ); 3041 var from = parseInt( connProps[ 0 ] ); 3042 var to = parseInt( connProps[ 1 ] ); 3043 3044 var rest = propValue.split( ',' ).slice( 3 ); 3045 3046 rest = rest.map( function ( elem ) { 3047 3048 return elem.trim().replace( /^"/, '' ); 3049 3050 } ); 3051 3052 propName = 'connections'; 3053 propValue = [ from, to ]; 3054 append( propValue, rest ); 3055 3056 if ( currentNode[ propName ] === undefined ) { 3057 3058 currentNode[ propName ] = []; 3059 3060 } 3061 3062 } 3063 3064 // Node 3065 if ( propName === 'Node' ) currentNode.id = propValue; 3066 3067 // connections 3068 if ( propName in currentNode && Array.isArray( currentNode[ propName ] ) ) { 3069 3070 currentNode[ propName ].push( propValue ); 3071 3072 } else { 3073 3074 if ( propName !== 'a' ) currentNode[ propName ] = propValue; 3075 else currentNode.a = propValue; 3076 3077 } 3078 3079 this.setCurrentProp( currentNode, propName ); 3080 3081 // convert string to array, unless it ends in ',' in which case more will be added to it 3082 if ( propName === 'a' && propValue.slice( - 1 ) !== ',' ) { 3083 3084 currentNode.a = parseNumberArray( propValue ); 3085 3086 } 3087 3088 }, 3089 3090 parseNodePropertyContinued: function ( line ) { 3091 3092 var currentNode = this.getCurrentNode(); 3093 3094 currentNode.a += line; 3095 3096 // if the line doesn't end in ',' we have reached the end of the property value 3097 // so convert the string to an array 3098 if ( line.slice( - 1 ) !== ',' ) { 3099 3100 currentNode.a = parseNumberArray( currentNode.a ); 3101 3102 } 3103 3104 }, 3105 3106 // parse "Property70" 3107 parseNodeSpecialProperty: function ( line, propName, propValue ) { 3108 3109 // split this 3110 // P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1 3111 // into array like below 3112 // ["Lcl Scaling", "Lcl Scaling", "", "A", "1,1,1" ] 3113 var props = propValue.split( '",' ).map( function ( prop ) { 3114 3115 return prop.trim().replace( /^\"/, '' ).replace( /\s/, '_' ); 3116 3117 } ); 3118 3119 var innerPropName = props[ 0 ]; 3120 var innerPropType1 = props[ 1 ]; 3121 var innerPropType2 = props[ 2 ]; 3122 var innerPropFlag = props[ 3 ]; 3123 var innerPropValue = props[ 4 ]; 3124 3125 // cast values where needed, otherwise leave as strings 3126 switch ( innerPropType1 ) { 3127 3128 case 'int': 3129 case 'enum': 3130 case 'bool': 3131 case 'ULongLong': 3132 case 'double': 3133 case 'Number': 3134 case 'FieldOfView': 3135 innerPropValue = parseFloat( innerPropValue ); 3136 break; 3137
3138 case 'Color': 3139 case 'ColorRGB': 3140 case 'Vector3D': 3141 case 'Lcl_Translation': 3142 case 'Lcl_Rotation': 3143 case 'Lcl_Scaling': 3144 innerPropValue = parseNumberArray( innerPropValue ); 3145 break; 3146 3147 } 3148 3149 // CAUTION: these props must append to parent's parent 3150 this.getPrevNode()[ innerPropName ] = { 3151 3152 'type': innerPropType1, 3153 'type2': innerPropType2, 3154 'flag': innerPropFlag, 3155 'value': innerPropValue 3156 3157 }; 3158 3159 this.setCurrentProp( this.getPrevNode(), innerPropName ); 3160 3161 }, 3162 3163 }; 3164 3165 // Parse an FBX file in Binary format 3166 function BinaryParser() {} 3167 3168 BinaryParser.prototype = { 3169 3170 constructor: BinaryParser, 3171 3172 parse: function ( buffer ) { 3173 3174 var reader = new BinaryReader( buffer ); 3175 reader.skip( 23 ); // skip magic 23 bytes 3176 3177 var version = reader.getUint32(); 3178 3179 console.log( 'THREE.FBXLoader: FBX binary version: ' + version ); 3180 3181 var allNodes = new FBXTree(); 3182 3183 while ( ! this.endOfContent( reader ) ) { 3184 3185 var node = this.parseNode( reader, version ); 3186 if ( node !== null ) allNodes.add( node.name, node ); 3187 3188 } 3189 3190 return allNodes; 3191 3192 }, 3193 3194 // Check if reader has reached the end of content. 3195 endOfContent: function ( reader ) { 3196 3197 // footer size: 160bytes + 16-byte alignment padding 3198 // - 16bytes: magic 3199 // - padding til 16-byte alignment (at least 1byte?) 3200 // (seems like some exporters embed fixed 15 or 16bytes?) 3201 // - 4bytes: magic 3202 // - 4bytes: version 3203 // - 120bytes: zero 3204 // - 16bytes: magic 3205 if ( reader.size() % 16 === 0 ) { 3206 3207 return ( ( reader.getOffset() + 160 + 16 ) & ~ 0xf ) >= reader.size(); 3208 3209 } else { 3210 3211 return reader.getOffset() + 160 + 16 >= reader.size(); 3212 3213 } 3214 3215 }, 3216 3217 // recursively parse nodes until the end of the file is reached 3218 parseNode: function ( reader, version ) { 3219 3220 var node = {}; 3221 3222 // The first three data sizes depends on version. 3223 var endOffset = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); 3224 var numProperties = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); 3225 3226 // note: do not remove this even if you get a linter warning as it moves the buffer forward 3227 var propertyListLen = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); 3228 3229 var nameLen = reader.getUint8(); 3230 var name = reader.getString( nameLen ); 3231 3232 // Regards this node as NULL-record if endOffset is zero 3233 if ( endOffset === 0 ) return null; 3234 3235 var propertyList = []; 3236 3237 for ( var i = 0; i < numProperties; i ++ ) { 3238 3239 propertyList.push( this.parseProperty( reader ) ); 3240 3241 } 3242 3243 // Regards the first three elements in propertyList as id, attrName, and attrType 3244 var id = propertyList.length > 0 ? propertyList[ 0 ] : ''; 3245 var attrName = propertyList.length > 1 ? propertyList[ 1 ] : ''; 3246 var attrType = propertyList.length > 2 ? propertyList[ 2 ] : ''; 3247 3248 // check if this node represents just a single property 3249 // like (name, 0) set or (name2, [0, 1, 2]) set of {name: 0, name2: [0, 1, 2]} 3250 node.singleProperty = ( numProperties === 1 && reader.getOffset() === endOffset ) ? true : false; 3251 3252 while ( endOffset > reader.getOffset() ) { 3253 3254 var subNode = this.parseNode( reader, version ); 3255 3256 if ( subNode !== null ) this.parseSubNode( name, node, subNode ); 3257 3258 } 3259 3260 node.propertyList = propertyList; // raw property list used by parent 3261 3262 if ( typeof id === 'number' ) node.id = id; 3263 if ( attrName !== '' ) node.attrName = attrName; 3264 if ( attrType !== '' ) node.attrType = attrType; 3265 if ( name !== '' ) node.name = name; 3266 3267 return node; 3268 3269 }, 3270 3271 parseSubNode: function ( name, node, subNode ) { 3272 3273 // special case: child node is single property 3274 if ( subNode.singleProperty === true ) { 3275 3276 var value = subNode.propertyList[ 0 ]; 3277 3278 if ( Array.isArray( value ) ) { 3279 3280 node[ subNode.name ] = subNode; 3281 3282 subNode.a = value; 3283 3284 } else { 3285 3286 node[ subNode.name ] = value; 3287 3288 } 3289 3290 } else if ( name === 'Connections' && subNode.name === 'C' ) { 3291 3292 var array = []; 3293 3294 subNode.propertyList.forEach( function ( property, i ) { 3295 3296 // first Connection is FBX type (OO, OP, etc.). We'll discard these 3297 if ( i !== 0 ) array.push( property ); 3298 3299 } ); 3300 3301 if ( node.connections === undefined ) { 3302 3303 node.connections = []; 3304 3305 } 3306 3307 node.connections.push( array ); 3308 3309 } else if ( subNode.name === 'Properties70' ) { 3310 3311 var keys = Object.keys( subNode ); 3312 3313 keys.forEach( function ( key ) { 3314 3315 node[ key ] = subNode[ key ]; 3316 3317 } ); 3318 3319 } else if ( name === 'Properties70' && subNode.name === 'P' ) { 3320 3321 var innerPropName = subNode.propertyList[ 0 ]; 3322 var innerPropType1 = subNode.propertyList[ 1 ]; 3323 var innerPropType2 = subNode.propertyList[ 2 ]; 3324 var innerPropFlag = subNode.propertyList[ 3 ]; 3325 var innerPropValue; 3326 3327 if ( innerPropName.indexOf( 'Lcl ' ) === 0 ) innerPropName = innerPropName.replace( 'Lcl ', 'Lcl_' ); 3328 if ( innerPropType1.indexOf( 'Lcl ' ) === 0 ) innerPropType1 = innerPropType1.replace( 'Lcl ', 'Lcl_' ); 3329 3330 if ( innerPropType1 === 'Color' || innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || innerPropType1 === 'Vector3D' || innerPropType1.indexOf( 'Lcl_' ) === 0 ) { 3331 3332 innerPropValue = [ 3333 subNode.propertyList[ 4 ], 3334 subNode.propertyList[ 5 ], 3335 subNode.propertyList[ 6 ] 3336 ]; 3337 3338 } else { 3339 3340 innerPropValue = subNode.propertyList[ 4 ]; 3341 3342 } 3343 3344 // this will be copied to parent, see above 3345 node[ innerPropName ] = { 3346 3347 'type': innerPropType1, 3348 'type2': innerPropType2, 3349 'flag': innerPropFlag, 3350 'value': innerPropValue 3351 3352 }; 3353 3354 } else if ( node[ subNode.name ] === undefined ) { 3355 3356 if ( typeof subNode.id === 'number' ) { 3357 3358 node[ subNode.name ] = {}; 3359 node[ subNode.name ][ subNode.id ] = subNode; 3360 3361 } else { 3362 3363 node[ subNode.name ] = subNode; 3364 3365 } 3366 3367 } else { 3368 3369 if ( subNode.name === 'PoseNode' ) { 3370 3371 if ( ! Array.isArray( node[ subNode.name ] ) ) { 3372 3373 node[ subNode.name ] = [ node[ subNode.name ] ]; 3374 3375 } 3376 3377 node[ subNode.name ].push( subNode ); 3378 3379 } else if ( node[ subNode.name ][ subNode.id ] === undefined ) { 3380 3381 node[ subNode.name ][ subNode.id ] = subNode; 3382 3383 } 3384 3385 } 3386 3387 }, 3388 3389 parseProperty: function ( reader ) { 3390 3391 var type = reader.getString( 1 ); 3392 3393 switch ( type ) { 3394 3395 case 'C': 3396 return reader.getBoolean(); 3397
3398 case 'D': 3399 return reader.getFloat64(); 3400 3401 case 'F': 3402 return reader.getFloat32(); 3403 3404 case 'I': 3405 return reader.getInt32(); 3406 3407 case 'L': 3408 return reader.getInt64(); 3409 3410 case 'R': 3411 var length = reader.getUint32(); 3412 return reader.getArrayBuffer( length ); 3413 3414 case 'S': 3415 var length = reader.getUint32(); 3416 return reader.getString( length ); 3417 3418 case 'Y': 3419 return reader.getInt16(); 3420 3421 case 'b': 3422 case 'c': 3423 case 'd': 3424 case 'f': 3425 case 'i': 3426 case 'l': 3427 3428 var arrayLength = reader.getUint32(); 3429 var encoding = reader.getUint32(); // 0: non-compressed, 1: compressed 3430 var compressedLength = reader.getUint32(); 3431 3432 if ( encoding === 0 ) { 3433 3434 switch ( type ) { 3435 3436 case 'b': 3437 case 'c': 3438 return reader.getBooleanArray( arrayLength ); 3439 3440 case 'd': 3441 return reader.getFloat64Array( arrayLength ); 3442 3443 case 'f': 3444 return reader.getFloat32Array( arrayLength ); 3445 3446 case 'i': 3447 return reader.getInt32Array( arrayLength ); 3448 3449 case 'l': 3450 return reader.getInt64Array( arrayLength ); 3451 3452 } 3453 3454 } 3455 3456 if ( typeof Zlib === 'undefined' ) { 3457 3458 console.error( 'THREE.FBXLoader: External library Inflate.min.js required, obtain or import from https://github.com/imaya/zlib.js' ); 3459 3460 } 3461 3462 var inflate = new Zlib.Inflate( new Uint8Array( reader.getArrayBuffer( compressedLength ) ) ); // eslint-disable-line no-undef 3463 var reader2 = new BinaryReader( inflate.decompress().buffer ); 3464 3465 switch ( type ) { 3466 3467 case 'b': 3468 case 'c': 3469 return reader2.getBooleanArray( arrayLength ); 3470 3471 case 'd': 3472 return reader2.getFloat64Array( arrayLength ); 3473 3474 case 'f': 3475 return reader2.getFloat32Array( arrayLength ); 3476 3477 case 'i': 3478 return reader2.getInt32Array( arrayLength ); 3479 3480 case 'l': 3481 return reader2.getInt64Array( arrayLength ); 3482 3483 } 3484 3485 default: 3486 throw new Error( 'THREE.FBXLoader: Unknown property type ' + type ); 3487 3488 } 3489 3490 } 3491 3492 }; 3493 3494 function BinaryReader( buffer, littleEndian ) { 3495 3496 this.dv = new DataView( buffer ); 3497 this.offset = 0; 3498 this.littleEndian = ( littleEndian !== undefined ) ? littleEndian : true; 3499 3500 } 3501 3502 BinaryReader.prototype = { 3503 3504 constructor: BinaryReader, 3505 3506 getOffset: function () { 3507 3508 return this.offset; 3509 3510 }, 3511 3512 size: function () { 3513 3514 return this.dv.buffer.byteLength; 3515 3516 }, 3517 3518 skip: function ( length ) { 3519 3520 this.offset += length; 3521 3522 }, 3523 3524 // seems like true/false representation depends on exporter. 3525 // true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54) 3526 // then sees LSB. 3527 getBoolean: function () { 3528 3529 return ( this.getUint8() & 1 ) === 1; 3530 3531 }, 3532 3533 getBooleanArray: function ( size ) { 3534 3535 var a = []; 3536 3537 for ( var i = 0; i < size; i ++ ) { 3538 3539 a.push( this.getBoolean() ); 3540 3541 } 3542 3543 return a; 3544 3545 }, 3546 3547 getUint8: function () { 3548 3549 var value = this.dv.getUint8( this.offset ); 3550 this.offset += 1; 3551 return value; 3552 3553 }, 3554 3555 getInt16: function () { 3556 3557 var value = this.dv.getInt16( this.offset, this.littleEndian ); 3558 this.offset += 2; 3559 return value; 3560 3561 }, 3562 3563 getInt32: function () { 3564 3565 var value = this.dv.getInt32( this.offset, this.littleEndian ); 3566 this.offset += 4; 3567 return value; 3568 3569 }, 3570 3571 getInt32Array: function ( size ) { 3572 3573 var a = []; 3574 3575 for ( var i = 0; i < size; i ++ ) { 3576 3577 a.push( this.getInt32() ); 3578 3579 } 3580 3581 return a; 3582 3583 }, 3584 3585 getUint32: function () { 3586 3587 var value = this.dv.getUint32( this.offset, this.littleEndian ); 3588 this.offset += 4; 3589 return value; 3590 3591 }, 3592 3593 // JavaScript doesn't support 64-bit integer so calculate this here 3594 // 1 << 32 will return 1 so using multiply operation instead here. 3595 // There's a possibility that this method returns wrong value if the value 3596 // is out of the range between Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER. 3597 // TODO: safely handle 64-bit integer 3598 getInt64: function () { 3599 3600 var low, high; 3601 3602 if ( this.littleEndian ) { 3603 3604 low = this.getUint32(); 3605 high = this.getUint32(); 3606 3607 } else { 3608 3609 high = this.getUint32(); 3610 low = this.getUint32(); 3611 3612 } 3613 3614 // calculate negative value 3615 if ( high & 0x80000000 ) { 3616 3617 high = ~ high & 0xFFFFFFFF; 3618 low = ~ low & 0xFFFFFFFF; 3619 3620 if ( low === 0xFFFFFFFF ) high = ( high + 1 ) & 0xFFFFFFFF; 3621 3622 low = ( low + 1 ) & 0xFFFFFFFF; 3623 3624 return - ( high * 0x100000000 + low ); 3625 3626 } 3627 3628 return high * 0x100000000 + low; 3629 3630 }, 3631 3632 getInt64Array: function ( size ) { 3633 3634 var a = []; 3635 3636 for ( var i = 0; i < size; i ++ ) { 3637 3638 a.push( this.getInt64() ); 3639 3640 } 3641 3642 return a; 3643 3644 }, 3645 3646 // Note: see getInt64() comment 3647 getUint64: function () { 3648 3649 var low, high; 3650 3651 if ( this.littleEndian ) { 3652 3653 low = this.getUint32(); 3654 high = this.getUint32(); 3655 3656 } else { 3657 3658 high = this.getUint32(); 3659 low = this.getUint32(); 3660 3661 } 3662 3663 return high * 0x100000000 + low; 3664 3665 }, 3666 3667 getFloat32: function () { 3668 3669 var value = this.dv.getFloat32( this.offset, this.littleEndian ); 3670 this.offset += 4; 3671 return value; 3672 3673 }, 3674 3675 getFloat32Array: function ( size ) { 3676 3677 var a = []; 3678 3679 for ( var i = 0; i < size; i ++ ) { 3680 3681 a.push( this.getFloat32() ); 3682 3683 } 3684 3685 return a; 3686 3687 }, 3688 3689 getFloat64: function () { 3690 3691 var value = this.dv.getFloat64( this.offset, this.littleEndian ); 3692 this.offset += 8; 3693 return value; 3694 3695 }, 3696 3697 getFloat64Array: function ( size ) { 3698 3699 var a = []; 3700 3701 for ( var i = 0; i < size; i ++ ) { 3702 3703 a.push( this.getFloat64() ); 3704 3705 } 3706 3707 return a; 3708 3709 }, 3710 3711 getArrayBuffer: function ( size ) { 3712 3713 var value = this.dv.buffer.slice( this.offset, this.offset + size ); 3714 this.offset += size; 3715 return value; 3716 3717 }, 3718 3719 getString: function ( size ) { 3720 3721 // note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead 3722 var a = []; 3723 3724 for ( var i = 0; i < size; i ++ ) { 3725 3726 a[ i ] = this.getUint8(); 3727 3728 } 3729 3730 var nullByte = a.indexOf( 0 ); 3731 if ( nullByte >= 0 ) a = a.slice( 0, nullByte ); 3732 3733 return THREE.LoaderUtils.decodeText( new Uint8Array( a ) ); 3734 3735 } 3736 3737 }; 3738 3739 // FBXTree holds a representation of the FBX data, returned by the TextParser ( FBX ASCII format) 3740 // and BinaryParser( FBX Binary format) 3741 function FBXTree() {} 3742 3743 FBXTree.prototype = { 3744 3745 constructor: FBXTree, 3746 3747 add: function ( key, val ) { 3748 3749 this[ key ] = val; 3750 3751 }, 3752 3753 }; 3754 3755 // ************** UTILITY FUNCTIONS ************** 3756 3757 function isFbxFormatBinary( buffer ) { 3758 3759 var CORRECT = 'Kaydara FBX Binary \0'; 3760 3761 return buffer.byteLength >= CORRECT.length && CORRECT === convertArrayBufferToString( buffer, 0, CORRECT.length ); 3762 3763 } 3764 3765 function isFbxFormatASCII( text ) { 3766 3767 var CORRECT = [ 'K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\' ]; 3768 3769 var cursor = 0; 3770 3771 function read( offset ) { 3772 3773 var result = text[ offset - 1 ];
3774 text = text.slice( cursor + offset ); 3775 cursor ++; 3776 return result; 3777 3778 } 3779 3780 for ( var i = 0; i < CORRECT.length; ++ i ) { 3781 3782 var num = read( 1 ); 3783 if ( num === CORRECT[ i ] ) { 3784 3785 return false; 3786 3787 } 3788 3789 } 3790 3791 return true; 3792 3793 } 3794 3795 function getFbxVersion( text ) { 3796 3797 var versionRegExp = /FBXVersion: (\d+)/; 3798 var match = text.match( versionRegExp ); 3799 if ( match ) { 3800 3801 var version = parseInt( match[ 1 ] ); 3802 return version; 3803 3804 } 3805 throw new Error( 'THREE.FBXLoader: Cannot find the version number for the file given.' ); 3806 3807 } 3808 3809 // Converts FBX ticks into real time seconds. 3810 function convertFBXTimeToSeconds( time ) { 3811 3812 return time / 46186158000; 3813 3814 } 3815 3816 var dataArray = []; 3817 3818 // extracts the data from the correct position in the FBX array based on indexing type 3819 function getData( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) { 3820 3821 var index; 3822 3823 switch ( infoObject.mappingType ) { 3824 3825 case 'ByPolygonVertex' : 3826 index = polygonVertexIndex; 3827 break; 3828 case 'ByPolygon' : 3829 index = polygonIndex; 3830 break; 3831 case 'ByVertice' : 3832 index = vertexIndex; 3833 break; 3834 case 'AllSame' : 3835 index = infoObject.indices[ 0 ]; 3836 break; 3837 default : 3838 console.warn( 'THREE.FBXLoader: unknown attribute mapping type ' + infoObject.mappingType ); 3839 3840 } 3841 3842 if ( infoObject.referenceType === 'IndexToDirect' ) index = infoObject.indices[ index ]; 3843 3844 var from = index * infoObject.dataSize; 3845 var to = from + infoObject.dataSize; 3846 3847 return slice( dataArray, infoObject.buffer, from, to ); 3848 3849 } 3850 3851 var tempMat = new THREE.Matrix4(); 3852 var tempEuler = new THREE.Euler(); 3853 var tempVec = new THREE.Vector3(); 3854 var translation = new THREE.Vector3(); 3855 var rotation = new THREE.Matrix4(); 3856 3857 // generate transformation from FBX transform data 3858 // ref: https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_10CDD63C_79C1_4F2D_BB28_AD2BE65A02ED_htm 3859 // transformData = { 3860 // eulerOrder: int, 3861 // translation: [], 3862 // rotationOffset: [], 3863 // preRotation 3864 // rotation 3865 // postRotation 3866 // scale 3867 // } 3868 // all entries are optional 3869 function generateTransform( transformData ) { 3870 3871 var transform = new THREE.Matrix4(); 3872 translation.set( 0, 0, 0 ); 3873 rotation.identity(); 3874 3875 var order = ( transformData.eulerOrder ) ? getEulerOrder( transformData.eulerOrder ) : getEulerOrder( 0 ); 3876 3877 if ( transformData.translation ) translation.fromArray( transformData.translation ); 3878 if ( transformData.rotationOffset ) translation.add( tempVec.fromArray( transformData.rotationOffset ) ); 3879 3880 if ( transformData.rotation ) { 3881 3882 var array = transformData.rotation.map( THREE.Math.degToRad ); 3883 array.push( order ); 3884 rotation.makeRotationFromEuler( tempEuler.fromArray( array ) ); 3885 3886 } 3887 3888 if ( transformData.preRotation ) { 3889 3890 var array = transformData.preRotation.map( THREE.Math.degToRad ); 3891 array.push( order ); 3892 tempMat.makeRotationFromEuler( tempEuler.fromArray( array ) ); 3893 3894 rotation.premultiply( tempMat ); 3895 3896 } 3897 3898 if ( transformData.postRotation ) { 3899 3900 var array = transformData.postRotation.map( THREE.Math.degToRad ); 3901 array.push( order ); 3902 tempMat.makeRotationFromEuler( tempEuler.fromArray( array ) ); 3903 3904 tempMat.getInverse( tempMat ); 3905 3906 rotation.multiply( tempMat ); 3907 3908 } 3909 3910 if ( transformData.scale ) transform.scale( tempVec.fromArray( transformData.scale ) ); 3911 3912 transform.setPosition( translation ); 3913 transform.multiply( rotation ); 3914 3915 return transform; 3916 3917 } 3918 3919 // Returns the three.js intrinsic Euler order corresponding to FBX extrinsic Euler order 3920 // ref: http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_euler_html 3921 function getEulerOrder( order ) { 3922 3923 var enums = [ 3924 'ZYX', // -> XYZ extrinsic 3925 'YZX', // -> XZY extrinsic 3926 'XZY', // -> YZX extrinsic 3927 'ZXY', // -> YXZ extrinsic 3928 'YXZ', // -> ZXY extrinsic 3929 'XYZ', // -> ZYX extrinsic 3930 //'SphericXYZ', // not possible to support 3931 ]; 3932 3933 if ( order === 6 ) { 3934 3935 console.warn( 'THREE.FBXLoader: unsupported Euler Order: Spherical XYZ. Animations and rotations may be incorrect.' ); 3936 return enums[ 0 ]; 3937 3938 } 3939 3940 return enums[ order ]; 3941 3942 } 3943 3944 // Parses comma separated list of numbers and returns them an array.
3945 // Used internally by the TextParser 3946 function parseNumberArray( value ) { 3947 3948 var array = value.split( ',' ).map( function ( val ) { 3949 3950 return parseFloat( val ); 3951 3952 } ); 3953 3954 return array; 3955 3956 } 3957 3958 function convertArrayBufferToString( buffer, from, to ) { 3959 3960 if ( from === undefined ) from = 0; 3961 if ( to === undefined ) to = buffer.byteLength; 3962 3963 return THREE.LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) ); 3964 3965 } 3966 3967 function append( a, b ) { 3968 3969 for ( var i = 0, j = a.length, l = b.length; i < l; i ++, j ++ ) { 3970 3971 a[ j ] = b[ i ]; 3972 3973 } 3974 3975 } 3976 3977 function slice( a, b, from, to ) { 3978 3979 for ( var i = from, j = 0; i < to; i ++, j ++ ) { 3980 3981 a[ j ] = b[ i ]; 3982 3983 } 3984 3985 return a; 3986 3987 } 3988 3989 // inject array a2 into array a1 at index 3990 function inject( a1, index, a2 ) { 3991 3992 return a1.slice( 0, index ).concat( a2 ).concat( a1.slice( index ) ); 3993 3994 } 3995 3996 return FBXLoader; 3997 3998} )();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.