GLTFExporter.js 41.3 KB
Newer Older
F
Fernando Serrano 已提交
1 2
/**
 * @author fernandojsg / http://fernandojsg.com
3 4
 * @author Don McCurdy / https://www.donmccurdy.com
 * @author Takahiro / https://github.com/takahirox
F
Fernando Serrano 已提交
5 6
 */

M
Mugen87 已提交
7 8 9
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
10 11
var WEBGL_CONSTANTS = {
	POINTS: 0x0000,
F
Fernando Serrano 已提交
12 13 14 15 16 17
	LINES: 0x0001,
	LINE_LOOP: 0x0002,
	LINE_STRIP: 0x0003,
	TRIANGLES: 0x0004,
	TRIANGLE_STRIP: 0x0005,
	TRIANGLE_FAN: 0x0006,
18 19 20 21 22 23 24 25 26

	UNSIGNED_BYTE: 0x1401,
	UNSIGNED_SHORT: 0x1403,
	FLOAT: 0x1406,
	UNSIGNED_INT: 0x1405,
	ARRAY_BUFFER: 0x8892,
	ELEMENT_ARRAY_BUFFER: 0x8893,

	NEAREST: 0x2600,
F
Fernando Serrano 已提交
27 28 29 30
	LINEAR: 0x2601,
	NEAREST_MIPMAP_NEAREST: 0x2700,
	LINEAR_MIPMAP_NEAREST: 0x2701,
	NEAREST_MIPMAP_LINEAR: 0x2702,
31
	LINEAR_MIPMAP_LINEAR: 0x2703,
32

33 34 35
	CLAMP_TO_EDGE: 33071,
	MIRRORED_REPEAT: 33648,
	REPEAT: 10497
M
Mugen87 已提交
36
};
37

38 39 40 41 42 43 44 45 46 47 48 49 50
var THREE_TO_WEBGL = {};

