FBXLoader.js 96.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import {
	AmbientLight,
	AnimationClip,
	Bone,
	BufferAttribute,
	BufferGeometry,
	ClampToEdgeWrapping,
	Color,
	DirectionalLight,
	EquirectangularReflectionMapping,
	Euler,
	FileLoader,
	Float32BufferAttribute,
	Group,
	Line,
	LineBasicMaterial,
	Loader,
	LoaderUtils,
M
Mugen87 已提交
19
	MathUtils,
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
	Matrix3,
	Matrix4,
	Mesh,
	MeshLambertMaterial,
	MeshPhongMaterial,
	NumberKeyframeTrack,
	Object3D,
	OrthographicCamera,
	PerspectiveCamera,
	PointLight,
	PropertyBinding,
	Quaternion,
	QuaternionKeyframeTrack,
	RepeatWrapping,
	Skeleton,
	SkinnedMesh,
	SpotLight,
	Texture,
	TextureLoader,
	Uint16BufferAttribute,
	Vector3,
	Vector4,
	VectorKeyframeTrack,
L
Lewy Blue 已提交
43
	sRGBEncoding
44
} from '../../../build/three.module.js';
M
Mugen87 已提交
45
import * as fflate from '../libs/fflate.module.min.js';
46
import { NURBSCurve } from '../curves/NURBSCurve.js';
M
Mr.doob 已提交
47

M
Mugen87 已提交
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
/**
 * Loader loads FBX file and generates Group representing FBX scene.
 * Requires FBX file to be >= 7.0 and in ASCII or >= 6400 in Binary format
 * Versions lower than this may load but will probably have errors
 *
 * Needs Support:
 *  Morph normals / blend shape normals
 *
 * FBX format references:
 * 	https://wiki.blender.org/index.php/User:Mont29/Foundation/FBX_File_Structure
 * 	http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_index_html (C++ SDK reference)
 *
 * 	Binary format specification:
 *		https://code.blender.org/2013/08/fbx-binary-file-format-specification/
 */
63 64


65 66 67
let fbxTree;
let connections;
let sceneGraph;
68

69
class FBXLoader extends Loader {
70

71
	constructor( manager ) {
72

73
		super( manager );
74 75 76

	}

77
	load( url, onLoad, onProgress, onError ) {
78

79
		const scope = this;
80

81
		const path = ( scope.path === '' ) ? LoaderUtils.extractUrlBase( url ) : scope.path;
82

83 84 85 86 87
		const loader = new FileLoader( this.manager );
		loader.setPath( scope.path );
		loader.setResponseType( 'arraybuffer' );
		loader.setRequestHeader( scope.requestHeader );
		loader.setWithCredentials( scope.withCredentials );
88

89
		loader.load( url, function ( buffer ) {
90

91
			try {
92

93
				onLoad( scope.parse( buffer, path ) );
94

95
			} catch ( e ) {
96

97
				if ( onError ) {
98

99
					onError( e );
100

101
				} else {
102

103
					console.error( e );
104

105
				}
M
Mugen87 已提交
106

107
				scope.manager.itemError( url );
M
Mugen87 已提交
108

109
			}
110

111
		}, onProgress, onError );
112

113
	}
114

115
	parse( FBXBuffer, path ) {
116

117
		if ( isFbxFormatBinary( FBXBuffer ) ) {
118

119
			fbxTree = new BinaryParser().parse( FBXBuffer );
120

121
		} else {
122

123
			const FBXText = convertArrayBufferToString( FBXBuffer );
124

125
			if ( ! isFbxFormatASCII( FBXText ) ) {
126

127
				throw new Error( 'THREE.FBXLoader: Unknown format.' );
128

129
			}
130

131
			if ( getFbxVersion( FBXText ) < 7000 ) {
132

133
				throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + getFbxVersion( FBXText ) );
134

135
			}
136

137
			fbxTree = new TextParser().parse( FBXText );
138

139
		}
140

141
		// console.log( fbxTree );
142

143
		const textureLoader = new TextureLoader( this.manager ).setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin );
144

145
		return new FBXTreeParser( textureLoader, this.manager ).parse( fbxTree );
146

147
	}
148

149
}
150

