PageSourceSearch

https://www.mhv-gifhorn.de/wp-content/plugins/vrm360/js/FBXLoader.js?ver=5.6

js mhv-gifhorn.de collected 2026-10-02 20:37:30 UTC 101,085 bytes, 4,136 lines download raw bytes

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

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.