THREE_TO_WEBGL[ THREE.NearestFilter ] = WEBGL_CONSTANTS.NEAREST;
THREE_TO_WEBGL[ THREE.NearestMipMapNearestFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST;
THREE_TO_WEBGL[ THREE.NearestMipMapLinearFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR;
THREE_TO_WEBGL[ THREE.LinearFilter ] = WEBGL_CONSTANTS.LINEAR;
THREE_TO_WEBGL[ THREE.LinearMipMapNearestFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST;
THREE_TO_WEBGL[ THREE.LinearMipMapLinearFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR;

THREE_TO_WEBGL[ THREE.ClampToEdgeWrapping ] = WEBGL_CONSTANTS.CLAMP_TO_EDGE;
THREE_TO_WEBGL[ THREE.RepeatWrapping ] = WEBGL_CONSTANTS.REPEAT;
THREE_TO_WEBGL[ THREE.MirroredRepeatWrapping ] = WEBGL_CONSTANTS.MIRRORED_REPEAT;

51 52 53 54 55 56 57
var PATH_PROPERTIES = {
	scale: 'scale',
	position: 'translation',
	quaternion: 'rotation',
	morphTargetInfluences: 'weights'
};

F
Fernando Serrano 已提交
58 59 60
//------------------------------------------------------------------------------
// GLTF Exporter
//------------------------------------------------------------------------------
61
THREE.GLTFExporter = function () {};
F
Fernando Serrano 已提交
62 63 64 65

THREE.GLTFExporter.prototype = {

	constructor: THREE.GLTFExporter,
F
Fernando Serrano 已提交
66

F
Fernando Serrano 已提交
67 68 69
	/**
	 * Parse scenes and generate GLTF output
	 * @param  {THREE.Scene or [THREE.Scenes]} input   THREE.Scene or Array of THREE.Scenes
F
Fernando Serrano 已提交
70 71
	 * @param  {Function} onDone  Callback on completed
	 * @param  {Object} options options
F
Fernando Serrano 已提交
72
	 */
F
Fernando Serrano 已提交
73 74
	parse: function ( input, onDone, options ) {

75
		var DEFAULT_OPTIONS = {
76
			binary: false,
77
			trs: false,
78
			onlyVisible: true,
79
			truncateDrawRange: true,
80
			embedImages: true,
81
			animations: [],
82
			forceIndices: false,
83
			forcePowerOfTwoTextures: false
84 85 86 87
		};

		options = Object.assign( {}, DEFAULT_OPTIONS, options );

D
Don McCurdy 已提交
88 89 90 91 92 93 94
		if ( options.animations.length > 0 ) {

			// Only TRS properties, and not matrices, may be targeted by animation.
			options.trs = true;

		}

F
Fernando Serrano 已提交
95
		var outputJSON = {
F
Fernando Serrano 已提交
96

F
Fernando Serrano 已提交
97
			asset: {
F
Fernando Serrano 已提交
98

F
Fernando Serrano 已提交
99
				version: "2.0",
M
Mr.doob 已提交
100
				generator: "THREE.GLTFExporter"
F
Fernando Serrano 已提交
101

M
Mr.doob 已提交
102
			}
F
Fernando Serrano 已提交
103

M
Mr.doob 已提交
104
		};
F
Fernando Serrano 已提交
105 106

		var byteOffset = 0;
107 108
		var buffers = [];
		var pending = [];
T
Takahiro 已提交
109
		var nodeMap = new Map();
D
Don McCurdy 已提交
110
		var skins = [];
111
		var extensionsUsed = {};
112 113
		var cachedData = {

114
			meshes: new Map(),
115
			attributes: new Map(),
116
			attributesNormalized: new Map(),
117
			materials: new Map(),
118 119
			textures: new Map(),
			images: new Map()
120 121

		};
F
Fernando Serrano 已提交
122

123 124
		var cachedCanvas;

F
Fernando Serrano 已提交
125 126 127
		/**
		 * Compare two arrays
		 */
F
Fernando Serrano 已提交
128 129 130 131 132 133
		/**
		 * Compare two arrays
		 * @param  {Array} array1 Array 1 to compare
		 * @param  {Array} array2 Array 2 to compare
		 * @return {Boolean}        Returns true if both arrays are equal
		 */
M
Mr.doob 已提交
134
		function equalArray( array1, array2 ) {
F
Fernando Serrano 已提交
135

M
Mugen87 已提交
136
			return ( array1.length === array2.length ) && array1.every( function ( element, index ) {
F
Fernando Serrano 已提交
137

M
Mr.doob 已提交
138
				return element === array2[ index ];
F
Fernando Serrano 已提交
139

M
Mugen87 已提交
140
			} );
F
Fernando Serrano 已提交
141

F
Fernando Serrano 已提交
142 143
		}

144 145 146 147 148
		/**
		 * Converts a string to an ArrayBuffer.
		 * @param  {string} text
		 * @return {ArrayBuffer}
		 */
149
		function stringToArrayBuffer( text ) {
150 151 152 153 154 155 156

			if ( window.TextEncoder !== undefined ) {

				return new TextEncoder().encode( text ).buffer;

			}

157
			var array = new Uint8Array( new ArrayBuffer( text.length ) );
158

159
			for ( var i = 0, il = text.length; i < il; i ++ ) {
160

161
				var value = text.charCodeAt( i );
162

163
				// Replacing multi-byte character with space(0x20).
F
Fernando Serrano 已提交
164
				array[ i ] = value > 0xFF ? 0x20 : value;
165 166 167

			}

168
			return array.buffer;
169 170 171

		}

F
Fernando Serrano 已提交
172
		/**
173
		 * Get the min and max vectors from the given attribute
T
Takahiro 已提交
174 175 176
		 * @param  {THREE.BufferAttribute} attribute Attribute to find the min/max in range from start to start + count
		 * @param  {Integer} start
		 * @param  {Integer} count
F
Fernando Serrano 已提交
177 178
		 * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components)
		 */
T
Takahiro 已提交
179
		function getMinMax( attribute, start, count ) {
F
Fernando Serrano 已提交
180

F
Fernando Serrano 已提交
181
			var output = {
F
Fernando Serrano 已提交
182

F
Fernando Serrano 已提交
183 184
				min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ),
				max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY )
F
Fernando Serrano 已提交
185

F
Fernando Serrano 已提交
186 187
			};

T
Takahiro 已提交
188
			for ( var i = start; i < start + count; i ++ ) {
F
Fernando Serrano 已提交
189

M
Mugen87 已提交
190
				for ( var a = 0; a < attribute.itemSize; a ++ ) {
F
Fernando Serrano 已提交
191

F
Fernando Serrano 已提交
192
					var value = attribute.array[ i * attribute.itemSize + a ];
F
Fernando Serrano 已提交
193 194 195
					output.min[ a ] = Math.min( output.min[ a ], value );
					output.max[ a ] = Math.max( output.max[ a ], value );

F
Fernando Serrano 已提交
196
				}
F
Fernando Serrano 已提交
197

F
Fernando Serrano 已提交
198 199
			}

F
Fernando Serrano 已提交
200
			return output;
M
Mugen87 已提交
201

F
Fernando Serrano 已提交
202 203
		}

204 205 206 207 208 209 210 211 212 213 214 215 216
		/**
		 * Checks if image size is POT.
		 *
		 * @param {Image} image The image to be checked.
		 * @returns {Boolean} Returns true if image size is POT.
		 *
		 */
		function isPowerOfTwo( image ) {

			return THREE.Math.isPowerOfTwo( image.width ) && THREE.Math.isPowerOfTwo( image.height );

		}

217 218 219 220 221 222 223 224 225
		/**
		 * Checks if normal attribute values are normalized.
		 *
		 * @param {THREE.BufferAttribute} normal
		 * @returns {Boolean}
		 *
		 */
		function isNormalizedNormalAttribute( normal ) {

226
			if ( cachedData.attributesNormalized.has( normal ) ) {
227 228 229 230 231

				return false;

			}

T
Takahiro 已提交
232
			var v = new THREE.Vector3();
233

T
Takahiro 已提交
234
			for ( var i = 0, il = normal.count; i < il; i ++ ) {
235 236

				// 0.0005 is from glTF-validator
T
Takahiro 已提交
237
				if ( Math.abs( v.fromArray( normal.array, i * 3 ).length() - 1.0 ) > 0.0005 ) return false;
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253

			}

			return true;

		}

		/**
		 * Creates normalized normal buffer attribute.
		 *
		 * @param {THREE.BufferAttribute} normal
		 * @returns {THREE.BufferAttribute}
		 *
		 */
		function createNormalizedNormalAttribute( normal ) {

254
			if ( cachedData.attributesNormalized.has( normal ) ) {
255

256
				return cachedData.attributesNormalized.get( normal );
257 258 259 260 261 262 263

			}

			var attribute = normal.clone();

			var v = new THREE.Vector3();

T
Takahiro 已提交
264
			for ( var i = 0, il = attribute.count; i < il; i ++ ) {
265

T
Takahiro 已提交
266
				v.fromArray( attribute.array, i * 3 );
267 268 269 270 271 272 273 274 275 276 277 278

				if ( v.x === 0 && v.y === 0 && v.z === 0 ) {

					// if values can't be normalized set (1, 0, 0)
					v.setX( 1.0 );

				} else {

					v.normalize();

				}

T
Takahiro 已提交
279
				v.toArray( attribute.array, i * 3 );
280 281 282

			}

283
			cachedData.attributesNormalized.set( normal, attribute );
284 285 286 287 288

			return attribute;

		}

289 290 291 292 293 294 295 296 297 298
		/**
		 * Get the required size + padding for a buffer, rounded to the next 4-byte boundary.
		 * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment
		 *
		 * @param {Integer} bufferSize The size the original buffer.
		 * @returns {Integer} new buffer size with required padding.
		 *
		 */
		function getPaddedBufferSize( bufferSize ) {

299
			return Math.ceil( bufferSize / 4 ) * 4;
300 301

		}
M
Mugen87 已提交
302

F
Fernando Serrano 已提交
303
		/**
304 305
		 * Returns a buffer aligned to 4-byte boundary.
		 *
F
Fernando Serrano 已提交
306
		 * @param {ArrayBuffer} arrayBuffer Buffer to pad
307
		 * @param {Integer} paddingByte (Optional)
F
Fernando Serrano 已提交
308 309
		 * @returns {ArrayBuffer} The same buffer if it's already aligned to 4-byte boundary or a new buffer
		 */
310
		function getPaddedArrayBuffer( arrayBuffer, paddingByte ) {
311

312
			paddingByte = paddingByte || 0;
313

F
Fernando Serrano 已提交
314
			var paddedLength = getPaddedBufferSize( arrayBuffer.byteLength );
315

316
			if ( paddedLength !== arrayBuffer.byteLength ) {
317

318 319
				var array = new Uint8Array( paddedLength );
				array.set( new Uint8Array( arrayBuffer ) );
320

321
				if ( paddingByte !== 0 ) {
322

T
Takahiro 已提交
323
					for ( var i = arrayBuffer.byteLength; i < paddedLength; i ++ ) {
324

325
						array[ i ] = paddingByte;
326 327 328 329

					}

				}
330

331
				return array.buffer;
F
Fernando Serrano 已提交
332 333 334 335 336 337 338

			}

			return arrayBuffer;

		}

339 340 341
		/**
		 * Serializes a userData.
		 *
342
		 * @param {THREE.Object3D|THREE.Material} object
343 344
		 * @returns {Object}
		 */
345
		function serializeUserData( object ) {
346 347 348

			try {

349
				return JSON.parse( JSON.stringify( object.userData ) );
350

351
			} catch ( error ) {
352

353 354
				console.warn( 'THREE.GLTFExporter: userData of \'' + object.name + '\' ' +
					'won\'t be serialized because of JSON.stringify error - ' + error.message );
355

356 357
				return {};

358 359 360 361
			}

		}

F
Fernando Serrano 已提交
362
		/**
F
Fernando Serrano 已提交
363
		 * Process a buffer to append to the default one.
364 365
		 * @param  {ArrayBuffer} buffer
		 * @return {Integer}
F
Fernando Serrano 已提交
366
		 */
367
		function processBuffer( buffer ) {
F
Fernando Serrano 已提交
368

M
Mugen87 已提交
369
			if ( ! outputJSON.buffers ) {
F
Fernando Serrano 已提交
370

371
				outputJSON.buffers = [ { byteLength: 0 } ];
F
Fernando Serrano 已提交
372

373
			}
F
Fernando Serrano 已提交
374

375 376
			// All buffers are merged before export.
			buffers.push( buffer );
F
Fernando Serrano 已提交
377

378
			return 0;
F
Fernando Serrano 已提交
379

380
		}
F
Fernando Serrano 已提交
381

382 383 384 385 386 387 388 389 390 391
		/**
		 * Process and generate a BufferView
		 * @param  {THREE.BufferAttribute} attribute
		 * @param  {number} componentType
		 * @param  {number} start
		 * @param  {number} count
		 * @param  {number} target (Optional) Target usage of the BufferView
		 * @return {Object}
		 */
		function processBufferView( attribute, componentType, start, count, target ) {
F
Fernando Serrano 已提交
392

393
			if ( ! outputJSON.bufferViews ) {
394

395
				outputJSON.bufferViews = [];
M
Mugen87 已提交
396

397
			}
F
Fernando Serrano 已提交
398

399
			// Create a new dataview and dump the attribute's array into it
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416

			var componentSize;

			if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {

				componentSize = 1;

			} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {

				componentSize = 2;

			} else {

				componentSize = 4;

			}

417
			var byteLength = getPaddedBufferSize( count * attribute.itemSize * componentSize );
418
			var dataView = new DataView( new ArrayBuffer( byteLength ) );
419
			var offset = 0;
F
Fernando Serrano 已提交
420

M
Mugen87 已提交
421
			for ( var i = start; i < start + count; i ++ ) {
F
Fernando Serrano 已提交
422

M
Mugen87 已提交
423
				for ( var a = 0; a < attribute.itemSize; a ++ ) {
F
Fernando Serrano 已提交
424

425 426
					// @TODO Fails on InterleavedBufferAttribute, and could probably be
					// optimized for normal BufferAttribute.
F
Fernando Serrano 已提交
427
					var value = attribute.array[ i * attribute.itemSize + a ];
F
Fernando Serrano 已提交
428

429
					if ( componentType === WEBGL_CONSTANTS.FLOAT ) {
F
Fernando Serrano 已提交
430

F
Fernando Serrano 已提交
431
						dataView.setFloat32( offset, value, true );
F
Fernando Serrano 已提交
432

433
					} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_INT ) {
F
Fernando Serrano 已提交
434

S
selimbek 已提交
435
						dataView.setUint32( offset, value, true );
F
Fernando Serrano 已提交
436

437
					} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {
F
Fernando Serrano 已提交
438

F
Fernando Serrano 已提交
439
						dataView.setUint16( offset, value, true );
F
Fernando Serrano 已提交
440

441 442 443 444
					} else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {

						dataView.setUint8( offset, value );

F
Fernando Serrano 已提交
445
					}
F
Fernando Serrano 已提交
446

447
					offset += componentSize;
F
Fernando Serrano 已提交
448

F
Fernando Serrano 已提交
449
				}
F
Fernando Serrano 已提交
450

F
Fernando Serrano 已提交
451 452 453
			}

			var gltfBufferView = {
F
Fernando Serrano 已提交
454

455
				buffer: processBuffer( dataView.buffer ),
F
Fernando Serrano 已提交
456
				byteOffset: byteOffset,
457
				byteLength: byteLength
F
Fernando Serrano 已提交
458

F
Fernando Serrano 已提交
459 460
			};

461 462
			if ( target !== undefined ) gltfBufferView.target = target;

463 464 465
			if ( target === WEBGL_CONSTANTS.ARRAY_BUFFER ) {

				// Only define byteStride for vertex attributes.
466
				gltfBufferView.byteStride = attribute.itemSize * componentSize;
467 468 469

			}

470
			byteOffset += byteLength;
F
Fernando Serrano 已提交
471

F
Fernando Serrano 已提交
472
			outputJSON.bufferViews.push( gltfBufferView );
F
Fernando Serrano 已提交
473

474
			// @TODO Merge bufferViews where possible.
F
Fernando Serrano 已提交
475
			var output = {
F
Fernando Serrano 已提交
476

F
Fernando Serrano 已提交
477 478
				id: outputJSON.bufferViews.length - 1,
				byteLength: 0
F
Fernando Serrano 已提交
479

F
Fernando Serrano 已提交
480
			};
F
Fernando Serrano 已提交
481

F
Fernando Serrano 已提交
482
			return output;
F
Fernando Serrano 已提交
483

F
Fernando Serrano 已提交
484 485
		}

486 487 488 489 490
		/**
		 * Process and generate a BufferView from an image Blob.
		 * @param {Blob} blob
		 * @return {Promise<Integer>}
		 */
D
Don McCurdy 已提交
491
		function processBufferViewImage( blob ) {
492 493 494 495 496 497 498

			if ( ! outputJSON.bufferViews ) {

				outputJSON.bufferViews = [];

			}

D
Don McCurdy 已提交
499
			return new Promise( function ( resolve ) {
500 501 502

				var reader = new window.FileReader();
				reader.readAsArrayBuffer( blob );
D
Don McCurdy 已提交
503
				reader.onloadend = function () {
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518

					var buffer = getPaddedArrayBuffer( reader.result );

					var bufferView = {
						buffer: processBuffer( buffer ),
						byteOffset: byteOffset,
						byteLength: buffer.byteLength
					};

					byteOffset += buffer.byteLength;

					outputJSON.bufferViews.push( bufferView );

					resolve( outputJSON.bufferViews.length - 1 );

D
Don McCurdy 已提交
519
				};
520 521 522 523 524

			} );

		}

F
Fernando Serrano 已提交
525
		/**
F
Fernando Serrano 已提交
526
		 * Process attribute to generate an accessor
527 528
		 * @param  {THREE.BufferAttribute} attribute Attribute to process
		 * @param  {THREE.BufferGeometry} geometry (Optional) Geometry used for truncated draw range
T
Takahiro 已提交
529 530
		 * @param  {Integer} start (Optional)
		 * @param  {Integer} count (Optional)
F
Fernando Serrano 已提交
531
		 * @return {Integer}           Index of the processed accessor on the "accessors" array
F
Fernando Serrano 已提交
532
		 */
T
Takahiro 已提交
533
		function processAccessor( attribute, geometry, start, count ) {
F
Fernando Serrano 已提交
534

D
Don McCurdy 已提交
535
			var types = {
F
Fernando Serrano 已提交
536

D
Don McCurdy 已提交
537 538 539 540 541
				1: 'SCALAR',
				2: 'VEC2',
				3: 'VEC3',
				4: 'VEC4',
				16: 'MAT4'
F
Fernando Serrano 已提交
542

D
Don McCurdy 已提交
543
			};
F
Fernando Serrano 已提交
544

545 546
			var componentType;

F
Fernando Serrano 已提交
547
			// Detect the component type of the attribute array (float, uint or ushort)
548
			if ( attribute.array.constructor === Float32Array ) {
F
Fernando Serrano 已提交
549

550
				componentType = WEBGL_CONSTANTS.FLOAT;
F
Fernando Serrano 已提交
551

552
			} else if ( attribute.array.constructor === Uint32Array ) {
F
Fernando Serrano 已提交
553

554
				componentType = WEBGL_CONSTANTS.UNSIGNED_INT;
F
Fernando Serrano 已提交
555

556
			} else if ( attribute.array.constructor === Uint16Array ) {
F
Fernando Serrano 已提交
557

558
				componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT;
F
Fernando Serrano 已提交
559

560 561 562 563
			} else if ( attribute.array.constructor === Uint8Array ) {

				componentType = WEBGL_CONSTANTS.UNSIGNED_BYTE;

564
			} else {
F
Fernando Serrano 已提交
565

566
				throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type.' );
F
Fernando Serrano 已提交
567

568
			}
F
Fernando Serrano 已提交
569

T
Takahiro 已提交
570 571
			if ( start === undefined ) start = 0;
			if ( count === undefined ) count = attribute.count;
572 573

			// @TODO Indexed buffer geometry with drawRange not supported yet
574
			if ( options.truncateDrawRange && geometry !== undefined && geometry.index === null ) {
M
Mugen87 已提交
575

T
Takahiro 已提交
576 577
				var end = start + count;
				var end2 = geometry.drawRange.count === Infinity
M
Mugen87 已提交
578 579
					? attribute.count
					: geometry.drawRange.start + geometry.drawRange.count;
T
Takahiro 已提交
580 581 582 583 584

				start = Math.max( start, geometry.drawRange.start );
				count = Math.min( end, end2 ) - start;

				if ( count < 0 ) count = 0;
M
Mugen87 已提交
585

586 587
			}

588
			// Skip creating an accessor if the attribute doesn't have data to export
F
Fernando Serrano 已提交
589
			if ( count === 0 ) {
590

591
				return null;
592 593 594

			}

T
Takahiro 已提交
595 596
			var minMax = getMinMax( attribute, start, count );

597 598 599 600 601 602
			var bufferViewTarget;

			// If geometry isn't provided, don't infer the target usage of the bufferView. For
			// animation samplers, target must not be set.
			if ( geometry !== undefined ) {

T
Takahiro 已提交
603
				bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER;
604 605 606 607

			}

			var bufferView = processBufferView( attribute, componentType, start, count, bufferViewTarget );
F
Fernando Serrano 已提交
608

F
Fernando Serrano 已提交
609
			var gltfAccessor = {
F
Fernando Serrano 已提交
610

F
Fernando Serrano 已提交
611 612 613
				bufferView: bufferView.id,
				byteOffset: bufferView.byteOffset,
				componentType: componentType,
614
				count: count,
F
Fernando Serrano 已提交
615 616
				max: minMax.max,
				min: minMax.min,
D
Don McCurdy 已提交
617
				type: types[ attribute.itemSize ]
F
Fernando Serrano 已提交
618

F
Fernando Serrano 已提交
619 620
			};

621 622 623 624 625 626
			if ( ! outputJSON.accessors ) {

				outputJSON.accessors = [];

			}

F
Fernando Serrano 已提交
627 628 629
			outputJSON.accessors.push( gltfAccessor );

			return outputJSON.accessors.length - 1;
F
Fernando Serrano 已提交
630

F
Fernando Serrano 已提交
631 632 633
		}

		/**
F
Fernando Serrano 已提交
634
		 * Process image
635
		 * @param  {Image} image to process
636
		 * @param  {Integer} format of the image (e.g. THREE.RGBFormat, THREE.RGBAFormat etc)
637
		 * @param  {Boolean} flipY before writing out the image
F
Fernando Serrano 已提交
638 639
		 * @return {Integer}     Index of the processed texture in the "images" array
		 */
640
		function processImage( image, format, flipY ) {
F
Fernando Serrano 已提交
641

642
			if ( ! cachedData.images.has( image ) ) {
643

644 645 646 647 648
				cachedData.images.set( image, {} );

			}

			var cachedImages = cachedData.images.get( image );
649 650
			var mimeType = format === THREE.RGBAFormat ? 'image/png' : 'image/jpeg';
			var key = mimeType + ":flipY/" + flipY.toString();
F
Fernando Serrano 已提交
651

652 653 654
			if ( cachedImages[ key ] !== undefined ) {

				return cachedImages[ key ];
655 656

			}
657

M
Mugen87 已提交
658
			if ( ! outputJSON.images ) {
F
Fernando Serrano 已提交
659

F
Fernando Serrano 已提交
660
				outputJSON.images = [];
F
Fernando Serrano 已提交
661

F
Fernando Serrano 已提交
662 663
			}

M
Mugen87 已提交
664
			var gltfImage = { mimeType: mimeType };
665

666
			if ( options.embedImages ) {
F
Fernando Serrano 已提交
667

668
				var canvas = cachedCanvas = cachedCanvas || document.createElement( 'canvas' );
669

670 671
				canvas.width = image.width;
				canvas.height = image.height;
672

673
				if ( options.forcePowerOfTwoTextures && ! isPowerOfTwo( image ) ) {
674

675
					console.warn( 'GLTFExporter: Resized non-power-of-two image.', image );
676 677 678 679 680 681

					canvas.width = THREE.Math.floorPowerOfTwo( canvas.width );
					canvas.height = THREE.Math.floorPowerOfTwo( canvas.height );

				}

682
				var ctx = canvas.getContext( '2d' );
683

684
				if ( flipY === true ) {
685

686
					ctx.translate( 0, canvas.height );
M
Mugen87 已提交
687
					ctx.scale( 1, - 1 );
688 689 690

				}

691
				ctx.drawImage( image, 0, 0, canvas.width, canvas.height );
692

693 694 695 696 697 698 699 700 701 702 703
				if ( options.binary === true ) {

					pending.push( new Promise( function ( resolve ) {

						canvas.toBlob( function ( blob ) {

							processBufferViewImage( blob ).then( function ( bufferViewIndex ) {

								gltfImage.bufferView = bufferViewIndex;

								resolve();
704

705 706 707 708 709 710 711 712 713 714 715
							} );

						}, mimeType );

					} ) );

				} else {

					gltfImage.uri = canvas.toDataURL( mimeType );

				}
F
Fernando Serrano 已提交
716

F
Fernando Serrano 已提交
717
			} else {
F
Fernando Serrano 已提交
718

719
				gltfImage.uri = image.src;
F
Fernando Serrano 已提交
720

F
Fernando Serrano 已提交
721 722 723 724
			}

			outputJSON.images.push( gltfImage );

725
			var index = outputJSON.images.length - 1;
726
			cachedImages[ key ] = index;
F
Fernando Serrano 已提交
727

728
			return index;
F
Fernando Serrano 已提交
729

F
Fernando Serrano 已提交
730 731 732 733 734 735 736
		}

		/**
		 * Process sampler
		 * @param  {Texture} map Texture to process
		 * @return {Integer}     Index of the processed texture in the "samplers" array
		 */
M
Mr.doob 已提交
737
		function processSampler( map ) {
F
Fernando Serrano 已提交
738

M
Mugen87 已提交
739
			if ( ! outputJSON.samplers ) {
F
Fernando Serrano 已提交
740

F
Fernando Serrano 已提交
741
				outputJSON.samplers = [];
F
Fernando Serrano 已提交
742

F
Fernando Serrano 已提交
743 744 745
			}

			var gltfSampler = {
F
Fernando Serrano 已提交
746

747 748 749 750
				magFilter: THREE_TO_WEBGL[ map.magFilter ],
				minFilter: THREE_TO_WEBGL[ map.minFilter ],
				wrapS: THREE_TO_WEBGL[ map.wrapS ],
				wrapT: THREE_TO_WEBGL[ map.wrapT ]
F
Fernando Serrano 已提交
751

F
Fernando Serrano 已提交
752 753 754 755 756
			};

			outputJSON.samplers.push( gltfSampler );

			return outputJSON.samplers.length - 1;
F
Fernando Serrano 已提交
757

F
Fernando Serrano 已提交
758 759 760 761 762 763 764
		}

		/**
		 * Process texture
		 * @param  {Texture} map Map to process
		 * @return {Integer}     Index of the processed texture in the "textures" array
		 */
M
Mr.doob 已提交
765
		function processTexture( map ) {
F
Fernando Serrano 已提交
766

T
Takahiro 已提交
767
			if ( cachedData.textures.has( map ) ) {
T
Takahiro 已提交
768

T
Takahiro 已提交
769
				return cachedData.textures.get( map );
T
Takahiro 已提交
770 771 772

			}

M
Mugen87 已提交
773
			if ( ! outputJSON.textures ) {
F
Fernando Serrano 已提交
774

F
Fernando Serrano 已提交
775
				outputJSON.textures = [];
F
Fernando Serrano 已提交
776

F
Fernando Serrano 已提交
777 778 779
			}

			var gltfTexture = {
F
Fernando Serrano 已提交
780

F
Fernando Serrano 已提交
781
				sampler: processSampler( map ),
M
Mugen87 已提交
782
				source: processImage( map.image, map.format, map.flipY )
F
Fernando Serrano 已提交
783

F
Fernando Serrano 已提交
784 785 786 787
			};

			outputJSON.textures.push( gltfTexture );

T
Takahiro 已提交
788
			var index = outputJSON.textures.length - 1;
T
Takahiro 已提交
789
			cachedData.textures.set( map, index );
T
Takahiro 已提交
790 791

			return index;
F
Fernando Serrano 已提交
792

F
Fernando Serrano 已提交
793 794 795 796 797 798
		}

		/**
		 * Process material
		 * @param  {THREE.Material} material Material to process
		 * @return {Integer}      Index of the processed material in the "materials" array
F
Fernando Serrano 已提交
799
		 */
M
Mr.doob 已提交
800
		function processMaterial( material ) {
F
Fernando Serrano 已提交
801

T
Takahiro 已提交
802
			if ( cachedData.materials.has( material ) ) {
803

T
Takahiro 已提交
804
				return cachedData.materials.get( material );
805 806 807

			}

M
Mugen87 已提交
808
			if ( ! outputJSON.materials ) {
F
Fernando Serrano 已提交
809

F
Fernando Serrano 已提交
810
				outputJSON.materials = [];
F
Fernando Serrano 已提交
811

F
Fernando Serrano 已提交
812
			}
F
Fernando Serrano 已提交
813

814
			if ( material.isShaderMaterial ) {
815 816 817 818 819 820

				console.warn( 'GLTFExporter: THREE.ShaderMaterial not supported.' );
				return null;

			}

F
Fernando Serrano 已提交
821
			// @QUESTION Should we avoid including any attribute that has the default value?
822
			var gltfMaterial = {
F
Fernando Serrano 已提交
823

824
				pbrMetallicRoughness: {}
F
Fernando Serrano 已提交
825

826
			};
827

828
			if ( material.isMeshBasicMaterial ) {
829 830 831 832 833

				gltfMaterial.extensions = { KHR_materials_unlit: {} };

				extensionsUsed[ 'KHR_materials_unlit' ] = true;

834
			} else if ( ! material.isMeshStandardMaterial ) {
835 836 837 838 839

				console.warn( 'GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results.' );

			}

840 841
			// pbrMetallicRoughness.baseColorFactor
			var color = material.color.toArray().concat( [ material.opacity ] );
F
Fernando Serrano 已提交
842

M
Mugen87 已提交
843
			if ( ! equalArray( color, [ 1, 1, 1, 1 ] ) ) {
844

845
				gltfMaterial.pbrMetallicRoughness.baseColorFactor = color;
846 847 848

			}

849
			if ( material.isMeshStandardMaterial ) {
850

851 852
				gltfMaterial.pbrMetallicRoughness.metallicFactor = material.metalness;
				gltfMaterial.pbrMetallicRoughness.roughnessFactor = material.roughness;
853

854
			} else if ( material.isMeshBasicMaterial ) {
855 856 857 858

				gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.0;
				gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.9;

M
Mr.doob 已提交
859
			} else {
860

M
Mugen87 已提交
861 862
				gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.5;
				gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.5;
F
Fernando Serrano 已提交
863

864
			}
865

866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884
			// pbrMetallicRoughness.metallicRoughnessTexture
			if ( material.metalnessMap || material.roughnessMap ) {

				if ( material.metalnessMap === material.roughnessMap ) {

					gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture = {

						index: processTexture( material.metalnessMap )

					};

				} else {

					console.warn( 'THREE.GLTFExporter: Ignoring metalnessMap and roughnessMap because they are not the same Texture.' );

				}

			}

885 886
			// pbrMetallicRoughness.baseColorTexture
			if ( material.map ) {
887

888
				gltfMaterial.pbrMetallicRoughness.baseColorTexture = {
F
Fernando Serrano 已提交
889

890
					index: processTexture( material.map )
F
Fernando Serrano 已提交
891

892
				};
893

894
			}
895

896 897 898
			if ( material.isMeshBasicMaterial ||
				material.isLineBasicMaterial ||
				material.isPointsMaterial ) {
899 900 901 902

			} else {

				// emissiveFactor
903
				var emissive = material.emissive.clone().multiplyScalar( material.emissiveIntensity ).toArray();
F
Fernando Serrano 已提交
904

M
Mugen87 已提交
905
				if ( ! equalArray( emissive, [ 0, 0, 0 ] ) ) {
906 907 908

					gltfMaterial.emissiveFactor = emissive;

F
Fernando Serrano 已提交
909
				}
910 911 912 913 914 915

				// emissiveTexture
				if ( material.emissiveMap ) {

					gltfMaterial.emissiveTexture = {

916
						index: processTexture( material.emissiveMap )
917 918 919 920 921 922 923 924 925 926 927

					};

				}

			}

			// normalTexture
			if ( material.normalMap ) {

				gltfMaterial.normalTexture = {
F
Fernando Serrano 已提交
928

929
					index: processTexture( material.normalMap )
F
Fernando Serrano 已提交
930

931 932
				};

M
Mugen87 已提交
933
				if ( material.normalScale.x !== - 1 ) {
934 935 936

					if ( material.normalScale.x !== material.normalScale.y ) {

M
Mugen87 已提交
937
						console.warn( 'THREE.GLTFExporter: Normal scale components are different, ignoring Y and exporting X.' );
938 939 940 941 942 943 944

					}

					gltfMaterial.normalTexture.scale = material.normalScale.x;

				}

F
Fernando Serrano 已提交
945 946
			}

947 948 949 950
			// occlusionTexture
			if ( material.aoMap ) {

				gltfMaterial.occlusionTexture = {
F
Fernando Serrano 已提交
951

952
					index: processTexture( material.aoMap )
F
Fernando Serrano 已提交
953

954 955
				};

956 957 958 959 960 961
				if ( material.aoMapIntensity !== 1.0 ) {

					gltfMaterial.occlusionTexture.strength = material.aoMapIntensity;

				}

962 963 964
			}

			// alphaMode
965
			if ( material.transparent || material.alphaTest > 0.0 ) {
966

967
				gltfMaterial.alphaMode = material.opacity < 1.0 ? 'BLEND' : 'MASK';
968

969 970
				// Write alphaCutoff if it's non-zero and different from the default (0.5).
				if ( material.alphaTest > 0.0 && material.alphaTest !== 0.5 ) {
971 972 973 974

					gltfMaterial.alphaCutoff = material.alphaTest;

				}
975 976 977 978

			}

			// doubleSided
F
Fernando Serrano 已提交
979
			if ( material.side === THREE.DoubleSide ) {
980

F
Fernando Serrano 已提交
981
				gltfMaterial.doubleSided = true;
982

F
Fernando Serrano 已提交
983 984
			}

985
			if ( material.name !== '' ) {
986

F
Fernando Serrano 已提交
987
				gltfMaterial.name = material.name;
988

F
Fernando Serrano 已提交
989 990
			}

991 992
			if ( Object.keys( material.userData ).length > 0 ) {

993
				gltfMaterial.extras = serializeUserData( material );
994 995 996

			}

F
Fernando Serrano 已提交
997
			outputJSON.materials.push( gltfMaterial );
F
Fernando Serrano 已提交
998

999
			var index = outputJSON.materials.length - 1;
T
Takahiro 已提交
1000
			cachedData.materials.set( material, index );
1001 1002

			return index;
1003

F
Fernando Serrano 已提交
1004 1005 1006
		}

		/**
F
Fernando Serrano 已提交
1007 1008 1009
		 * Process mesh
		 * @param  {THREE.Mesh} mesh Mesh to process
		 * @return {Integer}      Index of the processed mesh in the "meshes" array
F
Fernando Serrano 已提交
1010 1011
		 */
		function processMesh( mesh ) {
F
Fernando Serrano 已提交
1012

1013 1014 1015 1016 1017 1018 1019
			var cacheKey = mesh.geometry.uuid + ':' + mesh.material.uuid;
			if ( cachedData.meshes.has( cacheKey ) ) {

				return cachedData.meshes.get( cacheKey );

			}

F
Fernando Serrano 已提交
1020
			var geometry = mesh.geometry;
F
Fernando Serrano 已提交
1021

M
Mr.doob 已提交
1022 1023
			var mode;

1024
			// Use the correct mode
1025
			if ( mesh.isLineSegments ) {
1026

1027
				mode = WEBGL_CONSTANTS.LINES;
1028

1029
			} else if ( mesh.isLineLoop ) {
1030

1031
				mode = WEBGL_CONSTANTS.LINE_LOOP;
1032

1033
			} else if ( mesh.isLine ) {
1034

1035
				mode = WEBGL_CONSTANTS.LINE_STRIP;
1036

1037
			} else if ( mesh.isPoints ) {
1038

1039
				mode = WEBGL_CONSTANTS.POINTS;
1040 1041 1042

			} else {

M
Mugen87 已提交
1043
				if ( ! geometry.isBufferGeometry ) {
1044 1045 1046 1047 1048 1049 1050

					var geometryTemp = new THREE.BufferGeometry();
					geometryTemp.fromGeometry( geometry );
					geometry = geometryTemp;

				}

1051 1052
				if ( mesh.drawMode === THREE.TriangleFanDrawMode ) {

F
Fernando Serrano 已提交
1053
					console.warn( 'GLTFExporter: TriangleFanDrawMode and wireframe incompatible.' );
1054
					mode = WEBGL_CONSTANTS.TRIANGLE_FAN;
1055 1056 1057

				} else if ( mesh.drawMode === THREE.TriangleStripDrawMode ) {

1058
					mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINE_STRIP : WEBGL_CONSTANTS.TRIANGLE_STRIP;
1059 1060 1061

				} else {

1062
					mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES;
1063 1064 1065 1066

				}

			}
F
Fernando Serrano 已提交
1067

T
Takahiro 已提交
1068
			var gltfMesh = {};
1069

T
Takahiro 已提交
1070 1071 1072
			var attributes = {};
			var primitives = [];
			var targets = [];
F
Fernando Serrano 已提交
1073

F
Fernando Serrano 已提交
1074 1075
			// Conversion between attributes names in threejs and gltf spec
			var nameConversion = {
F
Fernando Serrano 已提交
1076

F
Fernando Serrano 已提交
1077 1078
				uv: 'TEXCOORD_0',
				uv2: 'TEXCOORD_1',
1079 1080 1081
				color: 'COLOR_0',
				skinWeight: 'WEIGHTS_0',
				skinIndex: 'JOINTS_0'
F
Fernando Serrano 已提交
1082

F
Fernando Serrano 已提交
1083 1084
			};

1085 1086 1087 1088 1089 1090 1091 1092 1093 1094
			var originalNormal = geometry.getAttribute( 'normal' );

			if ( originalNormal !== undefined && ! isNormalizedNormalAttribute( originalNormal ) ) {

				console.warn( 'THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.' );

				geometry.addAttribute( 'normal', createNormalizedNormalAttribute( originalNormal ) );

			}

1095
			// @QUESTION Detect if .vertexColors = THREE.VertexColors?
F
Fernando Serrano 已提交
1096
			// For every attribute create an accessor
F
Fernando Serrano 已提交
1097 1098
			for ( var attributeName in geometry.attributes ) {

F
Fernando Serrano 已提交
1099
				var attribute = geometry.attributes[ attributeName ];
F
Fernando Serrano 已提交
1100
				attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();
D
Don McCurdy 已提交
1101

1102 1103 1104 1105 1106 1107 1108
				if ( cachedData.attributes.has( attribute ) ) {

					attributes[ attributeName ] = cachedData.attributes.get( attribute );
					continue;

				}

1109
				// JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT.
1110
				var modifiedAttribute;
1111
				var array = attribute.array;
1112
				if ( attributeName === 'JOINTS_0' &&
1113 1114
					! ( array instanceof Uint16Array ) &&
					! ( array instanceof Uint8Array ) ) {
1115 1116

					console.warn( 'GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.' );
1117
					modifiedAttribute = new THREE.BufferAttribute( new Uint16Array( array ), attribute.itemSize, attribute.normalized );
1118 1119 1120

				}

D
Don McCurdy 已提交
1121
				if ( attributeName.substr( 0, 5 ) !== 'MORPH' ) {
F
Fernando Serrano 已提交
1122

1123
					var accessor = processAccessor( modifiedAttribute || attribute, geometry );
1124
					if ( accessor !== null ) {
D
Don McCurdy 已提交
1125

1126
						attributes[ attributeName ] = accessor;
1127
						cachedData.attributes.set( attribute, accessor );
1128 1129

					}
D
Don McCurdy 已提交
1130 1131 1132 1133 1134

				}

			}

1135 1136
			if ( originalNormal !== undefined ) geometry.addAttribute( 'normal', originalNormal );

1137 1138 1139 1140 1141 1142 1143
			// Skip if no exportable attributes found
			if ( Object.keys( attributes ).length === 0 ) {

				return null;

			}

D
Don McCurdy 已提交
1144 1145 1146
			// Morph targets
			if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) {

1147
				var weights = [];
1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160
				var targetNames = [];
				var reverseDictionary = {};

				if ( mesh.morphTargetDictionary !== undefined ) {

					for ( var key in mesh.morphTargetDictionary ) {

						reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key;

					}

				}

D
Don McCurdy 已提交
1161 1162 1163 1164
				for ( var i = 0; i < mesh.morphTargetInfluences.length; ++ i ) {

					var target = {};

1165 1166
					var warned = false;

D
Don McCurdy 已提交
1167 1168
					for ( var attributeName in geometry.morphAttributes ) {

T
Takahiro 已提交
1169
						// glTF 2.0 morph supports only POSITION/NORMAL/TANGENT.
1170
						// Three.js doesn't support TANGENT yet.
T
Takahiro 已提交
1171 1172 1173

						if ( attributeName !== 'position' && attributeName !== 'normal' ) {

1174 1175 1176 1177 1178 1179 1180
							if ( ! warned ) {

								console.warn( 'GLTFExporter: Only POSITION and NORMAL morph are supported.' );
								warned = true;

							}

T
Takahiro 已提交
1181 1182 1183 1184
							continue;

						}

D
Don McCurdy 已提交
1185
						var attribute = geometry.morphAttributes[ attributeName ][ i ];
1186
						var gltfAttributeName = attributeName.toUpperCase();
T
Takahiro 已提交
1187

1188
						// Three.js morph attribute has absolute values while the one of glTF has relative values.
T
Takahiro 已提交
1189 1190 1191 1192 1193
						//
						// glTF 2.0 Specification:
						// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets

						var baseAttribute = geometry.attributes[ attributeName ];
1194 1195 1196 1197 1198 1199 1200 1201

						if ( cachedData.attributes.has( baseAttribute ) ) {

							target[ gltfAttributeName ] = cachedData.attributes.get( baseAttribute );
							continue;

						}

T
Takahiro 已提交
1202
						// Clones attribute not to override
1203
						var relativeAttribute = attribute.clone();
T
Takahiro 已提交
1204 1205 1206

						for ( var j = 0, jl = attribute.count; j < jl; j ++ ) {

1207
							relativeAttribute.setXYZ(
T
Takahiro 已提交
1208 1209 1210 1211 1212 1213 1214 1215
								j,
								attribute.getX( j ) - baseAttribute.getX( j ),
								attribute.getY( j ) - baseAttribute.getY( j ),
								attribute.getZ( j ) - baseAttribute.getZ( j )
							);

						}

1216 1217
						target[ gltfAttributeName ] = processAccessor( relativeAttribute, geometry );
						cachedData.attributes.set( baseAttribute, target[ gltfAttributeName ] );
D
Don McCurdy 已提交
1218 1219 1220

					}

T
Takahiro 已提交
1221
					targets.push( target );
D
Don McCurdy 已提交
1222

1223
					weights.push( mesh.morphTargetInfluences[ i ] );
1224
					if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] );
1225

D
Don McCurdy 已提交
1226
				}
F
Fernando Serrano 已提交
1227

1228 1229
				gltfMesh.weights = weights;

1230 1231 1232 1233 1234 1235 1236
				if ( targetNames.length > 0 ) {

					gltfMesh.extras = {};
					gltfMesh.extras.targetNames = targetNames;

				}

F
Fernando Serrano 已提交
1237 1238
			}

1239 1240
			var extras = ( Object.keys( geometry.userData ).length > 0 ) ? serializeUserData( geometry ) : undefined;

1241
			var forceIndices = options.forceIndices;
T
Takahiro 已提交
1242
			var isMultiMaterial = Array.isArray( mesh.material );
1243

1244
			if ( isMultiMaterial && geometry.groups.length === 0 ) return null;
1245

1246
			if ( ! forceIndices && geometry.index === null && isMultiMaterial ) {
1247 1248

				// temporal workaround.
1249
				console.warn( 'THREE.GLTFExporter: Creating index for non-indexed multi-material mesh.' );
1250 1251 1252 1253
				forceIndices = true;

			}

1254
			var didForceIndices = false;
T
Takahiro 已提交
1255

1256
			if ( geometry.index === null && forceIndices ) {
T
Takahiro 已提交
1257

1258
				var indices = [];
T
Takahiro 已提交
1259

1260
				for ( var i = 0, il = geometry.attributes.position.count; i < il; i ++ ) {
T
Takahiro 已提交
1261 1262 1263 1264 1265

					indices[ i ] = i;

				}

1266
				geometry.setIndex( indices );
T
Takahiro 已提交
1267

1268
				didForceIndices = true;
T
Takahiro 已提交
1269 1270 1271

			}

F
Fernando Serrano 已提交
1272
			var materials = isMultiMaterial ? mesh.material : [ mesh.material ];
1273
			var groups = isMultiMaterial ? geometry.groups : [ { materialIndex: 0, start: undefined, count: undefined } ];
T
Takahiro 已提交
1274

1275
			for ( var i = 0, il = groups.length; i < il; i ++ ) {
T
Takahiro 已提交
1276 1277 1278 1279 1280 1281

				var primitive = {
					mode: mode,
					attributes: attributes,
				};

1282 1283
				if ( extras ) primitive.extras = extras;

T
Takahiro 已提交
1284 1285 1286 1287
				if ( targets.length > 0 ) primitive.targets = targets;

				if ( geometry.index !== null ) {

1288 1289 1290 1291 1292 1293 1294 1295 1296 1297
					if ( cachedData.attributes.has( geometry.index ) ) {

						primitive.indices = cachedData.attributes.get( geometry.index );

					} else {

						primitive.indices = processAccessor( geometry.index, geometry, groups[ i ].start, groups[ i ].count );
						cachedData.attributes.set( geometry.index, primitive.indices );

					}
T
Takahiro 已提交
1298 1299 1300

				}

1301
				var material = processMaterial( materials[ groups[ i ].materialIndex ] );
1302

1303
				if ( material !== null ) {
1304

1305
					primitive.material = material;
1306 1307

				}
T
Takahiro 已提交
1308

1309 1310
				primitives.push( primitive );

T
Takahiro 已提交
1311 1312
			}

1313
			if ( didForceIndices ) {
T
Takahiro 已提交
1314

1315
				geometry.setIndex( null );
T
Takahiro 已提交
1316 1317 1318 1319 1320

			}

			gltfMesh.primitives = primitives;

1321 1322 1323 1324 1325
			if ( ! outputJSON.meshes ) {

				outputJSON.meshes = [];

			}
F
Fernando Serrano 已提交
1326

F
Fernando Serrano 已提交
1327 1328
			outputJSON.meshes.push( gltfMesh );

1329 1330 1331 1332
			var index = outputJSON.meshes.length - 1;
			cachedData.meshes.set( cacheKey, index );

			return index;
M
Mugen87 已提交
1333

F
Fernando Serrano 已提交
1334 1335
		}