151 152
// Parse the FBXTree object returned by the BinaryParser or TextParser and return a Group
class FBXTreeParser {
153

154
	constructor( textureLoader, manager ) {
155 156

		this.textureLoader = textureLoader;
M
Mugen87 已提交
157
		this.manager = manager;
158 159 160

	}

161
	parse() {
162

163
		connections = this.parseConnections();
164

165 166 167 168 169
		const images = this.parseImages();
		const textures = this.parseTextures( images );
		const materials = this.parseMaterials( textures );
		const deformers = this.parseDeformers();
		const geometryMap = new GeometryParser().parse( deformers );
170

171
		this.parseScene( deformers, geometryMap, materials );
172

173
		return sceneGraph;
174

175
	}
176

177 178 179
	// Parses FBXTree.Connections which holds parent-child connections between objects (e.g. material -> texture, model->geometry )
	// and details the connection type
	parseConnections() {
180

181
		const connectionMap = new Map();
182

183
		if ( 'Connections' in fbxTree ) {
184

185
			const rawConnections = fbxTree.Connections.connections;
186

187
			rawConnections.forEach( function ( rawConnection ) {
188

189 190 191
				const fromID = rawConnection[ 0 ];
				const toID = rawConnection[ 1 ];
				const relationship = rawConnection[ 2 ];
192

193
				if ( ! connectionMap.has( fromID ) ) {
194

195 196 197 198
					connectionMap.set( fromID, {
						parents: [],
						children: []
					} );
199

200
				}
201

202 203
				const parentRelationship = { ID: toID, relationship: relationship };
				connectionMap.get( fromID ).parents.push( parentRelationship );
204

205
				if ( ! connectionMap.has( toID ) ) {
206

207 208 209 210
					connectionMap.set( toID, {
						parents: [],
						children: []
					} );
211

212
				}
213

214 215
				const childRelationship = { ID: fromID, relationship: relationship };
				connectionMap.get( toID ).children.push( childRelationship );
216

217
			} );
218

219
		}
220

221
		return connectionMap;
222

223
	}
224

225 226 227 228
	// Parse FBXTree.Objects.Video for embedded image data
	// These images are connected to textures in FBXTree.Objects.Textures
	// via FBXTree.Connections.
	parseImages() {
229

230 231
		const images = {};
		const blobs = {};
232

233
		if ( 'Video' in fbxTree.Objects ) {
234

235
			const videoNodes = fbxTree.Objects.Video;
236

237
			for ( const nodeID in videoNodes ) {
238

239
				const videoNode = videoNodes[ nodeID ];
240

241
				const id = parseInt( nodeID );
242

243
				images[ id ] = videoNode.RelativeFilename || videoNode.Filename;
244

245 246
				// raw image data is in videoNode.Content
				if ( 'Content' in videoNode ) {
247

248 249
					const arrayBufferContent = ( videoNode.Content instanceof ArrayBuffer ) && ( videoNode.Content.byteLength > 0 );
					const base64Content = ( typeof videoNode.Content === 'string' ) && ( videoNode.Content !== '' );
250

251
					if ( arrayBufferContent || base64Content ) {
252

253
						const image = this.parseImage( videoNodes[ nodeID ] );
254

255
						blobs[ videoNode.RelativeFilename || videoNode.Filename ] = image;
256 257 258 259 260 261 262

					}

				}

			}

263
		}
264

265
		for ( const id in images ) {
266

267
			const filename = images[ id ];
268

269 270
			if ( blobs[ filename ] !== undefined ) images[ id ] = blobs[ filename ];
			else images[ id ] = images[ id ].split( '\\' ).pop();
271

272
		}
273

274
		return images;
275

276
	}
277

278 279
	// Parse embedded image data in FBXTree.Video.Content
	parseImage( videoNode ) {
280

281 282 283
		const content = videoNode.Content;
		const fileName = videoNode.RelativeFilename || videoNode.Filename;
		const extension = fileName.slice( fileName.lastIndexOf( '.' ) + 1 ).toLowerCase();
284

285
		let type;
286

287
		switch ( extension ) {
288

289
			case 'bmp':
290

291 292
				type = 'image/bmp';
				break;
293

294 295
			case 'jpg':
			case 'jpeg':
296

297 298
				type = 'image/jpeg';
				break;
299

300
			case 'png':
301

302 303
				type = 'image/png';
				break;
304

305
			case 'tif':
306

307 308
				type = 'image/tiff';
				break;
309

310
			case 'tga':
311

312
				if ( this.manager.getHandler( '.tga' ) === null ) {
313

314
					console.warn( 'FBXLoader: TGA loader not found, skipping ', fileName );
315

316
				}
L
Lewy Blue 已提交
317

318 319
				type = 'image/tga';
				break;
320

321
			default:
322

323 324
				console.warn( 'FBXLoader: Image type "' + extension + '" is not supported.' );
				return;
325

326
		}
327

328
		if ( typeof content === 'string' ) { // ASCII format
329

330
			return 'data:' + type + ';base64,' + content;
331

332
		} else { // Binary Format
333

334 335
			const array = new Uint8Array( content );
			return window.URL.createObjectURL( new Blob( [ array ], { type: type } ) );
336

337
		}
338

339
	}
340

341 342 343 344
	// Parse nodes in FBXTree.Objects.Texture
	// These contain details such as UV scaling, cropping, rotation etc and are connected
	// to images in FBXTree.Objects.Video
	parseTextures( images ) {
345

346
		const textureMap = new Map();
347

348
		if ( 'Texture' in fbxTree.Objects ) {
349

350 351
			const textureNodes = fbxTree.Objects.Texture;
			for ( const nodeID in textureNodes ) {
352

353 354
				const texture = this.parseTexture( textureNodes[ nodeID ], images );
				textureMap.set( parseInt( nodeID ), texture );
355 356 357

			}

358
		}
359

360
		return textureMap;
361

362
	}
363

364 365
	// Parse individual node in FBXTree.Objects.Texture
	parseTexture( textureNode, images ) {
366

367
		const texture = this.loadTexture( textureNode, images );
368

369
		texture.ID = textureNode.id;
370

371
		texture.name = textureNode.attrName;
372

373 374
		const wrapModeU = textureNode.WrapModeU;
		const wrapModeV = textureNode.WrapModeV;
375

376 377
		const valueU = wrapModeU !== undefined ? wrapModeU.value : 0;
		const valueV = wrapModeV !== undefined ? wrapModeV.value : 0;
378

379 380
		// http://download.autodesk.com/us/fbx/SDKdocs/FBX_SDK_Help/files/fbxsdkref/class_k_fbx_texture.html#889640e63e2e681259ea81061b85143a
		// 0: repeat(default), 1: clamp
381

382 383
		texture.wrapS = valueU === 0 ? RepeatWrapping : ClampToEdgeWrapping;
		texture.wrapT = valueV === 0 ? RepeatWrapping : ClampToEdgeWrapping;
384

385
		if ( 'Scaling' in textureNode ) {
386

387
			const values = textureNode.Scaling.value;
388

389 390
			texture.repeat.x = values[ 0 ];
			texture.repeat.y = values[ 1 ];
391

392
		}
393

394
		return texture;
395

396
	}
397

398 399
	// load a texture specified as a blob or data URI, or via an external URL using TextureLoader
	loadTexture( textureNode, images ) {
400

401
		let fileName;
402

403
		const currentPath = this.textureLoader.path;
404

405
		const children = connections.get( textureNode.id ).children;
406

407
		if ( children !== undefined && children.length > 0 && images[ children[ 0 ].ID ] !== undefined ) {
408

409
			fileName = images[ children[ 0 ].ID ];
410

411
			if ( fileName.indexOf( 'blob:' ) === 0 || fileName.indexOf( 'data:' ) === 0 ) {
412

413
				this.textureLoader.setPath( undefined );
414 415 416

			}

417
		}
418

419
		let texture;
420

421
		const extension = textureNode.FileName.slice( - 3 ).toLowerCase();
422

423
		if ( extension === 'tga' ) {
424

425
			const loader = this.manager.getHandler( '.tga' );
426

427
			if ( loader === null ) {
428

429 430
				console.warn( 'FBXLoader: TGA loader not found, creating placeholder texture for', textureNode.RelativeFilename );
				texture = new Texture();
431

432
			} else {
433

434
				texture = loader.load( fileName );
435

436
			}
437

438
		} else if ( extension === 'psd' ) {
439

440 441
			console.warn( 'FBXLoader: PSD textures are not supported, creating placeholder texture for', textureNode.RelativeFilename );
			texture = new Texture();
442

443
		} else {
444

445
			texture = this.textureLoader.load( fileName );
446

447
		}
448

449
		this.textureLoader.setPath( currentPath );
450

451
		return texture;
452

453
	}
454

455 456
	// Parse nodes in FBXTree.Objects.Material
	parseMaterials( textureMap ) {
457

458
		const materialMap = new Map();
459

460
		if ( 'Material' in fbxTree.Objects ) {
461

462
			const materialNodes = fbxTree.Objects.Material;
463

464
			for ( const nodeID in materialNodes ) {
465

466
				const material = this.parseMaterial( materialNodes[ nodeID ], textureMap );
467

468
				if ( material !== null ) materialMap.set( parseInt( nodeID ), material );
469 470 471

			}

472
		}
473

474
		return materialMap;
475

476
	}
477

478 479 480 481
	// Parse single node in FBXTree.Objects.Material
	// Materials are connected to texture maps in FBXTree.Objects.Textures
	// FBX format currently only supports Lambert and Phong shading models
	parseMaterial( materialNode, textureMap ) {
482

483 484 485
		const ID = materialNode.id;
		const name = materialNode.attrName;
		let type = materialNode.ShadingModel;
486

487 488
		// Case where FBX wraps shading model in property object.
		if ( typeof type === 'object' ) {
489

490
			type = type.value;
491

492
		}
493

494 495
		// Ignore unused materials which don't have any connections.
		if ( ! connections.has( ID ) ) return null;
496

497
		const parameters = this.parseParameters( materialNode, textureMap, ID );
498

499
		let material;
500

501
		switch ( type.toLowerCase() ) {
502

503 504 505 506 507 508 509 510 511 512
			case 'phong':
				material = new MeshPhongMaterial();
				break;
			case 'lambert':
				material = new MeshLambertMaterial();
				break;
			default:
				console.warn( 'THREE.FBXLoader: unknown material type "%s". Defaulting to MeshPhongMaterial.', type );
				material = new MeshPhongMaterial();
				break;
513

514
		}
515

516 517
		material.setValues( parameters );
		material.name = name;
518

519
		return material;
520

521
	}
522

523 524 525
	// Parse FBX material and return parameters suitable for a three.js material
	// Also parse the texture map and return any textures associated with the material
	parseParameters( materialNode, textureMap, ID ) {
526

527
		const parameters = {};
528

529
		if ( materialNode.BumpFactor ) {
530

531
			parameters.bumpScale = materialNode.BumpFactor.value;
M
Mugen87 已提交
532

533
		}
534

535
		if ( materialNode.Diffuse ) {
536

537
			parameters.color = new Color().fromArray( materialNode.Diffuse.value );
538

539
		} else if ( materialNode.DiffuseColor && ( materialNode.DiffuseColor.type === 'Color' || materialNode.DiffuseColor.type === 'ColorRGB' ) ) {
540

541 542
			// The blender exporter exports diffuse here instead of in materialNode.Diffuse
			parameters.color = new Color().fromArray( materialNode.DiffuseColor.value );
543

544
		}
545

546
		if ( materialNode.DisplacementFactor ) {
547

548
			parameters.displacementScale = materialNode.DisplacementFactor.value;
549

550
		}
551

552
		if ( materialNode.Emissive ) {
553

554
			parameters.emissive = new Color().fromArray( materialNode.Emissive.value );
555

556
		} else if ( materialNode.EmissiveColor && ( materialNode.EmissiveColor.type === 'Color' || materialNode.EmissiveColor.type === 'ColorRGB' ) ) {
557

558 559
			// The blender exporter exports emissive color here instead of in materialNode.Emissive
			parameters.emissive = new Color().fromArray( materialNode.EmissiveColor.value );
560

561
		}
562

563
		if ( materialNode.EmissiveFactor ) {
564

565
			parameters.emissiveIntensity = parseFloat( materialNode.EmissiveFactor.value );
566

567
		}
568

569
		if ( materialNode.Opacity ) {
570

571
			parameters.opacity = parseFloat( materialNode.Opacity.value );
572

573
		}
574

575
		if ( parameters.opacity < 1.0 ) {
576

577
			parameters.transparent = true;
578

579
		}
580

581
		if ( materialNode.ReflectionFactor ) {
582

583
			parameters.reflectivity = materialNode.ReflectionFactor.value;
584

585
		}
586

587
		if ( materialNode.Shininess ) {
588

589
			parameters.shininess = materialNode.Shininess.value;
590

591
		}
592

593
		if ( materialNode.Specular ) {
594

595
			parameters.specular = new Color().fromArray( materialNode.Specular.value );
596

597
		} else if ( materialNode.SpecularColor && materialNode.SpecularColor.type === 'Color' ) {
598

599 600
			// The blender exporter exports specular color here instead of in materialNode.Specular
			parameters.specular = new Color().fromArray( materialNode.SpecularColor.value );
601

602
		}
603

604 605
		const scope = this;
		connections.get( ID ).children.forEach( function ( child ) {
606

607
			const type = child.relationship;
608

609
			switch ( type ) {
610

611 612 613
				case 'Bump':
					parameters.bumpMap = scope.getTexture( textureMap, child.ID );
					break;
614

615 616 617
				case 'Maya|TEX_ao_map':
					parameters.aoMap = scope.getTexture( textureMap, child.ID );
					break;
618

619 620 621 622 623
				case 'DiffuseColor':
				case 'Maya|TEX_color_map':
					parameters.map = scope.getTexture( textureMap, child.ID );
					parameters.map.encoding = sRGBEncoding;
					break;
624

625 626 627
				case 'DisplacementColor':
					parameters.displacementMap = scope.getTexture( textureMap, child.ID );
					break;
628

629 630 631 632
				case 'EmissiveColor':
					parameters.emissiveMap = scope.getTexture( textureMap, child.ID );
					parameters.emissiveMap.encoding = sRGBEncoding;
					break;
633

634 635 636 637
				case 'NormalMap':
				case 'Maya|TEX_normal_map':
					parameters.normalMap = scope.getTexture( textureMap, child.ID );
					break;
638

639 640 641 642 643
				case 'ReflectionColor':
					parameters.envMap = scope.getTexture( textureMap, child.ID );
					parameters.envMap.mapping = EquirectangularReflectionMapping;
					parameters.envMap.encoding = sRGBEncoding;
					break;
644

645 646 647 648
				case 'SpecularColor':
					parameters.specularMap = scope.getTexture( textureMap, child.ID );
					parameters.specularMap.encoding = sRGBEncoding;
					break;
649

650 651 652 653 654
				case 'TransparentColor':
				case 'TransparencyFactor':
					parameters.alphaMap = scope.getTexture( textureMap, child.ID );
					parameters.transparent = true;
					break;
655

656 657 658 659 660 661 662
				case 'AmbientColor':
				case 'ShininessExponent': // AKA glossiness map
				case 'SpecularFactor': // AKA specularLevel
				case 'VectorDisplacementColor': // NOTE: Seems to be a copy of DisplacementColor
				default:
					console.warn( 'THREE.FBXLoader: %s map is not supported in three.js, skipping texture.', type );
					break;
663

664
			}
665

666
		} );
667

668
		return parameters;
669

670
	}
671

672 673
	// get a texture from the textureMap for use by a material.
	getTexture( textureMap, id ) {
674

675 676
		// if the texture is a layered texture, just use the first layer and issue a warning
		if ( 'LayeredTexture' in fbxTree.Objects && id in fbxTree.Objects.LayeredTexture ) {
677

678 679
			console.warn( 'THREE.FBXLoader: layered textures are not supported in three.js. Discarding all but first layer.' );
			id = connections.get( id ).children[ 0 ].ID;
680

681
		}
682

683
		return textureMap.get( id );
684

685
	}
686

687 688 689 690
	// Parse nodes in FBXTree.Objects.Deformer
	// Deformer node can contain skinning or Vertex Cache animation data, however only skinning is supported here
	// Generates map of Skeleton-like objects for use later when generating and binding skeletons.
	parseDeformers() {
691

692 693
		const skeletons = {};
		const morphTargets = {};
694

695
		if ( 'Deformer' in fbxTree.Objects ) {
696

697
			const DeformerNodes = fbxTree.Objects.Deformer;
698

699
			for ( const nodeID in DeformerNodes ) {
700

701
				const deformerNode = DeformerNodes[ nodeID ];
702

703
				const relationships = connections.get( parseInt( nodeID ) );
704

705
				if ( deformerNode.attrType === 'Skin' ) {
706

707 708
					const skeleton = this.parseSkeleton( relationships, DeformerNodes );
					skeleton.ID = nodeID;
709

710 711
					if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: skeleton attached to more than one geometry is not supported.' );
					skeleton.geometryID = relationships.parents[ 0 ].ID;
712

713
					skeletons[ nodeID ] = skeleton;
714

715
				} else if ( deformerNode.attrType === 'BlendShape' ) {
716

717 718 719
					const morphTarget = {
						id: nodeID,
					};
720

721 722
					morphTarget.rawTargets = this.parseMorphTargets( relationships, DeformerNodes );
					morphTarget.id = nodeID;
723

724
					if ( relationships.parents.length > 1 ) console.warn( 'THREE.FBXLoader: morph target attached to more than one geometry is not supported.' );
725

726
					morphTargets[ nodeID ] = morphTarget;
727 728 729 730 731

				}

			}

732
		}
733

734
		return {
735

736 737
			skeletons: skeletons,
			morphTargets: morphTargets,
738

739
		};
740

741
	}
742

743 744 745 746
	// Parse single nodes in FBXTree.Objects.Deformer
	// The top level skeleton node has type 'Skin' and sub nodes have type 'Cluster'
	// Each skin node represents a skeleton and each cluster node represents a bone
	parseSkeleton( relationships, deformerNodes ) {
747

748
		const rawBones = [];
749

750
		relationships.children.forEach( function ( child ) {
751

752
			const boneNode = deformerNodes[ child.ID ];
753

754
			if ( boneNode.attrType !== 'Cluster' ) return;
755

756
			const rawBone = {
757

758 759 760 761 762 763
				ID: child.ID,
				indices: [],
				weights: [],
				transformLink: new Matrix4().fromArray( boneNode.TransformLink.a ),
				// transform: new Matrix4().fromArray( boneNode.Transform.a ),
				// linkMode: boneNode.Mode,
764

765
			};
766

767
			if ( 'Indexes' in boneNode ) {
768

769 770
				rawBone.indices = boneNode.Indexes.a;
				rawBone.weights = boneNode.Weights.a;
771

772
			}
773

774
			rawBones.push( rawBone );
775

776
		} );
777

778
		return {
779

780 781
			rawBones: rawBones,
			bones: []
782

783
		};
784

785
	}
786

787 788
	// The top level morph deformer node has type "BlendShape" and sub nodes have type "BlendShapeChannel"
	parseMorphTargets( relationships, deformerNodes ) {
789

790
		const rawMorphTargets = [];
791

792
		for ( let i = 0; i < relationships.children.length; i ++ ) {
793

794
			const child = relationships.children[ i ];
795

796
			const morphTargetNode = deformerNodes[ child.ID ];
797

798
			const rawMorphTarget = {
799

800 801 802 803
				name: morphTargetNode.attrName,
				initialWeight: morphTargetNode.DeformPercent,
				id: morphTargetNode.id,
				fullWeights: morphTargetNode.FullWeights.a
804

805
			};
806

807
			if ( morphTargetNode.attrType !== 'BlendShapeChannel' ) return;
808

809
			rawMorphTarget.geoID = connections.get( parseInt( child.ID ) ).children.filter( function ( child ) {
810

811
				return child.relationship === undefined;
812

813
			} )[ 0 ].ID;
814

815
			rawMorphTargets.push( rawMorphTarget );
816

817
		}
818

819
		return rawMorphTargets;
820

821
	}
822

823 824
	// create the main Group() to be returned by the loader
	parseScene( deformers, geometryMap, materialMap ) {
825

826
		sceneGraph = new Group();
827

828
		const modelMap = this.parseModels( deformers.skeletons, geometryMap, materialMap );
829

830
		const modelNodes = fbxTree.Objects.Model;
831

832 833
		const scope = this;
		modelMap.forEach( function ( model ) {
834

835 836
			const modelNode = modelNodes[ model.ID ];
			scope.setLookAtProperties( model, modelNode );
837

838
			const parentConnections = connections.get( model.ID ).parents;
839

840
			parentConnections.forEach( function ( connection ) {
841

842 843
				const parent = modelMap.get( connection.ID );
				if ( parent !== undefined ) parent.add( model );
844

845
			} );
846

847
			if ( model.parent === null ) {
848

849
				sceneGraph.add( model );
850

851
			}
852 853


854
		} );
855

856
		this.bindSkeleton( deformers.skeletons, geometryMap, modelMap );
857

858
		this.createAmbientLight();
859

860
		this.setupMorphMaterials();
M
Mr.doob 已提交
861

862
		sceneGraph.traverse( function ( node ) {
M
Mr.doob 已提交
863

864
			if ( node.userData.transformData ) {
865

866
				if ( node.parent ) {
867

868 869
					node.userData.transformData.parentMatrix = node.parent.matrix;
					node.userData.transformData.parentMatrixWorld = node.parent.matrixWorld;
870 871 872

				}

873
				const transform = generateTransform( node.userData.transformData );
874

875 876
				node.applyMatrix4( transform );
				node.updateWorldMatrix();
877

878
			}
879

880
		} );
881

882
		const animations = new AnimationParser().parse();
883

884 885
		// if all the models where already combined in a single group, just return that
		if ( sceneGraph.children.length === 1 && sceneGraph.children[ 0 ].isGroup ) {
886

887 888
			sceneGraph.children[ 0 ].animations = animations;
			sceneGraph = sceneGraph.children[ 0 ];
889

890
		}
891

892
		sceneGraph.animations = animations;
893

894
	}
895

896 897
	// parse nodes in FBXTree.Objects.Model
	parseModels( skeletons, geometryMap, materialMap ) {
898

899 900
		const modelMap = new Map();
		const modelNodes = fbxTree.Objects.Model;
901

902
		for ( const nodeID in modelNodes ) {
903

904 905 906
			const id = parseInt( nodeID );
			const node = modelNodes[ nodeID ];
			const relationships = connections.get( id );
907

908
			let model = this.buildSkeleton( relationships, skeletons, id, node.attrName );
909

910
			if ( ! model ) {
911

912
				switch ( node.attrType ) {
913

914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933
					case 'Camera':
						model = this.createCamera( relationships );
						break;
					case 'Light':
						model = this.createLight( relationships );
						break;
					case 'Mesh':
						model = this.createMesh( relationships, geometryMap, materialMap );
						break;
					case 'NurbsCurve':
						model = this.createCurve( relationships, geometryMap );
						break;
					case 'LimbNode':
					case 'Root':
						model = new Bone();
						break;
					case 'Null':
					default:
						model = new Group();
						break;
934 935 936

				}

937 938 939
				model.name = node.attrName ? PropertyBinding.sanitizeNodeName( node.attrName ) : '';

				model.ID = id;
940 941 942

			}

943 944
			this.getTransformData( model, node );
			modelMap.set( id, model );
945

946
		}
947

948
		return modelMap;
949

950
	}
951

952
	buildSkeleton( relationships, skeletons, id, name ) {
953

954
		let bone = null;
955

956
		relationships.parents.forEach( function ( parent ) {
957

958
			for ( const ID in skeletons ) {
959

960
				const skeleton = skeletons[ ID ];
961

962
				skeleton.rawBones.forEach( function ( rawBone, i ) {
963

964
					if ( rawBone.ID === parent.ID ) {
965

966 967
						const subBone = bone;
						bone = new Bone();
968

969
						bone.matrixWorld.copy( rawBone.transformLink );
970

971
						// set name and id here - otherwise in cases where "subBone" is created it will not have a name / id
972

973 974
						bone.name = name ? PropertyBinding.sanitizeNodeName( name ) : '';
						bone.ID = id;
975

976
						skeleton.bones[ i ] = bone;
977

978 979 980 981 982
						// In cases where a bone is shared between multiple meshes
						// duplicate the bone here and and it as a child of the first bone
						if ( subBone !== null ) {

							bone.add( subBone );
983 984 985

						}

986
					}
987

988
				} );
989

990
			}
991

992
		} );
993

994
		return bone;
995

996
	}
997

998 999
	// create a PerspectiveCamera or OrthographicCamera
	createCamera( relationships ) {
1000

1001 1002
		let model;
		let cameraAttribute;
1003

1004
		relationships.children.forEach( function ( child ) {
1005

1006
			const attr = fbxTree.Objects.NodeAttribute[ child.ID ];
1007

1008
			if ( attr !== undefined ) {
1009

1010
				cameraAttribute = attr;
1011

1012
			}
1013

1014
		} );
1015

1016
		if ( cameraAttribute === undefined ) {
1017

1018
			model = new Object3D();
1019

1020
		} else {
1021

1022 1023
			let type = 0;
			if ( cameraAttribute.CameraProjectionType !== undefined && cameraAttribute.CameraProjectionType.value === 1 ) {
1024

1025
				type = 1;
1026

1027
			}
1028

1029 1030
			let nearClippingPlane = 1;
			if ( cameraAttribute.NearPlane !== undefined ) {
1031

1032
				nearClippingPlane = cameraAttribute.NearPlane.value / 1000;
1033

1034
			}
1035

1036 1037
			let farClippingPlane = 1000;
			if ( cameraAttribute.FarPlane !== undefined ) {
1038

1039
				farClippingPlane = cameraAttribute.FarPlane.value / 1000;
1040

1041
			}
1042 1043


1044 1045
			let width = window.innerWidth;
			let height = window.innerHeight;
1046

1047
			if ( cameraAttribute.AspectWidth !== undefined && cameraAttribute.AspectHeight !== undefined ) {
1048

1049 1050
				width = cameraAttribute.AspectWidth.value;
				height = cameraAttribute.AspectHeight.value;
1051

1052
			}
1053

1054
			const aspect = width / height;
1055

1056 1057
			let fov = 45;
			if ( cameraAttribute.FieldOfView !== undefined ) {
1058

1059
				fov = cameraAttribute.FieldOfView.value;
1060

1061
			}
1062

1063
			const focalLength = cameraAttribute.FocalLength ? cameraAttribute.FocalLength.value : null;
1064

1065
			switch ( type ) {
1066

1067 1068 1069 1070
				case 0: // Perspective
					model = new PerspectiveCamera( fov, aspect, nearClippingPlane, farClippingPlane );
					if ( focalLength !== null ) model.setFocalLength( focalLength );
					break;
1071

1072 1073 1074
				case 1: // Orthographic
					model = new OrthographicCamera( - width / 2, width / 2, height / 2, - height / 2, nearClippingPlane, farClippingPlane );
					break;
1075

1076 1077 1078 1079
				default:
					console.warn( 'THREE.FBXLoader: Unknown camera type ' + type + '.' );
					model = new Object3D();
					break;
1080 1081 1082

			}

1083
		}
1084

1085
		return model;
1086

1087
	}
1088

1089 1090
	// Create a DirectionalLight, PointLight or SpotLight
	createLight( relationships ) {
1091

1092 1093
		let model;
		let lightAttribute;
1094

1095
		relationships.children.forEach( function ( child ) {
1096

1097
			const attr = fbxTree.Objects.NodeAttribute[ child.ID ];
1098

1099
			if ( attr !== undefined ) {
1100

1101
				lightAttribute = attr;
1102

1103
			}
1104

1105
		} );
1106

1107
		if ( lightAttribute === undefined ) {
1108

1109
			model = new Object3D();
1110

1111
		} else {
1112

1113
			let type;
1114

1115 1116
			// LightType can be undefined for Point lights
			if ( lightAttribute.LightType === undefined ) {
1117

1118
				type = 0;
1119

1120
			} else {
1121

1122
				type = lightAttribute.LightType.value;
1123

1124
			}
1125

1126
			let color = 0xffffff;
1127

1128
			if ( lightAttribute.Color !== undefined ) {
1129

1130
				color = new Color().fromArray( lightAttribute.Color.value );
1131

1132
			}
1133

1134
			let intensity = ( lightAttribute.Intensity === undefined ) ? 1 : lightAttribute.Intensity.value / 100;
1135

1136 1137
			// light disabled
			if ( lightAttribute.CastLightOnObject !== undefined && lightAttribute.CastLightOnObject.value === 0 ) {
1138

1139
				intensity = 0;
1140

1141
			}
1142

1143 1144
			let distance = 0;
			if ( lightAttribute.FarAttenuationEnd !== undefined ) {
1145

1146
				if ( lightAttribute.EnableFarAttenuation !== undefined && lightAttribute.EnableFarAttenuation.value === 0 ) {
1147

1148
					distance = 0;
1149

1150
				} else {
1151

1152
					distance = lightAttribute.FarAttenuationEnd.value;
1153 1154 1155

				}

1156
			}
1157

1158 1159
			// TODO: could this be calculated linearly from FarAttenuationStart to FarAttenuationEnd?
			const decay = 1;
1160

1161
			switch ( type ) {
1162

1163 1164 1165
				case 0: // Point
					model = new PointLight( color, intensity, distance, decay );
					break;
1166

1167 1168 1169
				case 1: // Directional
					model = new DirectionalLight( color, intensity );
					break;
1170

1171 1172
				case 2: // Spot
					let angle = Math.PI / 3;
1173

1174
					if ( lightAttribute.InnerAngle !== undefined ) {
1175

1176
						angle = MathUtils.degToRad( lightAttribute.InnerAngle.value );
1177

1178
					}
1179

1180 1181
					let penumbra = 0;
					if ( lightAttribute.OuterAngle !== undefined ) {
1182

1183 1184 1185 1186 1187
						// TODO: this is not correct - FBX calculates outer and inner angle in degrees
						// with OuterAngle > InnerAngle && OuterAngle <= Math.PI
						// while three.js uses a penumbra between (0, 1) to attenuate the inner angle
						penumbra = MathUtils.degToRad( lightAttribute.OuterAngle.value );
						penumbra = Math.max( penumbra, 1 );
1188

1189
					}
1190

1191 1192
					model = new SpotLight( color, intensity, distance, angle, penumbra, decay );
					break;
1193

1194 1195 1196 1197
				default:
					console.warn( 'THREE.FBXLoader: Unknown light type ' + lightAttribute.LightType.value + ', defaulting to a PointLight.' );
					model = new PointLight( color, intensity );
					break;
1198

1199
			}
1200

1201
			if ( lightAttribute.CastShadows !== undefined && lightAttribute.CastShadows.value === 1 ) {
1202

1203
				model.castShadow = true;
1204 1205 1206

			}

1207
		}
1208

1209
		return model;
1210

1211
	}
1212

1213
	createMesh( relationships, geometryMap, materialMap ) {
1214

1215 1216 1217 1218
		let model;
		let geometry = null;
		let material = null;
		const materials = [];
1219

1220 1221
		// get geometry and materials(s) from connections
		relationships.children.forEach( function ( child ) {
1222

1223
			if ( geometryMap.has( child.ID ) ) {
1224

1225
				geometry = geometryMap.get( child.ID );
1226

1227
			}
1228

1229
			if ( materialMap.has( child.ID ) ) {
1230

1231
				materials.push( materialMap.get( child.ID ) );
1232

1233
			}
1234

1235
		} );
1236

1237
		if ( materials.length > 1 ) {
1238

1239
			material = materials;
1240

1241
		} else if ( materials.length > 0 ) {
1242

1243
			material = materials[ 0 ];
1244

1245
		} else {
1246

1247 1248
			material = new MeshPhongMaterial( { color: 0xcccccc } );
			materials.push( material );
1249

1250
		}
1251

1252
		if ( 'color' in geometry.attributes ) {
1253

1254
			materials.forEach( function ( material ) {
1255

1256
				material.vertexColors = true;
1257

1258
			} );
1259

1260
		}
1261

1262
		if ( geometry.FBX_Deformer ) {
1263

1264
			materials.forEach( function ( material ) {
1265

1266
				material.skinning = true;
1267

1268
			} );
1269

1270 1271
			model = new SkinnedMesh( geometry, material );
			model.normalizeSkinWeights();
1272

1273
		} else {
1274

1275
			model = new Mesh( geometry, material );
1276

1277
		}
1278

1279
		return model;
1280

1281
	}
1282

1283
	createCurve( relationships, geometryMap ) {
1284

1285
		const geometry = relationships.children.reduce( function ( geo, child ) {
1286

1287
			if ( geometryMap.has( child.ID ) ) geo = geometryMap.get( child.ID );
1288

1289
			return geo;
1290

1291
		}, null );
1292

1293 1294 1295
		// FBX does not list materials for Nurbs lines, so we'll just put our own in here.
		const material = new LineBasicMaterial( { color: 0x3300ff, linewidth: 1 } );
		return new Line( geometry, material );
1296

1297
	}
1298

1299 1300
	// parse the model node for transform data
	getTransformData( model, modelNode ) {
1301

1302
		const transformData = {};
1303

1304
		if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value );
1305

1306 1307
		if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value );
		else transformData.eulerOrder = 'ZYX';
1308

1309
		if ( 'Lcl_Translation' in modelNode ) transformData.translation = modelNode.Lcl_Translation.value;
1310

1311 1312 1313
		if ( 'PreRotation' in modelNode ) transformData.preRotation = modelNode.PreRotation.value;
		if ( 'Lcl_Rotation' in modelNode ) transformData.rotation = modelNode.Lcl_Rotation.value;
		if ( 'PostRotation' in modelNode ) transformData.postRotation = modelNode.PostRotation.value;
1314

1315
		if ( 'Lcl_Scaling' in modelNode ) transformData.scale = modelNode.Lcl_Scaling.value;
1316

1317 1318
		if ( 'ScalingOffset' in modelNode ) transformData.scalingOffset = modelNode.ScalingOffset.value;
		if ( 'ScalingPivot' in modelNode ) transformData.scalingPivot = modelNode.ScalingPivot.value;
1319

1320 1321
		if ( 'RotationOffset' in modelNode ) transformData.rotationOffset = modelNode.RotationOffset.value;
		if ( 'RotationPivot' in modelNode ) transformData.rotationPivot = modelNode.RotationPivot.value;
1322

1323
		model.userData.transformData = transformData;
1324

1325
	}
1326

1327
	setLookAtProperties( model, modelNode ) {
1328

1329
		if ( 'LookAtProperty' in modelNode ) {
1330

1331
			const children = connections.get( model.ID ).children;
1332

1333
			children.forEach( function ( child ) {
1334

1335
				if ( child.relationship === 'LookAtProperty' ) {
1336

1337
					const lookAtTarget = fbxTree.Objects.Model[ child.ID ];
1338

1339
					if ( 'Lcl_Translation' in lookAtTarget ) {
1340

1341
						const pos = lookAtTarget.Lcl_Translation.value;
1342

1343 1344
						// DirectionalLight, SpotLight
						if ( model.target !== undefined ) {
1345

1346 1347
							model.target.position.fromArray( pos );
							sceneGraph.add( model.target );
1348

1349
						} else { // Cameras and other Object3Ds
1350

1351
							model.lookAt( new Vector3().fromArray( pos ) );
1352 1353 1354 1355 1356

						}

					}

1357
				}
1358

1359
			} );
1360

1361
		}
1362

1363
	}
1364

1365
	bindSkeleton( skeletons, geometryMap, modelMap ) {
1366

1367
		const bindMatrices = this.parsePoseNodes();
1368

1369
		for ( const ID in skeletons ) {
1370

1371
			const skeleton = skeletons[ ID ];
1372

1373
			const parents = connections.get( parseInt( skeleton.ID ) ).parents;
1374

1375
			parents.forEach( function ( parent ) {
1376

1377
				if ( geometryMap.has( parent.ID ) ) {
1378

1379 1380
					const geoID = parent.ID;
					const geoRelationships = connections.get( geoID );
1381

1382
					geoRelationships.parents.forEach( function ( geoConnParent ) {
1383

1384
						if ( modelMap.has( geoConnParent.ID ) ) {
1385

1386
							const model = modelMap.get( geoConnParent.ID );
1387

1388
							model.bind( new Skeleton( skeleton.bones ), bindMatrices[ geoConnParent.ID ] );
1389

1390
						}
1391

1392
					} );
1393

1394
				}
1395

1396
			} );
1397

1398
		}
1399

1400
	}
1401

1402
	parsePoseNodes() {
1403

1404
		const bindMatrices = {};
1405

1406
		if ( 'Pose' in fbxTree.Objects ) {
1407

1408
			const BindPoseNode = fbxTree.Objects.Pose;
1409

1410
			for ( const nodeID in BindPoseNode ) {
1411

1412
				if ( BindPoseNode[ nodeID ].attrType === 'BindPose' ) {
1413

1414
					const poseNodes = BindPoseNode[ nodeID ].PoseNode;
1415

1416
					if ( Array.isArray( poseNodes ) ) {
1417

1418
						poseNodes.forEach( function ( poseNode ) {
1419

1420
							bindMatrices[ poseNode.Node ] = new Matrix4().fromArray( poseNode.Matrix.a );
1421

1422
						} );
1423

1424
					} else {
1425

1426
						bindMatrices[ poseNodes.Node ] = new Matrix4().fromArray( poseNodes.Matrix.a );
1427 1428 1429 1430 1431 1432 1433

					}

				}

			}

1434
		}
1435

1436
		return bindMatrices;
1437

1438
	}
1439

1440 1441
	// Parse ambient color in FBXTree.GlobalSettings - if it's not set to black (default), create an ambient light
	createAmbientLight() {
1442

1443
		if ( 'GlobalSettings' in fbxTree && 'AmbientColor' in fbxTree.GlobalSettings ) {
1444

1445 1446 1447 1448
			const ambientColor = fbxTree.GlobalSettings.AmbientColor.value;
			const r = ambientColor[ 0 ];
			const g = ambientColor[ 1 ];
			const b = ambientColor[ 2 ];
1449

1450
			if ( r !== 0 || g !== 0 || b !== 0 ) {
1451

1452 1453
				const color = new Color( r, g, b );
				sceneGraph.add( new AmbientLight( color, 1 ) );
1454 1455 1456

			}

1457
		}
1458

1459
	}
1460

1461
	setupMorphMaterials() {
1462

1463 1464
		const scope = this;
		sceneGraph.traverse( function ( child ) {
1465

1466
			if ( child.isMesh ) {
1467

1468
				if ( child.geometry.morphAttributes.position && child.geometry.morphAttributes.position.length ) {
1469

1470
					if ( Array.isArray( child.material ) ) {
1471

1472
						child.material.forEach( function ( material, i ) {
1473

1474
							scope.setupMorphMaterial( child, material, i );
1475

1476
						} );
1477

1478
					} else {
1479

1480
						scope.setupMorphMaterial( child, child.material );
1481 1482 1483 1484 1485

					}

				}

1486
			}
1487

1488
		} );
1489

1490
	}
1491

1492
	setupMorphMaterial( child, material, index ) {
1493

1494 1495
		const uuid = child.uuid;
		const matUuid = material.uuid;
1496

1497 1498
		// if a geometry has morph targets, it cannot share the material with other geometries
		let sharedMat = false;
1499

1500
		sceneGraph.traverse( function ( node ) {
1501

1502
			if ( node.isMesh ) {
1503

1504
				if ( Array.isArray( node.material ) ) {
1505

1506
					node.material.forEach( function ( mat ) {
1507

1508
						if ( mat.uuid === matUuid && node.uuid !== uuid ) sharedMat = true;
1509

1510
					} );
1511

1512
				} else if ( node.material.uuid === matUuid && node.uuid !== uuid ) sharedMat = true;
1513

1514
			}
1515

1516
		} );
1517

1518
		if ( sharedMat === true ) {
1519

1520 1521
			const clonedMat = material.clone();
			clonedMat.morphTargets = true;
1522

1523 1524
			if ( index === undefined ) child.material = clonedMat;
			else child.material[ index ] = clonedMat;
1525

1526
		} else material.morphTargets = true;
1527

1528
	}
1529

1530
}
1531

1532 1533
// parse Geometry data from FBXTree and return map of BufferGeometries
class GeometryParser {
1534

1535 1536
	// Parse nodes in FBXTree.Objects.Geometry
	parse( deformers ) {
1537

1538
		const geometryMap = new Map();
1539

1540
		if ( 'Geometry' in fbxTree.Objects ) {
1541

1542
			const geoNodes = fbxTree.Objects.Geometry;
1543

1544
			for ( const nodeID in geoNodes ) {
1545

1546 1547
				const relationships = connections.get( parseInt( nodeID ) );
				const geo = this.parseGeometry( relationships, geoNodes[ nodeID ], deformers );
1548

1549
				geometryMap.set( parseInt( nodeID ), geo );
1550 1551 1552

			}

1553
		}
1554

1555
		return geometryMap;
1556

1557
	}
1558

1559 1560
	// Parse single node in FBXTree.Objects.Geometry
	parseGeometry( relationships, geoNode, deformers ) {
1561

1562
		switch ( geoNode.attrType ) {
1563

1564 1565 1566
			case 'Mesh':
				return this.parseMeshGeometry( relationships, geoNode, deformers );
				break;
1567

1568 1569 1570
			case 'NurbsCurve':
				return this.parseNurbsGeometry( geoNode );
				break;
1571

1572
		}
L
Lewy Blue 已提交
1573

1574
	}
1575

1576 1577
	// Parse single node mesh geometry in FBXTree.Objects.Geometry
	parseMeshGeometry( relationships, geoNode, deformers ) {
1578

1579 1580
		const skeletons = deformers.skeletons;
		const morphTargets = [];
1581

1582
		const modelNodes = relationships.parents.map( function ( parent ) {
1583

1584
			return fbxTree.Objects.Model[ parent.ID ];
1585

1586
		} );
1587

1588 1589
		// don't create geometry if it is not associated with any models
		if ( modelNodes.length === 0 ) return;
1590

1591
		const skeleton = relationships.children.reduce( function ( skeleton, child ) {
1592

1593
			if ( skeletons[ child.ID ] !== undefined ) skeleton = skeletons[ child.ID ];
1594

1595
			return skeleton;
1596

1597
		}, null );
1598

1599
		relationships.children.forEach( function ( child ) {
1600

1601
			if ( deformers.morphTargets[ child.ID ] !== undefined ) {
L
Lewy Blue 已提交
1602

1603
				morphTargets.push( deformers.morphTargets[ child.ID ] );
1604

1605
			}
1606

1607
		} );
1608

1609 1610 1611
		// Assume one model and get the preRotation from that
		// if there is more than one model associated with the geometry this may cause problems
		const modelNode = modelNodes[ 0 ];
1612

1613
		const transformData = {};
1614

1615 1616
		if ( 'RotationOrder' in modelNode ) transformData.eulerOrder = getEulerOrder( modelNode.RotationOrder.value );
		if ( 'InheritType' in modelNode ) transformData.inheritType = parseInt( modelNode.InheritType.value );
1617

1618 1619 1620
		if ( 'GeometricTranslation' in modelNode ) transformData.translation = modelNode.GeometricTranslation.value;
		if ( 'GeometricRotation' in modelNode ) transformData.rotation = modelNode.GeometricRotation.value;
		if ( 'GeometricScaling' in modelNode ) transformData.scale = modelNode.GeometricScaling.value;
1621

1622
		const transform = generateTransform( transformData );
1623

1624
		return this.genGeometry( geoNode, skeleton, morphTargets, transform );
1625

1626
	}
1627

1628 1629
	// Generate a BufferGeometry from a node in FBXTree.Objects.Geometry
	genGeometry( geoNode, skeleton, morphTargets, preTransform ) {
1630

1631 1632
		const geo = new BufferGeometry();
		if ( geoNode.attrName ) geo.name = geoNode.attrName;
1633

1634 1635
		const geoInfo = this.parseGeoNode( geoNode, skeleton );
		const buffers = this.genBuffers( geoInfo );
1636

1637
		const positionAttribute = new Float32BufferAttribute( buffers.vertex, 3 );
1638

1639
		positionAttribute.applyMatrix4( preTransform );
1640

1641
		geo.setAttribute( 'position', positionAttribute );
1642

1643
		if ( buffers.colors.length > 0 ) {
1644

1645
			geo.setAttribute( 'color', new Float32BufferAttribute( buffers.colors, 3 ) );
1646

1647
		}
1648

1649
		if ( skeleton ) {
1650

1651
			geo.setAttribute( 'skinIndex', new Uint16BufferAttribute( buffers.weightsIndices, 4 ) );
1652

1653
			geo.setAttribute( 'skinWeight', new Float32BufferAttribute( buffers.vertexWeights, 4 ) );
1654

1655 1656
			// used later to bind the skeleton to the model
			geo.FBX_Deformer = skeleton;
1657

1658
		}
1659

1660
		if ( buffers.normal.length > 0 ) {
1661

1662
			const normalMatrix = new Matrix3().getNormalMatrix( preTransform );
1663

1664 1665
			const normalAttribute = new Float32BufferAttribute( buffers.normal, 3 );
			normalAttribute.applyNormalMatrix( normalMatrix );
1666

1667
			geo.setAttribute( 'normal', normalAttribute );
1668

1669
		}
1670

1671
		buffers.uvs.forEach( function ( uvBuffer, i ) {
1672

1673 1674
			// subsequent uv buffers are called 'uv1', 'uv2', ...
			let name = 'uv' + ( i + 1 ).toString();
1675

1676 1677
			// the first uv buffer is just called 'uv'
			if ( i === 0 ) {
1678

1679
				name = 'uv';
1680

1681
			}
1682

1683
			geo.setAttribute( name, new Float32BufferAttribute( buffers.uvs[ i ], 2 ) );
1684

1685
		} );
1686

1687
		if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
1688

1689 1690 1691
			// Convert the material indices of each vertex into rendering groups on the geometry.
			let prevMaterialIndex = buffers.materialIndex[ 0 ];
			let startIndex = 0;
1692

1693
			buffers.materialIndex.forEach( function ( currentIndex, i ) {
1694

1695
				if ( currentIndex !== prevMaterialIndex ) {
1696

1697
					geo.addGroup( startIndex, i - startIndex, prevMaterialIndex );
1698

1699 1700
					prevMaterialIndex = currentIndex;
					startIndex = i;
1701

1702
				}
1703

1704
			} );
1705

1706 1707
			// the loop above doesn't add the last group, do that here.
			if ( geo.groups.length > 0 ) {
1708

1709 1710
				const lastGroup = geo.groups[ geo.groups.length - 1 ];
				const lastIndex = lastGroup.start + lastGroup.count;
1711

1712
				if ( lastIndex !== buffers.materialIndex.length ) {
1713

1714
					geo.addGroup( lastIndex, buffers.materialIndex.length - lastIndex, prevMaterialIndex );
1715 1716 1717

				}

1718
			}
1719

1720 1721 1722
			// case where there are multiple materials but the whole geometry is only
			// using one of them
			if ( geo.groups.length === 0 ) {
1723

1724
				geo.addGroup( 0, buffers.materialIndex.length, buffers.materialIndex[ 0 ] );
1725 1726 1727

			}

1728
		}
1729

1730
		this.addMorphTargets( geo, geoNode, morphTargets, preTransform );
1731

1732
		return geo;
1733

1734
	}