F
Fernando Serrano 已提交
1336 1337 1338 1339 1340 1341
		/**
		 * Process camera
		 * @param  {THREE.Camera} camera Camera to process
		 * @return {Integer}      Index of the processed mesh in the "camera" array
		 */
		function processCamera( camera ) {
F
Fernando Serrano 已提交
1342

M
Mugen87 已提交
1343
			if ( ! outputJSON.cameras ) {
F
Fernando Serrano 已提交
1344

F
Fernando Serrano 已提交
1345
				outputJSON.cameras = [];
F
Fernando Serrano 已提交
1346

F
Fernando Serrano 已提交
1347 1348
			}

1349
			var isOrtho = camera.isOrthographicCamera;
F
Fernando Serrano 已提交
1350 1351

			var gltfCamera = {
F
Fernando Serrano 已提交
1352

F
Fernando Serrano 已提交
1353
				type: isOrtho ? 'orthographic' : 'perspective'
F
Fernando Serrano 已提交
1354

F
Fernando Serrano 已提交
1355 1356 1357 1358 1359 1360 1361 1362
			};

			if ( isOrtho ) {

				gltfCamera.orthographic = {

					xmag: camera.right * 2,
					ymag: camera.top * 2,
1363 1364
					zfar: camera.far <= 0 ? 0.001 : camera.far,
					znear: camera.near < 0 ? 0 : camera.near
F
Fernando Serrano 已提交
1365

F
Fernando Serrano 已提交
1366
				};
F
Fernando Serrano 已提交
1367 1368 1369 1370 1371 1372 1373

			} else {

				gltfCamera.perspective = {

					aspectRatio: camera.aspect,
					yfov: THREE.Math.degToRad( camera.fov ) / camera.aspect,
1374 1375
					zfar: camera.far <= 0 ? 0.001 : camera.far,
					znear: camera.near < 0 ? 0 : camera.near
F
Fernando Serrano 已提交
1376 1377 1378 1379 1380

				};

			}

1381
			if ( camera.name !== '' ) {
F
Fernando Serrano 已提交
1382

F
Fernando Serrano 已提交
1383
				gltfCamera.name = camera.type;
F
Fernando Serrano 已提交
1384

F
Fernando Serrano 已提交
1385 1386 1387 1388 1389
			}

			outputJSON.cameras.push( gltfCamera );

			return outputJSON.cameras.length - 1;
M
Mugen87 已提交
1390

F
Fernando Serrano 已提交
1391 1392
		}