1735

1736
	parseGeoNode( geoNode, skeleton ) {
1737

1738
		const geoInfo = {};
1739

1740 1741
		geoInfo.vertexPositions = ( geoNode.Vertices !== undefined ) ? geoNode.Vertices.a : [];
		geoInfo.vertexIndices = ( geoNode.PolygonVertexIndex !== undefined ) ? geoNode.PolygonVertexIndex.a : [];
1742

1743
		if ( geoNode.LayerElementColor ) {
1744

1745
			geoInfo.color = this.parseVertexColors( geoNode.LayerElementColor[ 0 ] );
1746

1747
		}
1748

1749
		if ( geoNode.LayerElementMaterial ) {
1750

1751
			geoInfo.material = this.parseMaterialIndices( geoNode.LayerElementMaterial[ 0 ] );
1752

1753
		}
1754

1755
		if ( geoNode.LayerElementNormal ) {
1756

1757
			geoInfo.normal = this.parseNormals( geoNode.LayerElementNormal[ 0 ] );
1758

1759
		}
1760

1761
		if ( geoNode.LayerElementUV ) {
1762

1763
			geoInfo.uv = [];
1764

1765 1766
			let i = 0;
			while ( geoNode.LayerElementUV[ i ] ) {
1767

1768
				if ( geoNode.LayerElementUV[ i ].UV ) {
1769

1770
					geoInfo.uv.push( this.parseUVs( geoNode.LayerElementUV[ i ] ) );
1771 1772 1773

				}

1774 1775
				i ++;

1776 1777
			}

1778
		}
1779

1780
		geoInfo.weightTable = {};
1781

1782
		if ( skeleton !== null ) {
1783

1784
			geoInfo.skeleton = skeleton;
1785

1786
			skeleton.rawBones.forEach( function ( rawBone, i ) {
1787

1788 1789
				// loop over the bone's vertex indices and weights
				rawBone.indices.forEach( function ( index, j ) {
1790

1791
					if ( geoInfo.weightTable[ index ] === undefined ) geoInfo.weightTable[ index ] = [];
1792

1793
					geoInfo.weightTable[ index ].push( {
1794

1795 1796
						id: i,
						weight: rawBone.weights[ j ],
1797 1798 1799 1800 1801

					} );

				} );

1802
			} );
1803

1804
		}
1805

1806
		return geoInfo;
1807

1808
	}
1809

1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850
	genBuffers( geoInfo ) {

		const buffers = {
			vertex: [],
			normal: [],
			colors: [],
			uvs: [],
			materialIndex: [],
			vertexWeights: [],
			weightsIndices: [],
		};

		let polygonIndex = 0;
		let faceLength = 0;
		let displayedWeightsWarning = false;

		// these will hold data for a single face
		let facePositionIndexes = [];
		let faceNormals = [];
		let faceColors = [];
		let faceUVs = [];
		let faceWeights = [];
		let faceWeightIndices = [];

		const scope = this;
		geoInfo.vertexIndices.forEach( function ( vertexIndex, polygonVertexIndex ) {

			let materialIndex;
			let endOfFace = false;

			// Face index and vertex index arrays are combined in a single array
			// A cube with quad faces looks like this:
			// PolygonVertexIndex: *24 {
			//  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
			//  }
			// Negative numbers mark the end of a face - first face here is 0, 1, 3, -3
			// to find index of last vertex bit shift the index: ^ - 1
			if ( vertexIndex < 0 ) {

				vertexIndex = vertexIndex ^ - 1; // equivalent to ( x * -1 ) - 1
				endOfFace = true;
1851

1852
			}
1853

1854 1855
			let weightIndices = [];
			let weights = [];
1856

1857
			facePositionIndexes.push( vertexIndex * 3, vertexIndex * 3 + 1, vertexIndex * 3 + 2 );
1858

1859
			if ( geoInfo.color ) {
1860

1861
				const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.color );
1862

1863
				faceColors.push( data[ 0 ], data[ 1 ], data[ 2 ] );
1864

1865
			}
1866

1867
			if ( geoInfo.skeleton ) {
1868

1869
				if ( geoInfo.weightTable[ vertexIndex ] !== undefined ) {
1870

1871
					geoInfo.weightTable[ vertexIndex ].forEach( function ( wt ) {
1872

1873 1874
						weights.push( wt.weight );
						weightIndices.push( wt.id );
1875

1876
					} );
1877 1878


1879
				}
1880

1881
				if ( weights.length > 4 ) {
1882

1883
					if ( ! displayedWeightsWarning ) {
1884

1885 1886
						console.warn( 'THREE.FBXLoader: Vertex has more than 4 skinning weights assigned to vertex. Deleting additional weights.' );
						displayedWeightsWarning = true;
1887 1888 1889

					}

1890 1891
					const wIndex = [ 0, 0, 0, 0 ];
					const Weight = [ 0, 0, 0, 0 ];
1892

1893
					weights.forEach( function ( weight, weightIndex ) {
1894

1895 1896
						let currentWeight = weight;
						let currentIndex = weightIndices[ weightIndex ];
1897

1898
						Weight.forEach( function ( comparedWeight, comparedWeightIndex, comparedWeightArray ) {
1899

1900
							if ( currentWeight > comparedWeight ) {
1901

1902 1903
								comparedWeightArray[ comparedWeightIndex ] = currentWeight;
								currentWeight = comparedWeight;
1904

1905 1906 1907
								const tmp = wIndex[ comparedWeightIndex ];
								wIndex[ comparedWeightIndex ] = currentIndex;
								currentIndex = tmp;
1908

1909
							}
1910 1911 1912

						} );

1913
					} );
1914

1915 1916
					weightIndices = wIndex;
					weights = Weight;
1917

1918
				}
1919

1920 1921
				// if the weight array is shorter than 4 pad with 0s
				while ( weights.length < 4 ) {
1922

1923 1924
					weights.push( 0 );
					weightIndices.push( 0 );
1925

1926
				}
1927

1928
				for ( let i = 0; i < 4; ++ i ) {
1929

1930 1931
					faceWeights.push( weights[ i ] );
					faceWeightIndices.push( weightIndices[ i ] );
1932 1933 1934

				}

1935
			}
1936

1937
			if ( geoInfo.normal ) {
1938

1939
				const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.normal );
1940

1941
				faceNormals.push( data[ 0 ], data[ 1 ], data[ 2 ] );
1942

1943
			}
1944

1945
			if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
1946

1947
				materialIndex = getData( polygonVertexIndex, polygonIndex, vertexIndex, geoInfo.material )[ 0 ];
1948

1949
			}
1950

1951
			if ( geoInfo.uv ) {
1952

1953
				geoInfo.uv.forEach( function ( uv, i ) {
1954

1955
					const data = getData( polygonVertexIndex, polygonIndex, vertexIndex, uv );
1956

1957
					if ( faceUVs[ i ] === undefined ) {
1958

1959
						faceUVs[ i ] = [];
1960

1961
					}
1962

1963 1964
					faceUVs[ i ].push( data[ 0 ] );
					faceUVs[ i ].push( data[ 1 ] );
1965

1966
				} );
1967

1968
			}
1969

1970
			faceLength ++;
1971

1972
			if ( endOfFace ) {
1973

1974
				scope.genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength );
1975

1976 1977
				polygonIndex ++;
				faceLength = 0;
1978

1979 1980 1981 1982 1983 1984 1985
				// reset arrays for the next face
				facePositionIndexes = [];
				faceNormals = [];
				faceColors = [];
				faceUVs = [];
				faceWeights = [];
				faceWeightIndices = [];
1986

1987
			}
1988

1989
		} );
1990

1991
		return buffers;
1992

1993
	}
1994

1995 1996
	// Generate data for a single face in a geometry. If the face is a quad then split it into 2 tris
	genFace( buffers, geoInfo, facePositionIndexes, materialIndex, faceNormals, faceColors, faceUVs, faceWeights, faceWeightIndices, faceLength ) {
1997

1998
		for ( let i = 2; i < faceLength; i ++ ) {
1999

2000 2001 2002
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 0 ] ] );
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 1 ] ] );
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ 2 ] ] );
2003

2004 2005 2006
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 ] ] );
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 1 ] ] );
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ ( i - 1 ) * 3 + 2 ] ] );
2007

2008 2009 2010
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 ] ] );
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 1 ] ] );
			buffers.vertex.push( geoInfo.vertexPositions[ facePositionIndexes[ i * 3 + 2 ] ] );
2011

2012
			if ( geoInfo.skeleton ) {
2013

2014 2015 2016 2017
				buffers.vertexWeights.push( faceWeights[ 0 ] );
				buffers.vertexWeights.push( faceWeights[ 1 ] );
				buffers.vertexWeights.push( faceWeights[ 2 ] );
				buffers.vertexWeights.push( faceWeights[ 3 ] );
2018

2019 2020 2021 2022
				buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 ] );
				buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 1 ] );
				buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 2 ] );
				buffers.vertexWeights.push( faceWeights[ ( i - 1 ) * 4 + 3 ] );
2023

2024 2025 2026 2027
				buffers.vertexWeights.push( faceWeights[ i * 4 ] );
				buffers.vertexWeights.push( faceWeights[ i * 4 + 1 ] );
				buffers.vertexWeights.push( faceWeights[ i * 4 + 2 ] );
				buffers.vertexWeights.push( faceWeights[ i * 4 + 3 ] );
2028

2029 2030 2031 2032
				buffers.weightsIndices.push( faceWeightIndices[ 0 ] );
				buffers.weightsIndices.push( faceWeightIndices[ 1 ] );
				buffers.weightsIndices.push( faceWeightIndices[ 2 ] );
				buffers.weightsIndices.push( faceWeightIndices[ 3 ] );
2033

2034 2035 2036 2037
				buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 ] );
				buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 1 ] );
				buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 2 ] );
				buffers.weightsIndices.push( faceWeightIndices[ ( i - 1 ) * 4 + 3 ] );
2038

2039 2040 2041 2042
				buffers.weightsIndices.push( faceWeightIndices[ i * 4 ] );
				buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 1 ] );
				buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 2 ] );
				buffers.weightsIndices.push( faceWeightIndices[ i * 4 + 3 ] );
2043

2044
			}
2045

2046
			if ( geoInfo.color ) {
2047

2048 2049 2050
				buffers.colors.push( faceColors[ 0 ] );
				buffers.colors.push( faceColors[ 1 ] );
				buffers.colors.push( faceColors[ 2 ] );
2051

2052 2053 2054
				buffers.colors.push( faceColors[ ( i - 1 ) * 3 ] );
				buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 1 ] );
				buffers.colors.push( faceColors[ ( i - 1 ) * 3 + 2 ] );
2055

2056 2057 2058
				buffers.colors.push( faceColors[ i * 3 ] );
				buffers.colors.push( faceColors[ i * 3 + 1 ] );
				buffers.colors.push( faceColors[ i * 3 + 2 ] );
2059

2060
			}
2061

2062
			if ( geoInfo.material && geoInfo.material.mappingType !== 'AllSame' ) {
2063

2064 2065 2066
				buffers.materialIndex.push( materialIndex );
				buffers.materialIndex.push( materialIndex );
				buffers.materialIndex.push( materialIndex );
2067

2068
			}
2069

2070
			if ( geoInfo.normal ) {
2071

2072 2073 2074
				buffers.normal.push( faceNormals[ 0 ] );
				buffers.normal.push( faceNormals[ 1 ] );
				buffers.normal.push( faceNormals[ 2 ] );
2075

2076 2077 2078
				buffers.normal.push( faceNormals[ ( i - 1 ) * 3 ] );
				buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 1 ] );
				buffers.normal.push( faceNormals[ ( i - 1 ) * 3 + 2 ] );
2079

2080 2081 2082
				buffers.normal.push( faceNormals[ i * 3 ] );
				buffers.normal.push( faceNormals[ i * 3 + 1 ] );
				buffers.normal.push( faceNormals[ i * 3 + 2 ] );
2083

2084
			}
2085

2086
			if ( geoInfo.uv ) {
2087

2088
				geoInfo.uv.forEach( function ( uv, j ) {
2089

2090
					if ( buffers.uvs[ j ] === undefined ) buffers.uvs[ j ] = [];
2091

2092 2093
					buffers.uvs[ j ].push( faceUVs[ j ][ 0 ] );
					buffers.uvs[ j ].push( faceUVs[ j ][ 1 ] );
2094

2095 2096
					buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 ] );
					buffers.uvs[ j ].push( faceUVs[ j ][ ( i - 1 ) * 2 + 1 ] );
2097

2098 2099
					buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 ] );
					buffers.uvs[ j ].push( faceUVs[ j ][ i * 2 + 1 ] );
2100

2101
				} );
2102 2103 2104

			}

2105
		}
2106

2107
	}
2108

2109
	addMorphTargets( parentGeo, parentGeoNode, morphTargets, preTransform ) {
2110

2111
		if ( morphTargets.length === 0 ) return;
2112

2113
		parentGeo.morphTargetsRelative = true;
2114

2115 2116
		parentGeo.morphAttributes.position = [];
		// parentGeo.morphAttributes.normal = []; // not implemented
2117

2118 2119
		const scope = this;
		morphTargets.forEach( function ( morphTarget ) {
2120

2121
			morphTarget.rawTargets.forEach( function ( rawTarget ) {
L
Lewy Blue 已提交
2122

2123
				const morphGeoNode = fbxTree.Objects.Geometry[ rawTarget.geoID ];
L
Lewy Blue 已提交
2124

2125
				if ( morphGeoNode !== undefined ) {
2126

2127
					scope.genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, rawTarget.name );
2128

2129
				}
2130 2131 2132

			} );

2133
		} );
2134

2135
	}
2136

2137 2138 2139 2140 2141
	// a morph geometry node is similar to a standard  node, and the node is also contained
	// in FBXTree.Objects.Geometry, however it can only have attributes for position, normal
	// and a special attribute Index defining which vertices of the original geometry are affected
	// Normal and position attributes only have data for the vertices that are affected by the morph
	genMorphGeometry( parentGeo, parentGeoNode, morphGeoNode, preTransform, name ) {
2142

2143
		const vertexIndices = ( parentGeoNode.PolygonVertexIndex !== undefined ) ? parentGeoNode.PolygonVertexIndex.a : [];
L
Lewy Blue 已提交
2144

2145 2146
		const morphPositionsSparse = ( morphGeoNode.Vertices !== undefined ) ? morphGeoNode.Vertices.a : [];
		const indices = ( morphGeoNode.Indexes !== undefined ) ? morphGeoNode.Indexes.a : [];
2147

2148 2149
		const length = parentGeo.attributes.position.count * 3;
		const morphPositions = new Float32Array( length );
2150

2151
		for ( let i = 0; i < indices.length; i ++ ) {
2152

2153
			const morphIndex = indices[ i ] * 3;
2154

2155 2156 2157
			morphPositions[ morphIndex ] = morphPositionsSparse[ i * 3 ];
			morphPositions[ morphIndex + 1 ] = morphPositionsSparse[ i * 3 + 1 ];
			morphPositions[ morphIndex + 2 ] = morphPositionsSparse[ i * 3 + 2 ];
2158

2159
		}
2160

2161 2162 2163 2164
		// TODO: add morph normal support
		const morphGeoInfo = {
			vertexIndices: vertexIndices,
			vertexPositions: morphPositions,
2165

2166
		};
2167

2168
		const morphBuffers = this.genBuffers( morphGeoInfo );
2169

2170 2171
		const positionAttribute = new Float32BufferAttribute( morphBuffers.vertex, 3 );
		positionAttribute.name = name || morphGeoNode.attrName;
2172

2173
		positionAttribute.applyMatrix4( preTransform );
2174

2175
		parentGeo.morphAttributes.position.push( positionAttribute );
2176

2177
	}
2178

2179 2180
	// Parse normal from FBXTree.Objects.Geometry.LayerElementNormal if it exists
	parseNormals( NormalNode ) {
2181

2182 2183 2184 2185 2186
		const mappingType = NormalNode.MappingInformationType;
		const referenceType = NormalNode.ReferenceInformationType;
		const buffer = NormalNode.Normals.a;
		let indexBuffer = [];
		if ( referenceType === 'IndexToDirect' ) {
2187

2188
			if ( 'NormalIndex' in NormalNode ) {
2189

2190
				indexBuffer = NormalNode.NormalIndex.a;
2191

2192
			} else if ( 'NormalsIndex' in NormalNode ) {
2193

2194
				indexBuffer = NormalNode.NormalsIndex.a;
2195 2196 2197

			}

2198
		}
2199

2200 2201 2202 2203 2204 2205 2206
		return {
			dataSize: 3,
			buffer: buffer,
			indices: indexBuffer,
			mappingType: mappingType,
			referenceType: referenceType
		};
2207

2208
	}
2209

2210 2211
	// Parse UVs from FBXTree.Objects.Geometry.LayerElementUV if it exists
	parseUVs( UVNode ) {
2212

2213 2214 2215 2216 2217
		const mappingType = UVNode.MappingInformationType;
		const referenceType = UVNode.ReferenceInformationType;
		const buffer = UVNode.UV.a;
		let indexBuffer = [];
		if ( referenceType === 'IndexToDirect' ) {
2218

2219
			indexBuffer = UVNode.UVIndex.a;
2220

2221
		}
2222

2223 2224 2225 2226 2227 2228 2229
		return {
			dataSize: 2,
			buffer: buffer,
			indices: indexBuffer,
			mappingType: mappingType,
			referenceType: referenceType
		};
2230

2231
	}
2232

2233 2234
	// Parse Vertex Colors from FBXTree.Objects.Geometry.LayerElementColor if it exists
	parseVertexColors( ColorNode ) {
2235

2236 2237 2238 2239 2240
		const mappingType = ColorNode.MappingInformationType;
		const referenceType = ColorNode.ReferenceInformationType;
		const buffer = ColorNode.Colors.a;
		let indexBuffer = [];
		if ( referenceType === 'IndexToDirect' ) {
2241

2242
			indexBuffer = ColorNode.ColorIndex.a;
2243

2244
		}
2245

2246 2247 2248 2249 2250 2251 2252
		return {
			dataSize: 4,
			buffer: buffer,
			indices: indexBuffer,
			mappingType: mappingType,
			referenceType: referenceType
		};
2253

2254
	}
2255

2256 2257
	// Parse mapping and material data in FBXTree.Objects.Geometry.LayerElementMaterial if it exists
	parseMaterialIndices( MaterialNode ) {
2258

2259 2260
		const mappingType = MaterialNode.MappingInformationType;
		const referenceType = MaterialNode.ReferenceInformationType;
2261

2262
		if ( mappingType === 'NoMappingInformation' ) {
2263 2264 2265

			return {
				dataSize: 1,
2266 2267 2268
				buffer: [ 0 ],
				indices: [ 0 ],
				mappingType: 'AllSame',
2269 2270 2271
				referenceType: referenceType
			};

2272
		}
2273

2274
		const materialIndexBuffer = MaterialNode.Materials.a;
2275

2276 2277 2278 2279
		// Since materials are stored as indices, there's a bit of a mismatch between FBX and what
		// we expect.So we create an intermediate buffer that points to the index in the buffer,
		// for conforming with the other functions we've written for other data.
		const materialIndices = [];
2280

2281
		for ( let i = 0; i < materialIndexBuffer.length; ++ i ) {
2282

2283
			materialIndices.push( i );
2284

2285
		}
2286

2287 2288 2289 2290 2291 2292 2293
		return {
			dataSize: 1,
			buffer: materialIndexBuffer,
			indices: materialIndices,
			mappingType: mappingType,
			referenceType: referenceType
		};
2294

2295
	}
2296

2297 2298
	// Generate a NurbGeometry from a node in FBXTree.Objects.Geometry
	parseNurbsGeometry( geoNode ) {
2299

2300
		if ( NURBSCurve === undefined ) {
2301

2302 2303
			console.error( 'THREE.FBXLoader: The loader relies on NURBSCurve for any nurbs present in the model. Nurbs will show up as empty geometry.' );
			return new BufferGeometry();
2304

2305
		}
2306

2307
		const order = parseInt( geoNode.Order );
2308

2309
		if ( isNaN( order ) ) {
2310

2311 2312
			console.error( 'THREE.FBXLoader: Invalid Order %s given for geometry ID: %s', geoNode.Order, geoNode.id );
			return new BufferGeometry();
2313

2314
		}
2315

2316
		const degree = order - 1;
2317

2318 2319 2320
		const knots = geoNode.KnotVector.a;
		const controlPoints = [];
		const pointsValues = geoNode.Points.a;
2321

2322
		for ( let i = 0, l = pointsValues.length; i < l; i += 4 ) {
2323

2324
			controlPoints.push( new Vector4().fromArray( pointsValues, i ) );
2325

2326
		}
2327

2328
		let startKnot, endKnot;
2329

2330
		if ( geoNode.Form === 'Closed' ) {
2331

2332
			controlPoints.push( controlPoints[ 0 ] );
2333

2334
		} else if ( geoNode.Form === 'Periodic' ) {
2335

2336 2337
			startKnot = degree;
			endKnot = knots.length - 1 - startKnot;
2338

2339
			for ( let i = 0; i < degree; ++ i ) {
2340

2341
				controlPoints.push( controlPoints[ i ] );
2342

2343
			}
2344

2345
		}
2346

2347 2348
		const curve = new NURBSCurve( degree, knots, controlPoints, startKnot, endKnot );
		const vertices = curve.getPoints( controlPoints.length * 7 );
2349

2350
		const positions = new Float32Array( vertices.length * 3 );
2351

2352
		vertices.forEach( function ( vertex, i ) {
2353

2354
			vertex.toArray( positions, i * 3 );
2355

2356
		} );
2357

2358 2359
		const geometry = new BufferGeometry();
		geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
2360

2361
		return geometry;
2362

2363
	}
2364

2365
}
2366

2367 2368
// parse animation data from FBXTree
class AnimationParser {
2369

2370 2371
	// take raw animation clips and turn them into three.js animation clips
	parse() {
2372

2373
		const animationClips = [];
2374

2375
		const rawClips = this.parseClips();
2376

2377
		if ( rawClips !== undefined ) {
2378

2379
			for ( const key in rawClips ) {
2380

2381
				const rawClip = rawClips[ key ];
2382

2383
				const clip = this.addClip( rawClip );
2384

2385
				animationClips.push( clip );
2386

2387
			}
2388

2389
		}
2390

2391
		return animationClips;
2392

2393
	}
2394

2395
	parseClips() {
2396

2397 2398 2399
		// since the actual transformation data is stored in FBXTree.Objects.AnimationCurve,
		// if this is undefined we can safely assume there are no animations
		if ( fbxTree.Objects.AnimationCurve === undefined ) return undefined;
2400

2401
		const curveNodesMap = this.parseAnimationCurveNodes();
2402

2403
		this.parseAnimationCurves( curveNodesMap );
2404

2405 2406
		const layersMap = this.parseAnimationLayers( curveNodesMap );
		const rawClips = this.parseAnimStacks( layersMap );
2407

2408
		return rawClips;
2409

2410
	}
2411

2412 2413 2414 2415
	// parse nodes in FBXTree.Objects.AnimationCurveNode
	// each AnimationCurveNode holds data for an animation transform for a model (e.g. left arm rotation )
	// and is referenced by an AnimationLayer
	parseAnimationCurveNodes() {
2416

2417
		const rawCurveNodes = fbxTree.Objects.AnimationCurveNode;
2418

2419
		const curveNodesMap = new Map();
2420

2421
		for ( const nodeID in rawCurveNodes ) {
2422

2423
			const rawCurveNode = rawCurveNodes[ nodeID ];
2424

2425
			if ( rawCurveNode.attrName.match( /S|R|T|DeformPercent/ ) !== null ) {
2426

2427
				const curveNode = {
2428

2429 2430 2431
					id: rawCurveNode.id,
					attr: rawCurveNode.attrName,
					curves: {},
2432 2433 2434

				};

2435
				curveNodesMap.set( curveNode.id, curveNode );
2436

2437
			}
2438

2439
		}
2440

2441
		return curveNodesMap;
2442

2443
	}
2444

2445 2446 2447 2448
	// parse nodes in FBXTree.Objects.AnimationCurve and connect them up to
	// previously parsed AnimationCurveNodes. Each AnimationCurve holds data for a single animated
	// axis ( e.g. times and values of x rotation)
	parseAnimationCurves( curveNodesMap ) {
2449

2450
		const rawCurves = fbxTree.Objects.AnimationCurve;
2451

2452 2453 2454 2455 2456 2457
		// TODO: Many values are identical up to roundoff error, but won't be optimised
		// e.g. position times: [0, 0.4, 0. 8]
		// position values: [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.23538335023477e-7, 93.67518615722656, -0.9982695579528809, 7.235384487103147e-7, 93.67520904541016, -0.9982695579528809]
		// clearly, this should be optimised to
		// times: [0], positions [7.23538335023477e-7, 93.67518615722656, -0.9982695579528809]
		// this shows up in nearly every FBX file, and generally time array is length > 100
2458

2459
		for ( const nodeID in rawCurves ) {
2460

2461
			const animationCurve = {
2462

2463 2464 2465
				id: rawCurves[ nodeID ].id,
				times: rawCurves[ nodeID ].KeyTime.a.map( convertFBXTimeToSeconds ),
				values: rawCurves[ nodeID ].KeyValueFloat.a,
2466

2467
			};
2468

2469
			const relationships = connections.get( animationCurve.id );
2470

2471
			if ( relationships !== undefined ) {
2472

2473 2474
				const animationCurveID = relationships.parents[ 0 ].ID;
				const animationCurveRelationship = relationships.parents[ 0 ].relationship;
2475

2476
				if ( animationCurveRelationship.match( /X/ ) ) {
2477

2478
					curveNodesMap.get( animationCurveID ).curves[ 'x' ] = animationCurve;
2479

2480
				} else if ( animationCurveRelationship.match( /Y/ ) ) {
2481

2482
					curveNodesMap.get( animationCurveID ).curves[ 'y' ] = animationCurve;
2483

2484
				} else if ( animationCurveRelationship.match( /Z/ ) ) {
2485

2486
					curveNodesMap.get( animationCurveID ).curves[ 'z' ] = animationCurve;
2487

2488
				} else if ( animationCurveRelationship.match( /d|DeformPercent/ ) && curveNodesMap.has( animationCurveID ) ) {
2489

2490
					curveNodesMap.get( animationCurveID ).curves[ 'morph' ] = animationCurve;
2491

2492
				}
2493

2494
			}
2495

2496
		}
2497

2498
	}
2499

2500 2501 2502 2503
	// parse nodes in FBXTree.Objects.AnimationLayer. Each layers holds references
	// to various AnimationCurveNodes and is referenced by an AnimationStack node
	// note: theoretically a stack can have multiple layers, however in practice there always seems to be one per stack
	parseAnimationLayers( curveNodesMap ) {
2504

2505
		const rawLayers = fbxTree.Objects.AnimationLayer;
2506

2507
		const layersMap = new Map();
2508

2509
		for ( const nodeID in rawLayers ) {
2510

2511
			const layerCurveNodes = [];
2512

2513
			const connection = connections.get( parseInt( nodeID ) );
M
Mr.doob 已提交
2514

2515
			if ( connection !== undefined ) {
M
Mr.doob 已提交
2516

2517 2518
				// all the animationCurveNodes used in the layer
				const children = connection.children;
2519

2520
				children.forEach( function ( child, i ) {
2521

2522
					if ( curveNodesMap.has( child.ID ) ) {
2523

2524
						const curveNode = curveNodesMap.get( child.ID );
2525

2526 2527
						// check that the curves are defined for at least one axis, otherwise ignore the curveNode
						if ( curveNode.curves.x !== undefined || curveNode.curves.y !== undefined || curveNode.curves.z !== undefined ) {
2528

2529
							if ( layerCurveNodes[ i ] === undefined ) {
2530

2531
								const modelID = connections.get( child.ID ).parents.filter( function ( parent ) {
2532

2533
									return parent.relationship !== undefined;
2534

2535
								} )[ 0 ].ID;
2536

2537
								if ( modelID !== undefined ) {
2538

2539
									const rawModel = fbxTree.Objects.Model[ modelID.toString() ];
2540

2541
									if ( rawModel === undefined ) {
2542

2543 2544
										console.warn( 'THREE.FBXLoader: Encountered a unused curve.', child );
										return;
2545 2546 2547

									}

2548
									const node = {
2549

2550 2551 2552 2553 2554
										modelName: rawModel.attrName ? PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '',
										ID: rawModel.id,
										initialPosition: [ 0, 0, 0 ],
										initialRotation: [ 0, 0, 0 ],
										initialScale: [ 1, 1, 1 ],
2555

2556
									};
2557

2558
									sceneGraph.traverse( function ( child ) {
2559

2560
										if ( child.ID === rawModel.id ) {
2561

2562
											node.transform = child.matrix;
2563

2564
											if ( child.userData.transformData ) node.eulerOrder = child.userData.transformData.eulerOrder;
2565

2566
										}
2567

2568
									} );
2569

2570
									if ( ! node.transform ) node.transform = new Matrix4();
2571

2572 2573 2574 2575
									// if the animated model is pre rotated, we'll have to apply the pre rotations to every
									// animation value as well
									if ( 'PreRotation' in rawModel ) node.preRotation = rawModel.PreRotation.value;
									if ( 'PostRotation' in rawModel ) node.postRotation = rawModel.PostRotation.value;
2576 2577 2578 2579 2580 2581 2582

									layerCurveNodes[ i ] = node;

								}

							}

2583
							if ( layerCurveNodes[ i ] ) layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
2584

2585
						} else if ( curveNode.curves.morph !== undefined ) {
2586

2587
							if ( layerCurveNodes[ i ] === undefined ) {
2588

2589
								const deformerID = connections.get( child.ID ).parents.filter( function ( parent ) {
2590

2591
									return parent.relationship !== undefined;
2592

2593
								} )[ 0 ].ID;
2594

2595 2596
								const morpherID = connections.get( deformerID ).parents[ 0 ].ID;
								const geoID = connections.get( morpherID ).parents[ 0 ].ID;
2597

2598 2599
								// assuming geometry is not used in more than one model
								const modelID = connections.get( geoID ).parents[ 0 ].ID;
2600

2601
								const rawModel = fbxTree.Objects.Model[ modelID ];
2602

2603
								const node = {
2604

2605 2606
									modelName: rawModel.attrName ? PropertyBinding.sanitizeNodeName( rawModel.attrName ) : '',
									morphName: fbxTree.Objects.Deformer[ deformerID ].attrName,
2607

2608
								};
2609

2610
								layerCurveNodes[ i ] = node;
2611

2612
							}
2613

2614
							layerCurveNodes[ i ][ curveNode.attr ] = curveNode;
2615

2616
						}
2617

2618
					}
2619

2620
				} );
2621

2622
				layersMap.set( parseInt( nodeID ), layerCurveNodes );
2623 2624 2625

			}

2626
		}
2627

2628
		return layersMap;
2629

2630
	}
2631

2632 2633 2634
	// parse nodes in FBXTree.Objects.AnimationStack. These are the top level node in the animation
	// hierarchy. Each Stack node will be used to create a AnimationClip
	parseAnimStacks( layersMap ) {
2635

2636
		const rawStacks = fbxTree.Objects.AnimationStack;
2637

2638 2639
		// connect the stacks (clips) up to the layers
		const rawClips = {};
2640

2641
		for ( const nodeID in rawStacks ) {
2642

2643
			const children = connections.get( parseInt( nodeID ) ).children;
2644

2645
			if ( children.length > 1 ) {
2646

2647 2648 2649
				// it seems like stacks will always be associated with a single layer. But just in case there are files
				// where there are multiple layers per stack, we'll display a warning
				console.warn( 'THREE.FBXLoader: Encountered an animation stack with multiple layers, this is currently not supported. Ignoring subsequent layers.' );
2650

2651
			}
2652

2653
			const layer = layersMap.get( children[ 0 ].ID );
2654

2655
			rawClips[ nodeID ] = {
2656

2657 2658
				name: rawStacks[ nodeID ].attrName,
				layer: layer,
2659

2660
			};
2661

2662
		}
2663

2664
		return rawClips;
2665

2666
	}
2667

2668
	addClip( rawClip ) {
2669

2670
		let tracks = [];
2671

2672 2673
		const scope = this;
		rawClip.layer.forEach( function ( rawTracks ) {
2674

2675
			tracks = tracks.concat( scope.generateTracks( rawTracks ) );
2676

2677
		} );
2678

2679
		return new AnimationClip( rawClip.name, - 1, tracks );
2680

2681
	}
2682

2683
	generateTracks( rawTracks ) {
2684

2685
		const tracks = [];
2686

2687 2688 2689
		let initialPosition = new Vector3();
		let initialRotation = new Quaternion();
		let initialScale = new Vector3();
2690

2691
		if ( rawTracks.transform ) rawTracks.transform.decompose( initialPosition, initialRotation, initialScale );
2692

2693 2694 2695
		initialPosition = initialPosition.toArray();
		initialRotation = new Euler().setFromQuaternion( initialRotation, rawTracks.eulerOrder ).toArray();
		initialScale = initialScale.toArray();
2696

2697
		if ( rawTracks.T !== undefined && Object.keys( rawTracks.T.curves ).length > 0 ) {
2698

2699 2700
			const positionTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.T.curves, initialPosition, 'position' );
			if ( positionTrack !== undefined ) tracks.push( positionTrack );
2701

2702
		}
M
Mugen87 已提交
2703

2704
		if ( rawTracks.R !== undefined && Object.keys( rawTracks.R.curves ).length > 0 ) {
2705

2706 2707
			const rotationTrack = this.generateRotationTrack( rawTracks.modelName, rawTracks.R.curves, initialRotation, rawTracks.preRotation, rawTracks.postRotation, rawTracks.eulerOrder );
			if ( rotationTrack !== undefined ) tracks.push( rotationTrack );
2708

2709
		}
M
Mugen87 已提交
2710

2711
		if ( rawTracks.S !== undefined && Object.keys( rawTracks.S.curves ).length > 0 ) {
2712

2713 2714
			const scaleTrack = this.generateVectorTrack( rawTracks.modelName, rawTracks.S.curves, initialScale, 'scale' );
			if ( scaleTrack !== undefined ) tracks.push( scaleTrack );
2715

2716
		}
2717

2718
		if ( rawTracks.DeformPercent !== undefined ) {
2719

2720 2721
			const morphTrack = this.generateMorphTrack( rawTracks );
			if ( morphTrack !== undefined ) tracks.push( morphTrack );
2722

2723
		}
2724

2725
		return tracks;
2726

2727
	}