1393 1394 1395 1396 1397 1398 1399 1400 1401 1402
		/**
		 * Creates glTF animation entry from AnimationClip object.
		 *
		 * Status:
		 * - Only properties listed in PATH_PROPERTIES may be animated.
		 *
		 * @param {THREE.AnimationClip} clip
		 * @param {THREE.Object3D} root
		 * @return {number}
		 */
M
Mugen87 已提交
1403
		function processAnimation( clip, root ) {
1404 1405 1406 1407 1408 1409 1410

			if ( ! outputJSON.animations ) {

				outputJSON.animations = [];

			}

D
Don McCurdy 已提交
1411
			clip = THREE.AnimationUtils.mergeMorphTargetTracks( clip.clone(), root );
1412 1413

			var tracks = clip.tracks;
1414 1415 1416
			var channels = [];
			var samplers = [];

1417
			for ( var i = 0; i < tracks.length; ++ i ) {
1418

1419
				var track = tracks[ i ];
1420 1421 1422 1423
				var trackBinding = THREE.PropertyBinding.parseTrackName( track.name );
				var trackNode = THREE.PropertyBinding.findNode( root, trackBinding.nodeName );
				var trackProperty = PATH_PROPERTIES[ trackBinding.propertyName ];

1424
				if ( trackBinding.objectName === 'bones' ) {
1425

1426 1427 1428 1429 1430 1431 1432 1433 1434
					if ( trackNode.isSkinnedMesh === true ) {

						trackNode = trackNode.skeleton.getBoneByName( trackBinding.objectIndex );

					} else {

						trackNode = undefined;

					}
1435 1436 1437

				}

1438 1439 1440
				if ( ! trackNode || ! trackProperty ) {

					console.warn( 'THREE.GLTFExporter: Could not export animation track "%s".', track.name );
1441
					return null;
1442 1443 1444

				}

D
Don McCurdy 已提交
1445 1446 1447 1448 1449 1450 1451 1452 1453
				var inputItemSize = 1;
				var outputItemSize = track.values.length / track.times.length;

				if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) {

					outputItemSize /= trackNode.morphTargetInfluences.length;

				}

T
Takahiro 已提交
1454 1455
				var interpolation;

1456 1457
				// @TODO export CubicInterpolant(InterpolateSmooth) as CUBICSPLINE

1458
				// Detecting glTF cubic spline interpolant by checking factory method's special property
1459 1460
				// GLTFCubicSplineInterpolant is a custom interpolant and track doesn't return
				// valid value from .getInterpolation().
1461
				if ( track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true ) {
T
Takahiro 已提交
1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479

					interpolation = 'CUBICSPLINE';

					// itemSize of CUBICSPLINE keyframe is 9
					// (VEC3 * 3: inTangent, splineVertex, and outTangent)
					// but needs to be stored as VEC3 so dividing by 3 here.
					outputItemSize /= 3;

				} else if ( track.getInterpolation() === THREE.InterpolateDiscrete ) {

					interpolation = 'STEP';

				} else {

					interpolation = 'LINEAR';

				}

1480 1481
				samplers.push( {

D
Don McCurdy 已提交
1482 1483
					input: processAccessor( new THREE.BufferAttribute( track.times, inputItemSize ) ),
					output: processAccessor( new THREE.BufferAttribute( track.values, outputItemSize ) ),
T
Takahiro 已提交
1484
					interpolation: interpolation
1485 1486 1487 1488 1489 1490 1491

				} );

				channels.push( {

					sampler: samplers.length - 1,
					target: {
T
Takahiro 已提交
1492
						node: nodeMap.get( trackNode ),
1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511
						path: trackProperty
					}

				} );

			}

			outputJSON.animations.push( {

				name: clip.name || 'clip_' + outputJSON.animations.length,
				samplers: samplers,
				channels: channels

			} );

			return outputJSON.animations.length - 1;

		}