2728

2729
	generateVectorTrack( modelName, curves, initialValue, type ) {
2730

2731 2732
		const times = this.getTimesForAllAxes( curves );
		const values = this.getKeyframeTrackValues( times, curves, initialValue );
2733

2734
		return new VectorKeyframeTrack( modelName + '.' + type, times, values );
2735

2736
	}
2737

2738
	generateRotationTrack( modelName, curves, initialValue, preRotation, postRotation, eulerOrder ) {
2739

2740
		if ( curves.x !== undefined ) {
2741

2742 2743
			this.interpolateRotations( curves.x );
			curves.x.values = curves.x.values.map( MathUtils.degToRad );
2744

2745
		}
2746

2747
		if ( curves.y !== undefined ) {
2748

2749 2750
			this.interpolateRotations( curves.y );
			curves.y.values = curves.y.values.map( MathUtils.degToRad );
2751

2752
		}
2753

2754
		if ( curves.z !== undefined ) {
2755

2756 2757
			this.interpolateRotations( curves.z );
			curves.z.values = curves.z.values.map( MathUtils.degToRad );
2758

2759
		}
2760

2761 2762
		const times = this.getTimesForAllAxes( curves );
		const values = this.getKeyframeTrackValues( times, curves, initialValue );
2763

2764
		if ( preRotation !== undefined ) {
2765

2766 2767
			preRotation = preRotation.map( MathUtils.degToRad );
			preRotation.push( eulerOrder );
2768

2769 2770
			preRotation = new Euler().fromArray( preRotation );
			preRotation = new Quaternion().setFromEuler( preRotation );
2771

2772
		}
2773

2774
		if ( postRotation !== undefined ) {
2775

2776 2777
			postRotation = postRotation.map( MathUtils.degToRad );
			postRotation.push( eulerOrder );
2778

2779 2780
			postRotation = new Euler().fromArray( postRotation );
			postRotation = new Quaternion().setFromEuler( postRotation ).invert();
2781

2782
		}
2783

2784 2785
		const quaternion = new Quaternion();
		const euler = new Euler();
2786

2787
		const quaternionValues = [];
2788

2789
		for ( let i = 0; i < values.length; i += 3 ) {
2790

2791
			euler.set( values[ i ], values[ i + 1 ], values[ i + 2 ], eulerOrder );
2792

2793
			quaternion.setFromEuler( euler );
2794

2795 2796
			if ( preRotation !== undefined ) quaternion.premultiply( preRotation );
			if ( postRotation !== undefined ) quaternion.multiply( postRotation );
K
Kimbatt 已提交
2797

2798
			quaternion.toArray( quaternionValues, ( i / 3 ) * 4 );
K
Kimbatt 已提交
2799

2800
		}
K
Kimbatt 已提交
2801

2802
		return new QuaternionKeyframeTrack( modelName + '.quaternion', times, quaternionValues );
K
Kimbatt 已提交
2803

2804
	}
K
Kimbatt 已提交
2805

2806
	generateMorphTrack( rawTracks ) {
K
Kimbatt 已提交
2807

2808 2809
		const curves = rawTracks.DeformPercent.curves.morph;
		const values = curves.values.map( function ( val ) {
2810

2811
			return val / 100;
2812

2813
		} );
2814

2815
		const morphNum = sceneGraph.getObjectByName( rawTracks.modelName ).morphTargetDictionary[ rawTracks.morphName ];
2816

2817
		return new NumberKeyframeTrack( rawTracks.modelName + '.morphTargetInfluences[' + morphNum + ']', curves.times, values );
2818

2819
	}
2820

2821 2822 2823
	// For all animated objects, times are defined separately for each axis
	// Here we'll combine the times into one sorted array without duplicates
	getTimesForAllAxes( curves ) {
2824

2825
		let times = [];
2826

2827 2828 2829 2830
		// first join together the times for each axis, if defined
		if ( curves.x !== undefined ) times = times.concat( curves.x.times );
		if ( curves.y !== undefined ) times = times.concat( curves.y.times );
		if ( curves.z !== undefined ) times = times.concat( curves.z.times );
2831

2832 2833
		// then sort them
		times = times.sort( function ( a, b ) {
2834

2835
			return a - b;
2836

2837
		} );
2838

2839 2840
		// and remove duplicates
		if ( times.length > 1 ) {
2841

2842 2843 2844
			let targetIndex = 1;
			let lastValue = times[ 0 ];
			for ( let i = 1; i < times.length; i ++ ) {
2845

2846 2847
				const currentValue = times[ i ];
				if ( currentValue !== lastValue ) {
2848

2849 2850 2851
					times[ targetIndex ] = currentValue;
					lastValue = currentValue;
					targetIndex ++;
2852

2853
				}
2854

2855
			}
2856

2857
			times = times.slice( 0, targetIndex );
2858

2859
		}
2860

2861
		return times;
2862

2863
	}
2864

2865
	getKeyframeTrackValues( times, curves, initialValue ) {
2866

2867
		const prevValue = initialValue;
2868

2869
		const values = [];
2870

2871 2872 2873
		let xIndex = - 1;
		let yIndex = - 1;
		let zIndex = - 1;
2874

2875
		times.forEach( function ( time ) {
2876

2877 2878 2879
			if ( curves.x ) xIndex = curves.x.times.indexOf( time );
			if ( curves.y ) yIndex = curves.y.times.indexOf( time );
			if ( curves.z ) zIndex = curves.z.times.indexOf( time );
2880

2881 2882
			// if there is an x value defined for this frame, use that
			if ( xIndex !== - 1 ) {
2883

2884 2885 2886
				const xValue = curves.x.values[ xIndex ];
				values.push( xValue );
				prevValue[ 0 ] = xValue;
2887

2888
			} else {
2889

2890 2891
				// otherwise use the x value from the previous frame
				values.push( prevValue[ 0 ] );
2892

2893
			}
2894

2895
			if ( yIndex !== - 1 ) {
2896

2897 2898 2899
				const yValue = curves.y.values[ yIndex ];
				values.push( yValue );
				prevValue[ 1 ] = yValue;
2900

2901
			} else {
2902

2903
				values.push( prevValue[ 1 ] );
2904

2905
			}
2906

2907
			if ( zIndex !== - 1 ) {
2908

2909 2910 2911
				const zValue = curves.z.values[ zIndex ];
				values.push( zValue );
				prevValue[ 2 ] = zValue;
2912

2913
			} else {
2914

2915
				values.push( prevValue[ 2 ] );
2916 2917 2918

			}

2919
		} );
2920

2921
		return values;
2922

2923
	}
2924

2925 2926 2927 2928
	// Rotations are defined as Euler angles which can have values  of any size
	// These will be converted to quaternions which don't support values greater than
	// PI, so we'll interpolate large rotations
	interpolateRotations( curve ) {
2929

2930
		for ( let i = 1; i < curve.values.length; i ++ ) {
2931

2932 2933
			const initialValue = curve.values[ i - 1 ];
			const valuesSpan = curve.values[ i ] - initialValue;
2934

2935
			const absoluteSpan = Math.abs( valuesSpan );
2936

2937
			if ( absoluteSpan >= 180 ) {
2938

2939
				const numSubIntervals = absoluteSpan / 180;
2940

2941 2942
				const step = valuesSpan / numSubIntervals;
				let nextValue = initialValue + step;
2943

2944 2945 2946 2947
				const initialTime = curve.times[ i - 1 ];
				const timeSpan = curve.times[ i ] - initialTime;
				const interval = timeSpan / numSubIntervals;
				let nextTime = initialTime + interval;
2948

2949 2950
				const interpolatedTimes = [];
				const interpolatedValues = [];
2951

2952
				while ( nextTime < curve.times[ i ] ) {
2953

2954 2955
					interpolatedTimes.push( nextTime );
					nextTime += interval;
2956

2957 2958
					interpolatedValues.push( nextValue );
					nextValue += step;
2959

2960
				}
2961

2962 2963
				curve.times = inject( curve.times, i, interpolatedTimes );
				curve.values = inject( curve.values, i, interpolatedValues );
2964

2965
			}
2966

2967
		}
2968

2969
	}
2970

2971
}
2972

2973 2974
// parse an FBX file in ASCII format
class TextParser {
2975

2976
	getPrevNode() {
2977

2978
		return this.nodeStack[ this.currentIndent - 2 ];
2979

2980
	}
2981

2982
	getCurrentNode() {
2983

2984
		return this.nodeStack[ this.currentIndent - 1 ];
2985

2986
	}
2987

2988
	getCurrentProp() {
2989

2990
		return this.currentProp;
2991

2992
	}
2993

2994
	pushStack( node ) {
2995

2996 2997
		this.nodeStack.push( node );
		this.currentIndent += 1;
2998

2999
	}
3000

3001
	popStack() {
3002

3003 3004
		this.nodeStack.pop();
		this.currentIndent -= 1;
3005

3006
	}
3007

3008
	setCurrentProp( val, name ) {
3009

3010 3011
		this.currentProp = val;
		this.currentPropName = name;
3012

3013
	}
3014

3015
	parse( text ) {
3016

3017
		this.currentIndent = 0;
3018

3019 3020 3021 3022
		this.allNodes = new FBXTree();
		this.nodeStack = [];
		this.currentProp = [];
		this.currentPropName = '';
3023

3024
		const scope = this;
3025

3026
		const split = text.split( /[\r\n]+/ );
3027

3028
		split.forEach( function ( line, i ) {
3029

3030 3031
			const matchComment = line.match( /^[\s\t]*;/ );
			const matchEmpty = line.match( /^[\s\t]*$/ );
3032

3033
			if ( matchComment || matchEmpty ) return;
3034

3035 3036 3037
			const matchBeginning = line.match( '^\\t{' + scope.currentIndent + '}(\\w+):(.*){', '' );
			const matchProperty = line.match( '^\\t{' + ( scope.currentIndent ) + '}(\\w+):[\\s\\t\\r\\n](.*)' );
			const matchEnd = line.match( '^\\t{' + ( scope.currentIndent - 1 ) + '}}' );
3038

3039
			if ( matchBeginning ) {
3040

3041
				scope.parseNodeBegin( line, matchBeginning );
3042

3043
			} else if ( matchProperty ) {
3044

3045
				scope.parseNodeProperty( line, matchProperty, split[ ++ i ] );
3046

3047
			} else if ( matchEnd ) {
3048

3049
				scope.popStack();
3050

3051
			} else if ( line.match( /^[^\s\t}]/ ) ) {
3052

3053 3054 3055
				// large arrays are split over multiple lines terminated with a ',' character
				// if this is encountered the line needs to be joined to the previous line
				scope.parseNodePropertyContinued( line );
3056

3057
			}
3058

3059
		} );
3060

3061
		return this.allNodes;
3062

3063
	}
3064

3065
	parseNodeBegin( line, property ) {
3066

3067
		const nodeName = property[ 1 ].trim().replace( /^"/, '' ).replace( /"$/, '' );
3068

3069
		const nodeAttrs = property[ 2 ].split( ',' ).map( function ( attr ) {
3070

3071
			return attr.trim().replace( /^"/, '' ).replace( /"$/, '' );
3072

3073
		} );
3074

3075 3076
		const node = { name: nodeName };
		const attrs = this.parseNodeAttr( nodeAttrs );
3077

3078
		const currentNode = this.getCurrentNode();
3079

3080 3081
		// a top node
		if ( this.currentIndent === 0 ) {
3082

3083
			this.allNodes.add( nodeName, node );
3084

3085
		} else { // a subnode
3086

3087 3088
			// if the subnode already exists, append it
			if ( nodeName in currentNode ) {
3089

3090 3091
				// special case Pose needs PoseNodes as an array
				if ( nodeName === 'PoseNode' ) {
3092

3093
					currentNode.PoseNode.push( node );
3094

3095
				} else if ( currentNode[ nodeName ].id !== undefined ) {
3096

3097 3098
					currentNode[ nodeName ] = {};
					currentNode[ nodeName ][ currentNode[ nodeName ].id ] = currentNode[ nodeName ];
3099 3100 3101

				}

3102
				if ( attrs.id !== '' ) currentNode[ nodeName ][ attrs.id ] = node;
3103

3104
			} else if ( typeof attrs.id === 'number' ) {
3105

3106 3107
				currentNode[ nodeName ] = {};
				currentNode[ nodeName ][ attrs.id ] = node;
3108

3109 3110 3111 3112
			} else if ( nodeName !== 'Properties70' ) {

				if ( nodeName === 'PoseNode' )	currentNode[ nodeName ] = [ node ];
				else currentNode[ nodeName ] = node;
3113 3114 3115

			}

3116
		}
3117

3118 3119 3120
		if ( typeof attrs.id === 'number' ) node.id = attrs.id;
		if ( attrs.name !== '' ) node.attrName = attrs.name;
		if ( attrs.type !== '' ) node.attrType = attrs.type;
3121

3122
		this.pushStack( node );
3123

3124
	}