D
Don McCurdy 已提交
1512 1513
		function processSkin( object ) {

T
Takahiro 已提交
1514
			var node = outputJSON.nodes[ nodeMap.get( object ) ];
D
Don McCurdy 已提交
1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525

			var skeleton = object.skeleton;
			var rootJoint = object.skeleton.bones[ 0 ];

			if ( rootJoint === undefined ) return null;

			var joints = [];
			var inverseBindMatrices = new Float32Array( skeleton.bones.length * 16 );

			for ( var i = 0; i < skeleton.bones.length; ++ i ) {

T
Takahiro 已提交
1526
				joints.push( nodeMap.get( skeleton.bones[ i ] ) );
D
Don McCurdy 已提交
1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541

				skeleton.boneInverses[ i ].toArray( inverseBindMatrices, i * 16 );

			}

			if ( outputJSON.skins === undefined ) {

				outputJSON.skins = [];

			}

			outputJSON.skins.push( {

				inverseBindMatrices: processAccessor( new THREE.BufferAttribute( inverseBindMatrices, 16 ) ),
				joints: joints,
T
Takahiro 已提交
1542
				skeleton: nodeMap.get( rootJoint )
D
Don McCurdy 已提交
1543 1544 1545 1546 1547 1548 1549 1550 1551

			} );

			var skinIndex = node.skin = outputJSON.skins.length - 1;

			return skinIndex;

		}