3125

3126
	parseNodeAttr( attrs ) {
3127

3128
		let id = attrs[ 0 ];
3129

3130
		if ( attrs[ 0 ] !== '' ) {
3131

3132
			id = parseInt( attrs[ 0 ] );
3133

3134
			if ( isNaN( id ) ) {
3135

3136
				id = attrs[ 0 ];
3137 3138 3139

			}

3140
		}
3141

3142
		let name = '', type = '';
3143

3144
		if ( attrs.length > 1 ) {
3145

3146 3147
			name = attrs[ 1 ].replace( /^(\w+)::/, '' );
			type = attrs[ 2 ];
3148

3149
		}
3150

3151
		return { id: id, name: name, type: type };
3152

3153
	}
3154

3155
	parseNodeProperty( line, property, contentLine ) {
3156

3157 3158
		let propName = property[ 1 ].replace( /^"/, '' ).replace( /"$/, '' ).trim();
		let propValue = property[ 2 ].replace( /^"/, '' ).replace( /"$/, '' ).trim();
3159

3160 3161 3162 3163
		// for special case: base64 image data follows "Content: ," line
		//	Content: ,
		//	 "/9j/4RDaRXhpZgAATU0A..."
		if ( propName === 'Content' && propValue === ',' ) {
3164

3165
			propValue = contentLine.replace( /"/g, '' ).replace( /,$/, '' ).trim();
3166

3167
		}
3168

3169 3170
		const currentNode = this.getCurrentNode();
		const parentName = currentNode.name;
3171

3172
		if ( parentName === 'Properties70' ) {
3173

3174 3175
			this.parseNodeSpecialProperty( line, propName, propValue );
			return;
3176

3177
		}
3178

3179 3180
		// Connections
		if ( propName === 'C' ) {
3181

3182 3183 3184
			const connProps = propValue.split( ',' ).slice( 1 );
			const from = parseInt( connProps[ 0 ] );
			const to = parseInt( connProps[ 1 ] );
3185

3186
			let rest = propValue.split( ',' ).slice( 3 );
3187

3188
			rest = rest.map( function ( elem ) {
3189

3190
				return elem.trim().replace( /^"/, '' );
3191

3192
			} );
3193

3194 3195 3196
			propName = 'connections';
			propValue = [ from, to ];
			append( propValue, rest );
3197

3198
			if ( currentNode[ propName ] === undefined ) {
3199

3200
				currentNode[ propName ] = [];
3201

3202
			}
3203

3204
		}
3205

3206 3207
		// Node
		if ( propName === 'Node' ) currentNode.id = propValue;
3208

3209 3210
		// connections
		if ( propName in currentNode && Array.isArray( currentNode[ propName ] ) ) {
3211

3212
			currentNode[ propName ].push( propValue );
3213

3214
		} else {
3215

3216 3217
			if ( propName !== 'a' ) currentNode[ propName ] = propValue;
			else currentNode.a = propValue;
3218

3219
		}
3220

3221
		this.setCurrentProp( currentNode, propName );
3222

3223 3224
		// convert string to array, unless it ends in ',' in which case more will be added to it
		if ( propName === 'a' && propValue.slice( - 1 ) !== ',' ) {
3225

3226
			currentNode.a = parseNumberArray( propValue );
3227

3228
		}
3229

3230
	}
3231

3232
	parseNodePropertyContinued( line ) {
3233

3234
		const currentNode = this.getCurrentNode();
3235

3236
		currentNode.a += line;
3237

3238 3239 3240
		// if the line doesn't end in ',' we have reached the end of the property value
		// so convert the string to an array
		if ( line.slice( - 1 ) !== ',' ) {
3241

3242
			currentNode.a = parseNumberArray( currentNode.a );
3243

3244
		}
3245

3246
	}
3247

3248 3249
	// parse "Property70"
	parseNodeSpecialProperty( line, propName, propValue ) {
3250

3251 3252 3253 3254 3255
		// split this
		// P: "Lcl Scaling", "Lcl Scaling", "", "A",1,1,1
		// into array like below
		// ["Lcl Scaling", "Lcl Scaling", "", "A", "1,1,1" ]
		const props = propValue.split( '",' ).map( function ( prop ) {
3256

3257
			return prop.trim().replace( /^\"/, '' ).replace( /\s/, '_' );
3258

3259
		} );
3260

3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278
		const innerPropName = props[ 0 ];
		const innerPropType1 = props[ 1 ];
		const innerPropType2 = props[ 2 ];
		const innerPropFlag = props[ 3 ];
		let innerPropValue = props[ 4 ];

		// cast values where needed, otherwise leave as strings
		switch ( innerPropType1 ) {

			case 'int':
			case 'enum':
			case 'bool':
			case 'ULongLong':
			case 'double':
			case 'Number':
			case 'FieldOfView':
				innerPropValue = parseFloat( innerPropValue );
				break;
3279

3280 3281 3282 3283 3284 3285 3286 3287
			case 'Color':
			case 'ColorRGB':
			case 'Vector3D':
			case 'Lcl_Translation':
			case 'Lcl_Rotation':
			case 'Lcl_Scaling':
				innerPropValue = parseNumberArray( innerPropValue );
				break;
3288

3289
		}
3290

3291 3292
		// CAUTION: these props must append to parent's parent
		this.getPrevNode()[ innerPropName ] = {
3293

3294 3295 3296 3297
			'type': innerPropType1,
			'type2': innerPropType2,
			'flag': innerPropFlag,
			'value': innerPropValue
3298

3299
		};
3300

3301
		this.setCurrentProp( this.getPrevNode(), innerPropName );
3302

3303
	}
3304

3305
}
3306

3307 3308
// Parse an FBX file in Binary format
class BinaryParser {
3309

3310
	parse( buffer ) {
3311

3312 3313
		const reader = new BinaryReader( buffer );
		reader.skip( 23 ); // skip magic 23 bytes
3314

3315
		const version = reader.getUint32();
3316

3317
		if ( version < 6400 ) {
3318

3319
			throw new Error( 'THREE.FBXLoader: FBX version not supported, FileVersion: ' + version );
3320

3321
		}
3322

3323
		const allNodes = new FBXTree();
3324

3325
		while ( ! this.endOfContent( reader ) ) {
3326

3327 3328
			const node = this.parseNode( reader, version );
			if ( node !== null ) allNodes.add( node.name, node );
3329

3330
		}
3331

3332
		return allNodes;
3333

3334
	}
3335

3336 3337
	// Check if reader has reached the end of content.
	endOfContent( reader ) {
3338

3339 3340 3341 3342 3343 3344 3345 3346 3347
		// footer size: 160bytes + 16-byte alignment padding
		// - 16bytes: magic
		// - padding til 16-byte alignment (at least 1byte?)
		//	(seems like some exporters embed fixed 15 or 16bytes?)
		// - 4bytes: magic
		// - 4bytes: version
		// - 120bytes: zero
		// - 16bytes: magic
		if ( reader.size() % 16 === 0 ) {
3348

3349
			return ( ( reader.getOffset() + 160 + 16 ) & ~ 0xf ) >= reader.size();
3350

3351
		} else {
3352

3353
			return reader.getOffset() + 160 + 16 >= reader.size();
3354

3355
		}
3356

3357
	}
3358

3359 3360
	// recursively parse nodes until the end of the file is reached
	parseNode( reader, version ) {
3361

3362
		const node = {};
3363

3364 3365 3366
		// The first three data sizes depends on version.
		const endOffset = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32();
		const numProperties = ( version >= 7500 ) ? reader.getUint64() : reader.getUint32();
3367

3368
		( version >= 7500 ) ? reader.getUint64() : reader.getUint32(); // the returned propertyListLen is not used
3369

3370 3371
		const nameLen = reader.getUint8();
		const name = reader.getString( nameLen );
3372

3373 3374
		// Regards this node as NULL-record if endOffset is zero
		if ( endOffset === 0 ) return null;
3375

3376
		const propertyList = [];
3377

3378
		for ( let i = 0; i < numProperties; i ++ ) {
3379

3380
			propertyList.push( this.parseProperty( reader ) );
3381

3382
		}
3383

3384 3385 3386 3387
		// Regards the first three elements in propertyList as id, attrName, and attrType
		const id = propertyList.length > 0 ? propertyList[ 0 ] : '';
		const attrName = propertyList.length > 1 ? propertyList[ 1 ] : '';
		const attrType = propertyList.length > 2 ? propertyList[ 2 ] : '';
3388

3389 3390 3391
		// check if this node represents just a single property
		// like (name, 0) set or (name2, [0, 1, 2]) set of {name: 0, name2: [0, 1, 2]}
		node.singleProperty = ( numProperties === 1 && reader.getOffset() === endOffset ) ? true : false;
3392

3393
		while ( endOffset > reader.getOffset() ) {
3394

3395
			const subNode = this.parseNode( reader, version );
3396

3397
			if ( subNode !== null ) this.parseSubNode( name, node, subNode );
3398

3399
		}
3400

3401
		node.propertyList = propertyList; // raw property list used by parent
3402

3403 3404 3405 3406
		if ( typeof id === 'number' ) node.id = id;
		if ( attrName !== '' ) node.attrName = attrName;
		if ( attrType !== '' ) node.attrType = attrType;
		if ( name !== '' ) node.name = name;
3407

3408
		return node;
3409

3410
	}
3411

3412
	parseSubNode( name, node, subNode ) {
3413

3414 3415
		// special case: child node is single property
		if ( subNode.singleProperty === true ) {
3416

3417
			const value = subNode.propertyList[ 0 ];
3418

3419
			if ( Array.isArray( value ) ) {
3420

3421
				node[ subNode.name ] = subNode;
3422

3423
				subNode.a = value;
3424

3425
			} else {
3426

3427
				node[ subNode.name ] = value;
3428

3429
			}
3430

3431
		} else if ( name === 'Connections' && subNode.name === 'C' ) {
3432

3433
			const array = [];
3434

3435
			subNode.propertyList.forEach( function ( property, i ) {
3436

3437 3438
				// first Connection is FBX type (OO, OP, etc.). We'll discard these
				if ( i !== 0 ) array.push( property );
3439

3440
			} );
3441

3442
			if ( node.connections === undefined ) {
3443

3444
				node.connections = [];
3445

3446
			}
3447

3448
			node.connections.push( array );
3449

3450
		} else if ( subNode.name === 'Properties70' ) {
3451

3452
			const keys = Object.keys( subNode );
3453

3454
			keys.forEach( function ( key ) {
3455

3456
				node[ key ] = subNode[ key ];
3457

3458
			} );
3459

3460
		} else if ( name === 'Properties70' && subNode.name === 'P' ) {
3461

3462 3463 3464 3465 3466
			let innerPropName = subNode.propertyList[ 0 ];
			let innerPropType1 = subNode.propertyList[ 1 ];
			const innerPropType2 = subNode.propertyList[ 2 ];
			const innerPropFlag = subNode.propertyList[ 3 ];
			let innerPropValue;
3467

3468 3469 3470 3471 3472 3473 3474 3475 3476 3477
			if ( innerPropName.indexOf( 'Lcl ' ) === 0 ) innerPropName = innerPropName.replace( 'Lcl ', 'Lcl_' );
			if ( innerPropType1.indexOf( 'Lcl ' ) === 0 ) innerPropType1 = innerPropType1.replace( 'Lcl ', 'Lcl_' );

			if ( innerPropType1 === 'Color' || innerPropType1 === 'ColorRGB' || innerPropType1 === 'Vector' || innerPropType1 === 'Vector3D' || innerPropType1.indexOf( 'Lcl_' ) === 0 ) {

				innerPropValue = [
					subNode.propertyList[ 4 ],
					subNode.propertyList[ 5 ],
					subNode.propertyList[ 6 ]
				];
3478 3479 3480

			} else {

3481
				innerPropValue = subNode.propertyList[ 4 ];
3482

3483
			}
3484

3485 3486
			// this will be copied to parent, see above
			node[ innerPropName ] = {
3487

3488 3489 3490 3491
				'type': innerPropType1,
				'type2': innerPropType2,
				'flag': innerPropFlag,
				'value': innerPropValue
3492

3493
			};
3494

3495
		} else if ( node[ subNode.name ] === undefined ) {
3496

3497
			if ( typeof subNode.id === 'number' ) {
3498

3499 3500
				node[ subNode.name ] = {};
				node[ subNode.name ][ subNode.id ] = subNode;
3501

3502
			} else {
3503

3504
				node[ subNode.name ] = subNode;
3505

3506
			}
3507

3508
		} else {
3509

3510
			if ( subNode.name === 'PoseNode' ) {
3511

3512
				if ( ! Array.isArray( node[ subNode.name ] ) ) {
3513

3514
					node[ subNode.name ] = [ node[ subNode.name ] ];
3515

3516
				}
3517

3518
				node[ subNode.name ].push( subNode );
3519

3520
			} else if ( node[ subNode.name ][ subNode.id ] === undefined ) {
3521

3522
				node[ subNode.name ][ subNode.id ] = subNode;
3523

3524
			}
3525

3526
		}
3527

3528
	}
3529

3530
	parseProperty( reader ) {
3531

3532 3533
		const type = reader.getString( 1 );
		let length;
3534

3535
		switch ( type ) {
3536

3537 3538
			case 'C':
				return reader.getBoolean();
3539

3540 3541
			case 'D':
				return reader.getFloat64();
3542

3543 3544
			case 'F':
				return reader.getFloat32();
3545

3546 3547
			case 'I':
				return reader.getInt32();
3548

3549 3550
			case 'L':
				return reader.getInt64();
3551

3552 3553 3554
			case 'R':
				length = reader.getUint32();
				return reader.getArrayBuffer( length );
3555

3556 3557 3558
			case 'S':
				length = reader.getUint32();
				return reader.getString( length );
3559

3560 3561
			case 'Y':
				return reader.getInt16();
3562

3563 3564 3565 3566 3567 3568
			case 'b':
			case 'c':
			case 'd':
			case 'f':
			case 'i':
			case 'l':
3569

3570 3571 3572
				const arrayLength = reader.getUint32();
				const encoding = reader.getUint32(); // 0: non-compressed, 1: compressed
				const compressedLength = reader.getUint32();
3573

3574
				if ( encoding === 0 ) {
3575 3576 3577 3578 3579

					switch ( type ) {

						case 'b':
						case 'c':
3580
							return reader.getBooleanArray( arrayLength );
3581 3582

						case 'd':
3583
							return reader.getFloat64Array( arrayLength );
3584 3585

						case 'f':
3586
							return reader.getFloat32Array( arrayLength );
3587 3588

						case 'i':
3589
							return reader.getInt32Array( arrayLength );
3590 3591

						case 'l':
3592
							return reader.getInt64Array( arrayLength );
3593 3594 3595

					}

3596
				}
3597

3598
				if ( typeof fflate === 'undefined' ) {
3599

3600
					console.error( 'THREE.FBXLoader: External library fflate.min.js required.' );
3601

3602
				}
3603

3604 3605
				const data = fflate.unzlibSync( new Uint8Array( reader.getArrayBuffer( compressedLength ) ) ); // eslint-disable-line no-undef
				const reader2 = new BinaryReader( data.buffer );
3606

3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630
				switch ( type ) {

					case 'b':
					case 'c':
						return reader2.getBooleanArray( arrayLength );

					case 'd':
						return reader2.getFloat64Array( arrayLength );

					case 'f':
						return reader2.getFloat32Array( arrayLength );

					case 'i':
						return reader2.getInt32Array( arrayLength );

					case 'l':
						return reader2.getInt64Array( arrayLength );

				}

			default:
				throw new Error( 'THREE.FBXLoader: Unknown property type ' + type );

		}
3631 3632 3633

	}

3634
}
3635

3636
class BinaryReader {
3637

3638
	constructor( buffer, littleEndian ) {
3639

3640 3641 3642
		this.dv = new DataView( buffer );
		this.offset = 0;
		this.littleEndian = ( littleEndian !== undefined ) ? littleEndian : true;
3643

3644
	}
3645

3646
	getOffset() {
3647

3648
		return this.offset;
3649

3650
	}
3651

3652
	size() {
3653

3654
		return this.dv.buffer.byteLength;
3655

3656
	}
3657

3658
	skip( length ) {
3659

3660
		this.offset += length;
3661

3662
	}
3663

3664 3665 3666 3667
	// seems like true/false representation depends on exporter.
	// true: 1 or 'Y'(=0x59), false: 0 or 'T'(=0x54)
	// then sees LSB.
	getBoolean() {
3668

3669
		return ( this.getUint8() & 1 ) === 1;
3670

3671
	}
3672

3673
	getBooleanArray( size ) {
3674

3675
		const a = [];
3676

3677
		for ( let i = 0; i < size; i ++ ) {
3678

3679
			a.push( this.getBoolean() );
3680

3681
		}
3682

3683
		return a;
3684

3685
	}
3686

3687
	getUint8() {
3688

3689 3690 3691
		const value = this.dv.getUint8( this.offset );
		this.offset += 1;
		return value;
3692

3693
	}
3694

3695
	getInt16() {
3696

3697 3698 3699
		const value = this.dv.getInt16( this.offset, this.littleEndian );
		this.offset += 2;
		return value;
3700

3701
	}
3702

3703
	getInt32() {
3704

3705 3706 3707
		const value = this.dv.getInt32( this.offset, this.littleEndian );
		this.offset += 4;
		return value;
3708

3709
	}
3710

3711
	getInt32Array( size ) {
3712

3713
		const a = [];
3714

3715
		for ( let i = 0; i < size; i ++ ) {
3716

3717
			a.push( this.getInt32() );
3718

3719
		}
3720

3721
		return a;
3722

3723
	}
3724

3725
	getUint32() {
3726

3727 3728 3729
		const value = this.dv.getUint32( this.offset, this.littleEndian );
		this.offset += 4;
		return value;
3730

3731
	}
3732

3733 3734 3735 3736 3737 3738
	// JavaScript doesn't support 64-bit integer so calculate this here
	// 1 << 32 will return 1 so using multiply operation instead here.
	// There's a possibility that this method returns wrong value if the value
	// is out of the range between Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_INTEGER.
	// TODO: safely handle 64-bit integer
	getInt64() {
3739

3740
		let low, high;
3741

3742
		if ( this.littleEndian ) {
3743

3744 3745
			low = this.getUint32();
			high = this.getUint32();
3746

3747
		} else {
3748

3749 3750
			high = this.getUint32();
			low = this.getUint32();
3751

3752
		}
3753

3754 3755
		// calculate negative value
		if ( high & 0x80000000 ) {
3756

3757 3758
			high = ~ high & 0xFFFFFFFF;
			low = ~ low & 0xFFFFFFFF;
3759

3760
			if ( low === 0xFFFFFFFF ) high = ( high + 1 ) & 0xFFFFFFFF;
3761

3762
			low = ( low + 1 ) & 0xFFFFFFFF;
3763

3764
			return - ( high * 0x100000000 + low );
3765

3766
		}
3767

3768
		return high * 0x100000000 + low;
3769

3770
	}
3771

3772
	getInt64Array( size ) {
3773

3774
		const a = [];
3775

3776
		for ( let i = 0; i < size; i ++ ) {
3777

3778
			a.push( this.getInt64() );
3779

3780
		}
3781

3782
		return a;
3783

3784
	}
3785

3786 3787
	// Note: see getInt64() comment
	getUint64() {
3788

3789
		let low, high;
3790

3791
		if ( this.littleEndian ) {
3792

3793 3794
			low = this.getUint32();
			high = this.getUint32();
3795

3796
		} else {
3797

3798 3799
			high = this.getUint32();
			low = this.getUint32();
3800

3801
		}
3802

3803
		return high * 0x100000000 + low;
3804

3805
	}
3806

3807
	getFloat32() {
3808

3809 3810 3811
		const value = this.dv.getFloat32( this.offset, this.littleEndian );
		this.offset += 4;
		return value;
3812

3813
	}
3814

3815
	getFloat32Array( size ) {
3816

3817
		const a = [];
3818

3819
		for ( let i = 0; i < size; i ++ ) {
3820

3821
			a.push( this.getFloat32() );
3822

3823
		}
3824

3825
		return a;
3826

3827
	}
3828

3829
	getFloat64() {
3830

3831 3832 3833
		const value = this.dv.getFloat64( this.offset, this.littleEndian );
		this.offset += 8;
		return value;
3834

3835
	}
3836

3837
	getFloat64Array( size ) {
3838

3839
		const a = [];
3840

3841
		for ( let i = 0; i < size; i ++ ) {
3842

3843
			a.push( this.getFloat64() );
3844

3845
		}
3846

3847
		return a;
3848

3849
	}
3850

3851
	getArrayBuffer( size ) {
3852

3853 3854 3855
		const value = this.dv.buffer.slice( this.offset, this.offset + size );
		this.offset += size;
		return value;
3856

3857
	}
3858

3859
	getString( size ) {
3860

3861 3862
		// note: safari 9 doesn't support Uint8Array.indexOf; create intermediate array instead
		let a = [];
3863

3864
		for ( let i = 0; i < size; i ++ ) {
3865

3866
			a[ i ] = this.getUint8();
3867 3868 3869

		}

3870 3871
		const nullByte = a.indexOf( 0 );
		if ( nullByte >= 0 ) a = a.slice( 0, nullByte );
3872

3873
		return LoaderUtils.decodeText( new Uint8Array( a ) );
3874

3875
	}
3876

3877
}
3878

3879 3880 3881
// FBXTree holds a representation of the FBX data, returned by the TextParser ( FBX ASCII format)
// and BinaryParser( FBX Binary format)
class FBXTree {
3882

3883
	add( key, val ) {
3884

3885
		this[ key ] = val;
3886

3887
	}
3888

3889
}
3890

3891
// ************** UTILITY FUNCTIONS **************
3892

3893
function isFbxFormatBinary( buffer ) {
3894

M
Michael Herzog 已提交
3895
	const CORRECT = 'Kaydara\u0020FBX\u0020Binary\u0020\u0020\0';
3896

3897
	return buffer.byteLength >= CORRECT.length && CORRECT === convertArrayBufferToString( buffer, 0, CORRECT.length );
3898

3899
}
3900

3901
function isFbxFormatASCII( text ) {
3902

3903
	const CORRECT = [ 'K', 'a', 'y', 'd', 'a', 'r', 'a', '\\', 'F', 'B', 'X', '\\', 'B', 'i', 'n', 'a', 'r', 'y', '\\', '\\' ];
3904

3905
	let cursor = 0;
3906

3907
	function read( offset ) {
3908

3909 3910 3911 3912
		const result = text[ offset - 1 ];
		text = text.slice( cursor + offset );
		cursor ++;
		return result;
3913

3914
	}
3915

3916
	for ( let i = 0; i < CORRECT.length; ++ i ) {
3917

3918 3919
		const num = read( 1 );
		if ( num === CORRECT[ i ] ) {
3920

3921
			return false;
3922 3923 3924 3925 3926

		}

	}

3927
	return true;
3928

3929
}
M
Mugen87 已提交
3930

3931
function getFbxVersion( text ) {
3932

3933 3934
	const versionRegExp = /FBXVersion: (\d+)/;
	const match = text.match( versionRegExp );
3935

3936
	if ( match ) {
M
Mugen87 已提交
3937

3938 3939
		const version = parseInt( match[ 1 ] );
		return version;
3940 3941 3942

	}

3943
	throw new Error( 'THREE.FBXLoader: Cannot find the version number for the file given.' );
3944

3945
}
3946

3947 3948
// Converts FBX ticks into real time seconds.
function convertFBXTimeToSeconds( time ) {
3949

3950
	return time / 46186158000;
3951

3952
}
3953

3954
const dataArray = [];
3955

3956 3957
// extracts the data from the correct position in the FBX array based on indexing type
function getData( polygonVertexIndex, polygonIndex, vertexIndex, infoObject ) {
3958

3959
	let index;
3960

3961
	switch ( infoObject.mappingType ) {
3962

3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976
		case 'ByPolygonVertex' :
			index = polygonVertexIndex;
			break;
		case 'ByPolygon' :
			index = polygonIndex;
			break;
		case 'ByVertice' :
			index = vertexIndex;
			break;
		case 'AllSame' :
			index = infoObject.indices[ 0 ];
			break;
		default :
			console.warn( 'THREE.FBXLoader: unknown attribute mapping type ' + infoObject.mappingType );
3977

3978
	}
3979

3980
	if ( infoObject.referenceType === 'IndexToDirect' ) index = infoObject.indices[ index ];
3981

3982 3983
	const from = index * infoObject.dataSize;
	const to = from + infoObject.dataSize;
3984

3985
	return slice( dataArray, infoObject.buffer, from, to );
3986

3987
}
3988

3989 3990
const tempEuler = new Euler();
const tempVec = new Vector3();
3991

3992 3993 3994 3995
// generate transformation from FBX transform data
// ref: https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_10CDD63C_79C1_4F2D_BB28_AD2BE65A02ED_htm
// 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
function generateTransform( transformData ) {
3996

3997 3998 3999 4000
	const lTranslationM = new Matrix4();
	const lPreRotationM = new Matrix4();
	const lRotationM = new Matrix4();
	const lPostRotationM = new Matrix4();
4001

4002 4003 4004 4005 4006
	const lScalingM = new Matrix4();
	const lScalingPivotM = new Matrix4();
	const lScalingOffsetM = new Matrix4();
	const lRotationOffsetM = new Matrix4();
	const lRotationPivotM = new Matrix4();
4007

4008 4009 4010
	const lParentGX = new Matrix4();
	const lParentLX = new Matrix4();
	const lGlobalT = new Matrix4();
4011

4012
	const inheritType = ( transformData.inheritType ) ? transformData.inheritType : 0;
4013

4014
	if ( transformData.translation ) lTranslationM.setPosition( tempVec.fromArray( transformData.translation ) );
4015

4016
	if ( transformData.preRotation ) {
4017

4018 4019 4020
		const array = transformData.preRotation.map( MathUtils.degToRad );
		array.push( transformData.eulerOrder );
		lPreRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) );
4021

4022
	}
4023

4024
	if ( transformData.rotation ) {
4025

4026 4027 4028
		const array = transformData.rotation.map( MathUtils.degToRad );
		array.push( transformData.eulerOrder );
		lRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) );