F
Fernando Serrano 已提交
1552 1553 1554 1555 1556
		/**
		 * Process Object3D node
		 * @param  {THREE.Object3D} node Object3D to processNode
		 * @return {Integer}      Index of the node in the nodes list
		 */
M
Mr.doob 已提交
1557
		function processNode( object ) {
F
Fernando Serrano 已提交
1558

1559
			if ( object.isLight ) {
1560 1561

				console.warn( 'GLTFExporter: Unsupported node type:', object.constructor.name );
1562
				return null;
1563 1564 1565

			}

M
Mugen87 已提交
1566
			if ( ! outputJSON.nodes ) {
F
Fernando Serrano 已提交
1567

F
Fernando Serrano 已提交
1568
				outputJSON.nodes = [];
F
Fernando Serrano 已提交
1569

F
Fernando Serrano 已提交
1570 1571
			}

F
Fernando Serrano 已提交
1572 1573 1574
			var gltfNode = {};

			if ( options.trs ) {
F
Fernando Serrano 已提交
1575

F
Fernando Serrano 已提交
1576 1577 1578 1579
				var rotation = object.quaternion.toArray();
				var position = object.position.toArray();
				var scale = object.scale.toArray();

M
Mugen87 已提交
1580
				if ( ! equalArray( rotation, [ 0, 0, 0, 1 ] ) ) {
F
Fernando Serrano 已提交
1581

F
Fernando Serrano 已提交
1582
					gltfNode.rotation = rotation;
F
Fernando Serrano 已提交
1583

F
Fernando Serrano 已提交
1584 1585
				}

M
Mugen87 已提交
1586
				if ( ! equalArray( position, [ 0, 0, 0 ] ) ) {
F
Fernando Serrano 已提交
1587

D
Don McCurdy 已提交
1588
					gltfNode.translation = position;
F
Fernando Serrano 已提交
1589

F
Fernando Serrano 已提交
1590 1591
				}

M
Mugen87 已提交
1592
				if ( ! equalArray( scale, [ 1, 1, 1 ] ) ) {
F
Fernando Serrano 已提交
1593

F
Fernando Serrano 已提交
1594
					gltfNode.scale = scale;
F
Fernando Serrano 已提交
1595

F
Fernando Serrano 已提交
1596 1597 1598
				}

			} else {
F
Fernando Serrano 已提交
1599

F
Fernando Serrano 已提交
1600
				object.updateMatrix();
M
Mugen87 已提交
1601
				if ( ! equalArray( object.matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] ) ) {
F
Fernando Serrano 已提交
1602

F
Fernando Serrano 已提交
1603
					gltfNode.matrix = object.matrix.elements;
F
Fernando Serrano 已提交
1604

F
Fernando Serrano 已提交
1605
				}
F
Fernando Serrano 已提交
1606

F
Fernando Serrano 已提交
1607 1608
			}

1609
			// We don't export empty strings name because it represents no-name in Three.js.
1610
			if ( object.name !== '' ) {
F
Fernando Serrano 已提交
1611

C
Christopher Cook 已提交
1612
				gltfNode.name = String( object.name );
F
Fernando Serrano 已提交
1613

F
Fernando Serrano 已提交
1614 1615
			}

1616 1617
			if ( object.userData && Object.keys( object.userData ).length > 0 ) {

1618
				gltfNode.extras = serializeUserData( object );
1619 1620 1621

			}

1622
			if ( object.isMesh || object.isLine || object.isPoints ) {
F
Fernando Serrano 已提交
1623

1624 1625
				var mesh = processMesh( object );

1626
				if ( mesh !== null ) {
1627 1628 1629 1630

					gltfNode.mesh = mesh;

				}
F
Fernando Serrano 已提交
1631

1632
			} else if ( object.isCamera ) {
F
Fernando Serrano 已提交
1633

F
Fernando Serrano 已提交
1634
				gltfNode.camera = processCamera( object );
F
Fernando Serrano 已提交
1635

F
Fernando Serrano 已提交
1636 1637
			}

1638
			if ( object.isSkinnedMesh ) {
D
Don McCurdy 已提交
1639 1640 1641 1642 1643

				skins.push( object );

			}

F
Fernando Serrano 已提交
1644
			if ( object.children.length > 0 ) {
F
Fernando Serrano 已提交
1645

1646
				var children = [];
F
Fernando Serrano 已提交
1647 1648

				for ( var i = 0, l = object.children.length; i < l; i ++ ) {
F
Fernando Serrano 已提交
1649

F
Fernando Serrano 已提交
1650
					var child = object.children[ i ];
F
Fernando Serrano 已提交
1651

1652 1653
					if ( child.visible || options.onlyVisible === false ) {

1654 1655
						var node = processNode( child );

1656
						if ( node !== null ) {
1657

1658
							children.push( node );
1659 1660

						}
F
Fernando Serrano 已提交
1661

F
Fernando Serrano 已提交
1662
					}
F
Fernando Serrano 已提交
1663

F
Fernando Serrano 已提交
1664
				}
F
Fernando Serrano 已提交
1665

1666 1667 1668 1669 1670 1671 1672
				if ( children.length > 0 ) {

					gltfNode.children = children;

				}


F
Fernando Serrano 已提交
1673 1674 1675 1676
			}

			outputJSON.nodes.push( gltfNode );