4029

4030
	}
4031

4032
	if ( transformData.postRotation ) {
4033

4034 4035 4036 4037
		const array = transformData.postRotation.map( MathUtils.degToRad );
		array.push( transformData.eulerOrder );
		lPostRotationM.makeRotationFromEuler( tempEuler.fromArray( array ) );
		lPostRotationM.invert();
4038

4039
	}
4040

4041
	if ( transformData.scale ) lScalingM.scale( tempVec.fromArray( transformData.scale ) );
M
Mr.doob 已提交
4042

4043 4044 4045 4046 4047
	// Pivots and offsets
	if ( transformData.scalingOffset ) lScalingOffsetM.setPosition( tempVec.fromArray( transformData.scalingOffset ) );
	if ( transformData.scalingPivot ) lScalingPivotM.setPosition( tempVec.fromArray( transformData.scalingPivot ) );
	if ( transformData.rotationOffset ) lRotationOffsetM.setPosition( tempVec.fromArray( transformData.rotationOffset ) );
	if ( transformData.rotationPivot ) lRotationPivotM.setPosition( tempVec.fromArray( transformData.rotationPivot ) );
4048

4049 4050
	// parent transform
	if ( transformData.parentMatrixWorld ) {
M
Mr.doob 已提交
4051

4052 4053
		lParentLX.copy( transformData.parentMatrix );
		lParentGX.copy( transformData.parentMatrixWorld );
4054

4055
	}
4056

4057 4058 4059 4060
	const lLRM = new Matrix4().copy( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM );
	// Global Rotation
	const lParentGRM = new Matrix4();
	lParentGRM.extractRotation( lParentGX );
4061

4062 4063 4064
	// Global Shear*Scaling
	const lParentTM = new Matrix4();
	lParentTM.copyPosition( lParentGX );
M
Mr.doob 已提交
4065

4066 4067 4068 4069
	const lParentGSM = new Matrix4();
	const lParentGRSM = new Matrix4().copy( lParentTM ).invert().multiply( lParentGX );
	lParentGSM.copy( lParentGRM ).invert().multiply( lParentGRSM );
	const lLSM = lScalingM;
4070

4071
	const lGlobalRS = new Matrix4();
4072

4073
	if ( inheritType === 0 ) {
4074

4075
		lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM ).multiply( lLSM );
4076

4077
	} else if ( inheritType === 1 ) {
4078

4079
		lGlobalRS.copy( lParentGRM ).multiply( lParentGSM ).multiply( lLRM ).multiply( lLSM );
4080

4081
	} else {
4082

4083 4084 4085
		const lParentLSM = new Matrix4().scale( new Vector3().setFromMatrixScale( lParentLX ) );
		const lParentLSM_inv = new Matrix4().copy( lParentLSM ).invert();
		const lParentGSM_noLocal = new Matrix4().copy( lParentGSM ).multiply( lParentLSM_inv );
4086

4087
		lGlobalRS.copy( lParentGRM ).multiply( lLRM ).multiply( lParentGSM_noLocal ).multiply( lLSM );
4088

4089
	}
4090

4091 4092 4093 4094 4095 4096 4097
	const lRotationPivotM_inv = new Matrix4();
	lRotationPivotM_inv.copy( lRotationPivotM ).invert();
	const lScalingPivotM_inv = new Matrix4();
	lScalingPivotM_inv.copy( lScalingPivotM ).invert();
	// Calculate the local transform matrix
	let lTransform = new Matrix4();
	lTransform.copy( lTranslationM ).multiply( lRotationOffsetM ).multiply( lRotationPivotM ).multiply( lPreRotationM ).multiply( lRotationM ).multiply( lPostRotationM ).multiply( lRotationPivotM_inv ).multiply( lScalingOffsetM ).multiply( lScalingPivotM ).multiply( lScalingM ).multiply( lScalingPivotM_inv );
4098

4099
	const lLocalTWithAllPivotAndOffsetInfo = new Matrix4().copyPosition( lTransform );
M
Mr.doob 已提交
4100

4101 4102
	const lGlobalTranslation = new Matrix4().copy( lParentGX ).multiply( lLocalTWithAllPivotAndOffsetInfo );
	lGlobalT.copyPosition( lGlobalTranslation );
4103

4104
	lTransform = new Matrix4().copy( lGlobalT ).multiply( lGlobalRS );
4105

4106 4107
	// from global to local
	lTransform.premultiply( lParentGX.invert() );
4108

4109
	return lTransform;
4110

4111
}
4112

4113 4114 4115
// Returns the three.js intrinsic Euler order corresponding to FBX extrinsic Euler order
// ref: http://help.autodesk.com/view/FBX/2017/ENU/?guid=__cpp_ref_class_fbx_euler_html
function getEulerOrder( order ) {
4116

4117
	order = order || 0;
4118

4119 4120 4121 4122 4123 4124 4125 4126 4127
	const enums = [
		'ZYX', // -> XYZ extrinsic
		'YZX', // -> XZY extrinsic
		'XZY', // -> YZX extrinsic
		'ZXY', // -> YXZ extrinsic
		'YXZ', // -> ZXY extrinsic
		'XYZ', // -> ZYX extrinsic
		//'SphericXYZ', // not possible to support
	];
4128

4129
	if ( order === 6 ) {
4130

4131 4132
		console.warn( 'THREE.FBXLoader: unsupported Euler Order: Spherical XYZ. Animations and rotations may be incorrect.' );
		return enums[ 0 ];
4133 4134 4135

	}

4136
	return enums[ order ];
4137

4138
}
4139

4140 4141 4142
// Parses comma separated list of numbers and returns them an array.
// Used internally by the TextParser
function parseNumberArray( value ) {
4143

4144
	const array = value.split( ',' ).map( function ( val ) {
4145

4146
		return parseFloat( val );
4147

4148
	} );
4149

4150
	return array;
4151

4152
}
4153

4154
function convertArrayBufferToString( buffer, from, to ) {
4155

4156 4157
	if ( from === undefined ) from = 0;
	if ( to === undefined ) to = buffer.byteLength;
4158

4159
	return LoaderUtils.decodeText( new Uint8Array( buffer, from, to ) );
4160

4161
}
4162

4163
function append( a, b ) {
4164

4165
	for ( let i = 0, j = a.length, l = b.length; i < l; i ++, j ++ ) {
4166

4167
		a[ j ] = b[ i ];
4168

4169
	}
4170

4171
}
4172

4173
function slice( a, b, from, to ) {
4174

4175
	for ( let i = from, j = 0; i < to; i ++, j ++ ) {
4176

4177
		a[ j ] = b[ i ];
4178 4179 4180

	}

4181
	return a;
4182

4183
}
4184

4185 4186
// inject array a2 into array a1 at index
function inject( a1, index, a2 ) {
4187

4188
	return a1.slice( 0, index ).concat( a2 ).concat( a1.slice( index ) );
4189

4190
}
4191

4192
export { FBXLoader };