T
Takahiro 已提交
1677 1678
			var nodeIndex = outputJSON.nodes.length - 1;
			nodeMap.set( object, nodeIndex );
1679 1680

			return nodeIndex;
F
Fernando Serrano 已提交
1681

F
Fernando Serrano 已提交
1682 1683 1684
		}

		/**
F
Fernando Serrano 已提交
1685
		 * Process Scene
F
Fernando Serrano 已提交
1686 1687 1688
		 * @param  {THREE.Scene} node Scene to process
		 */
		function processScene( scene ) {
F
Fernando Serrano 已提交
1689

M
Mugen87 已提交
1690
			if ( ! outputJSON.scenes ) {
F
Fernando Serrano 已提交
1691

F
Fernando Serrano 已提交
1692 1693
				outputJSON.scenes = [];
				outputJSON.scene = 0;
F
Fernando Serrano 已提交
1694

F
Fernando Serrano 已提交
1695 1696 1697
			}

			var gltfScene = {
F
Fernando Serrano 已提交
1698

F
Fernando Serrano 已提交
1699
				nodes: []
F
Fernando Serrano 已提交
1700

F
Fernando Serrano 已提交
1701 1702
			};

1703
			if ( scene.name !== '' ) {
F
Fernando Serrano 已提交
1704

F
Fernando Serrano 已提交
1705
				gltfScene.name = scene.name;
F
Fernando Serrano 已提交
1706

F
Fernando Serrano 已提交
1707 1708
			}

M
makc 已提交
1709 1710 1711 1712 1713 1714
			if ( scene.userData && Object.keys( scene.userData ).length > 0 ) {

				gltfScene.extras = serializeUserData( scene );

			}

F
Fernando Serrano 已提交
1715
			outputJSON.scenes.push( gltfScene );
F
Fernando Serrano 已提交
1716

1717 1718
			var nodes = [];

F
Fernando Serrano 已提交
1719
			for ( var i = 0, l = scene.children.length; i < l; i ++ ) {
F
Fernando Serrano 已提交
1720

F
Fernando Serrano 已提交
1721 1722
				var child = scene.children[ i ];

1723
				if ( child.visible || options.onlyVisible === false ) {
F
Fernando Serrano 已提交
1724

1725 1726
					var node = processNode( child );

1727
					if ( node !== null ) {
1728

1729
						nodes.push( node );
1730 1731

					}
1732 1733 1734

				}

1735
			}
1736

1737
			if ( nodes.length > 0 ) {
F
Fernando Serrano 已提交
1738

1739
				gltfScene.nodes = nodes;
F
Fernando Serrano 已提交
1740

F
Fernando Serrano 已提交
1741
			}
F
Fernando Serrano 已提交
1742

F
Fernando Serrano 已提交
1743 1744
		}

1745 1746 1747 1748
		/**
		 * Creates a THREE.Scene to hold a list of objects and parse it
		 * @param  {Array} objects List of objects to process
		 */
M
Mr.doob 已提交
1749
		function processObjects( objects ) {
1750 1751

			var scene = new THREE.Scene();
1752
			scene.name = 'AuxScene';
1753

M
Mugen87 已提交
1754
			for ( var i = 0; i < objects.length; i ++ ) {
1755

1756 1757 1758
				// We push directly to children instead of calling `add` to prevent
				// modify the .parent and break its original scene and hierarchy
				scene.children.push( objects[ i ] );
1759 1760 1761 1762 1763 1764 1765

			}

			processScene( scene );

		}

1766
		function processInput( input ) {
1767

1768
			input = input instanceof Array ? input : [ input ];
F
Fernando Serrano 已提交
1769

1770
			var objectsWithoutScene = [];
M
Mr.doob 已提交
1771

M
Mugen87 已提交
1772
			for ( var i = 0; i < input.length; i ++ ) {
F
Fernando Serrano 已提交
1773

1774
				if ( input[ i ] instanceof THREE.Scene ) {
1775 1776 1777

					processScene( input[ i ] );

1778
				} else {
1779

1780
					objectsWithoutScene.push( input[ i ] );
1781

1782
				}
F
Fernando Serrano 已提交
1783

F
Fernando Serrano 已提交
1784
			}
F
Fernando Serrano 已提交
1785

1786
			if ( objectsWithoutScene.length > 0 ) {
1787

1788
				processObjects( objectsWithoutScene );
1789 1790

			}
F
Fernando Serrano 已提交
1791

D
Don McCurdy 已提交
1792 1793 1794 1795 1796 1797
			for ( var i = 0; i < skins.length; ++ i ) {

				processSkin( skins[ i ] );

			}

1798 1799 1800 1801 1802 1803
			for ( var i = 0; i < options.animations.length; ++ i ) {

				processAnimation( options.animations[ i ], input[ 0 ] );

			}

F
Fernando Serrano 已提交
1804
		}
F
Fernando Serrano 已提交
1805

D
Don McCurdy 已提交
1806
		processInput( input );
F
Fernando Serrano 已提交
1807

1808
		Promise.all( pending ).then( function () {
F
Fernando Serrano 已提交
1809

1810 1811
			// Merge buffers.
			var blob = new Blob( buffers, { type: 'application/octet-stream' } );
1812

1813 1814 1815 1816
			// Declare extensions.
			var extensionsUsedList = Object.keys( extensionsUsed );
			if ( extensionsUsedList.length > 0 ) outputJSON.extensionsUsed = extensionsUsedList;

1817
			if ( outputJSON.buffers && outputJSON.buffers.length > 0 ) {
1818

1819 1820
				// Update bytelength of the single buffer.
				outputJSON.buffers[ 0 ].byteLength = blob.size;
F
Fernando Serrano 已提交
1821

1822
				var reader = new window.FileReader();
1823

1824
				if ( options.binary === true ) {
1825

1826
					// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
1827

1828 1829 1830
					var GLB_HEADER_BYTES = 12;
					var GLB_HEADER_MAGIC = 0x46546C67;
					var GLB_VERSION = 2;
1831

1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845
					var GLB_CHUNK_PREFIX_BYTES = 8;
					var GLB_CHUNK_TYPE_JSON = 0x4E4F534A;
					var GLB_CHUNK_TYPE_BIN = 0x004E4942;

					reader.readAsArrayBuffer( blob );
					reader.onloadend = function () {

						// Binary chunk.
						var binaryChunk = getPaddedArrayBuffer( reader.result );
						var binaryChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
						binaryChunkPrefix.setUint32( 0, binaryChunk.byteLength, true );
						binaryChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_BIN, true );

						// JSON chunk.
1846
						var jsonChunk = getPaddedArrayBuffer( stringToArrayBuffer( JSON.stringify( outputJSON ) ), 0x20 );
1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875
						var jsonChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
						jsonChunkPrefix.setUint32( 0, jsonChunk.byteLength, true );
						jsonChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_JSON, true );

						// GLB header.
						var header = new ArrayBuffer( GLB_HEADER_BYTES );
						var headerView = new DataView( header );
						headerView.setUint32( 0, GLB_HEADER_MAGIC, true );
						headerView.setUint32( 4, GLB_VERSION, true );
						var totalByteLength = GLB_HEADER_BYTES
							+ jsonChunkPrefix.byteLength + jsonChunk.byteLength
							+ binaryChunkPrefix.byteLength + binaryChunk.byteLength;
						headerView.setUint32( 8, totalByteLength, true );

						var glbBlob = new Blob( [
							header,
							jsonChunkPrefix,
							jsonChunk,
							binaryChunkPrefix,
							binaryChunk
						], { type: 'application/octet-stream' } );

						var glbReader = new window.FileReader();
						glbReader.readAsArrayBuffer( glbBlob );
						glbReader.onloadend = function () {

							onDone( glbReader.result );

						};
F
Fernando Serrano 已提交
1876

1877 1878
					};

1879
				} else {
1880

1881 1882
					reader.readAsDataURL( blob );
					reader.onloadend = function () {
1883

1884 1885 1886
						var base64data = reader.result;
						outputJSON.buffers[ 0 ].uri = base64data;
						onDone( outputJSON );
1887

1888
					};
F
Fernando Serrano 已提交
1889

1890
				}
F
Fernando Serrano 已提交
1891

1892
			} else {
F
Fernando Serrano 已提交
1893

1894
				onDone( outputJSON );
1895

1896
			}
1897

1898
		} );
1899

F
Fernando Serrano 已提交
1900
	}
M
Mr.doob 已提交
1901

D
Don McCurdy 已提交
1902
};