UNPKG

64.1 kBJavaScriptView Raw
1( function () {
2
3 class GLTFExporter {
4
5 constructor() {
6
7 this.pluginCallbacks = [];
8 this.register( function ( writer ) {
9
10 return new GLTFLightExtension( writer );
11
12 } );
13 this.register( function ( writer ) {
14
15 return new GLTFMaterialsUnlitExtension( writer );
16
17 } );
18 this.register( function ( writer ) {
19
20 return new GLTFMaterialsTransmissionExtension( writer );
21
22 } );
23 this.register( function ( writer ) {
24
25 return new GLTFMaterialsVolumeExtension( writer );
26
27 } );
28 this.register( function ( writer ) {
29
30 return new GLTFMaterialsClearcoatExtension( writer );
31
32 } );
33 this.register( function ( writer ) {
34
35 return new GLTFMaterialsIridescenceExtension( writer );
36
37 } );
38
39 }
40 register( callback ) {
41
42 if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) {
43
44 this.pluginCallbacks.push( callback );
45
46 }
47
48 return this;
49
50 }
51 unregister( callback ) {
52
53 if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) {
54
55 this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 );
56
57 }
58
59 return this;
60
61 }
62
63 /**
64 * Parse scenes and generate GLTF output
65 * @param {Scene or [THREE.Scenes]} input THREE.Scene or Array of THREE.Scenes
66 * @param {Function} onDone Callback on completed
67 * @param {Function} onError Callback on errors
68 * @param {Object} options options
69 */
70 parse( input, onDone, onError, options ) {
71
72 const writer = new GLTFWriter();
73 const plugins = [];
74 for ( let i = 0, il = this.pluginCallbacks.length; i < il; i ++ ) {
75
76 plugins.push( this.pluginCallbacks[ i ]( writer ) );
77
78 }
79
80 writer.setPlugins( plugins );
81 writer.write( input, onDone, options ).catch( onError );
82
83 }
84 parseAsync( input, options ) {
85
86 const scope = this;
87 return new Promise( function ( resolve, reject ) {
88
89 scope.parse( input, resolve, reject, options );
90
91 } );
92
93 }
94
95 }
96
97 //------------------------------------------------------------------------------
98 // Constants
99 //------------------------------------------------------------------------------
100
101 const WEBGL_CONSTANTS = {
102 POINTS: 0x0000,
103 LINES: 0x0001,
104 LINE_LOOP: 0x0002,
105 LINE_STRIP: 0x0003,
106 TRIANGLES: 0x0004,
107 TRIANGLE_STRIP: 0x0005,
108 TRIANGLE_FAN: 0x0006,
109 UNSIGNED_BYTE: 0x1401,
110 UNSIGNED_SHORT: 0x1403,
111 FLOAT: 0x1406,
112 UNSIGNED_INT: 0x1405,
113 ARRAY_BUFFER: 0x8892,
114 ELEMENT_ARRAY_BUFFER: 0x8893,
115 NEAREST: 0x2600,
116 LINEAR: 0x2601,
117 NEAREST_MIPMAP_NEAREST: 0x2700,
118 LINEAR_MIPMAP_NEAREST: 0x2701,
119 NEAREST_MIPMAP_LINEAR: 0x2702,
120 LINEAR_MIPMAP_LINEAR: 0x2703,
121 CLAMP_TO_EDGE: 33071,
122 MIRRORED_REPEAT: 33648,
123 REPEAT: 10497
124 };
125 const THREE_TO_WEBGL = {};
126 THREE_TO_WEBGL[ THREE.NearestFilter ] = WEBGL_CONSTANTS.NEAREST;
127 THREE_TO_WEBGL[ THREE.NearestMipmapNearestFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST;
128 THREE_TO_WEBGL[ THREE.NearestMipmapLinearFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR;
129 THREE_TO_WEBGL[ THREE.LinearFilter ] = WEBGL_CONSTANTS.LINEAR;
130 THREE_TO_WEBGL[ THREE.LinearMipmapNearestFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST;
131 THREE_TO_WEBGL[ THREE.LinearMipmapLinearFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR;
132 THREE_TO_WEBGL[ THREE.ClampToEdgeWrapping ] = WEBGL_CONSTANTS.CLAMP_TO_EDGE;
133 THREE_TO_WEBGL[ THREE.RepeatWrapping ] = WEBGL_CONSTANTS.REPEAT;
134 THREE_TO_WEBGL[ THREE.MirroredRepeatWrapping ] = WEBGL_CONSTANTS.MIRRORED_REPEAT;
135 const PATH_PROPERTIES = {
136 scale: 'scale',
137 position: 'translation',
138 quaternion: 'rotation',
139 morphTargetInfluences: 'weights'
140 };
141
142 // GLB constants
143 // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
144
145 const GLB_HEADER_BYTES = 12;
146 const GLB_HEADER_MAGIC = 0x46546C67;
147 const GLB_VERSION = 2;
148 const GLB_CHUNK_PREFIX_BYTES = 8;
149 const GLB_CHUNK_TYPE_JSON = 0x4E4F534A;
150 const GLB_CHUNK_TYPE_BIN = 0x004E4942;
151
152 //------------------------------------------------------------------------------
153 // Utility functions
154 //------------------------------------------------------------------------------
155
156 /**
157 * Compare two arrays
158 * @param {Array} array1 Array 1 to compare
159 * @param {Array} array2 Array 2 to compare
160 * @return {Boolean} Returns true if both arrays are equal
161 */
162 function equalArray( array1, array2 ) {
163
164 return array1.length === array2.length && array1.every( function ( element, index ) {
165
166 return element === array2[ index ];
167
168 } );
169
170 }
171
172 /**
173 * Converts a string to an ArrayBuffer.
174 * @param {string} text
175 * @return {ArrayBuffer}
176 */
177 function stringToArrayBuffer( text ) {
178
179 return new TextEncoder().encode( text ).buffer;
180
181 }
182
183 /**
184 * Is identity matrix
185 *
186 * @param {Matrix4} matrix
187 * @returns {Boolean} Returns true, if parameter is identity matrix
188 */
189 function isIdentityMatrix( matrix ) {
190
191 return equalArray( matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] );
192
193 }
194
195 /**
196 * Get the min and max vectors from the given attribute
197 * @param {BufferAttribute} attribute Attribute to find the min/max in range from start to start + count
198 * @param {Integer} start
199 * @param {Integer} count
200 * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components)
201 */
202 function getMinMax( attribute, start, count ) {
203
204 const output = {
205 min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ),
206 max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY )
207 };
208 for ( let i = start; i < start + count; i ++ ) {
209
210 for ( let a = 0; a < attribute.itemSize; a ++ ) {
211
212 let value;
213 if ( attribute.itemSize > 4 ) {
214
215 // no support for interleaved data for itemSize > 4
216
217 value = attribute.array[ i * attribute.itemSize + a ];
218
219 } else {
220
221 if ( a === 0 ) value = attribute.getX( i ); else if ( a === 1 ) value = attribute.getY( i ); else if ( a === 2 ) value = attribute.getZ( i ); else if ( a === 3 ) value = attribute.getW( i );
222
223 }
224
225 output.min[ a ] = Math.min( output.min[ a ], value );
226 output.max[ a ] = Math.max( output.max[ a ], value );
227
228 }
229
230 }
231
232 return output;
233
234 }
235
236 /**
237 * Get the required size + padding for a buffer, rounded to the next 4-byte boundary.
238 * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment
239 *
240 * @param {Integer} bufferSize The size the original buffer.
241 * @returns {Integer} new buffer size with required padding.
242 *
243 */
244 function getPaddedBufferSize( bufferSize ) {
245
246 return Math.ceil( bufferSize / 4 ) * 4;
247
248 }
249
250 /**
251 * Returns a buffer aligned to 4-byte boundary.
252 *
253 * @param {ArrayBuffer} arrayBuffer Buffer to pad
254 * @param {Integer} paddingByte (Optional)
255 * @returns {ArrayBuffer} The same buffer if it's already aligned to 4-byte boundary or a new buffer
256 */
257 function getPaddedArrayBuffer( arrayBuffer, paddingByte = 0 ) {
258
259 const paddedLength = getPaddedBufferSize( arrayBuffer.byteLength );
260 if ( paddedLength !== arrayBuffer.byteLength ) {
261
262 const array = new Uint8Array( paddedLength );
263 array.set( new Uint8Array( arrayBuffer ) );
264 if ( paddingByte !== 0 ) {
265
266 for ( let i = arrayBuffer.byteLength; i < paddedLength; i ++ ) {
267
268 array[ i ] = paddingByte;
269
270 }
271
272 }
273
274 return array.buffer;
275
276 }
277
278 return arrayBuffer;
279
280 }
281
282 function getCanvas() {
283
284 if ( typeof document === 'undefined' && typeof OffscreenCanvas !== 'undefined' ) {
285
286 return new OffscreenCanvas( 1, 1 );
287
288 }
289
290 return document.createElement( 'canvas' );
291
292 }
293
294 function getToBlobPromise( canvas, mimeType ) {
295
296 if ( canvas.toBlob !== undefined ) {
297
298 return new Promise( resolve => canvas.toBlob( resolve, mimeType ) );
299
300 }
301
302 let quality;
303
304 // Blink's implementation of convertToBlob seems to default to a quality level of 100%
305 // Use the Blink default quality levels of toBlob instead so that file sizes are comparable.
306 if ( mimeType === 'image/jpeg' ) {
307
308 quality = 0.92;
309
310 } else if ( mimeType === 'image/webp' ) {
311
312 quality = 0.8;
313
314 }
315
316 return canvas.convertToBlob( {
317 type: mimeType,
318 quality: quality
319 } );
320
321 }
322
323 /**
324 * Writer
325 */
326 class GLTFWriter {
327
328 constructor() {
329
330 this.plugins = [];
331 this.options = {};
332 this.pending = [];
333 this.buffers = [];
334 this.byteOffset = 0;
335 this.buffers = [];
336 this.nodeMap = new Map();
337 this.skins = [];
338 this.extensionsUsed = {};
339 this.uids = new Map();
340 this.uid = 0;
341 this.json = {
342 asset: {
343 version: '2.0',
344 generator: 'THREE.GLTFExporter'
345 }
346 };
347 this.cache = {
348 meshes: new Map(),
349 attributes: new Map(),
350 attributesNormalized: new Map(),
351 materials: new Map(),
352 textures: new Map(),
353 images: new Map()
354 };
355
356 }
357 setPlugins( plugins ) {
358
359 this.plugins = plugins;
360
361 }
362
363 /**
364 * Parse scenes and generate GLTF output
365 * @param {Scene or [THREE.Scenes]} input THREE.Scene or Array of THREE.Scenes
366 * @param {Function} onDone Callback on completed
367 * @param {Object} options options
368 */
369 async write( input, onDone, options ) {
370
371 this.options = Object.assign( {}, {
372 // default options
373 binary: false,
374 trs: false,
375 onlyVisible: true,
376 maxTextureSize: Infinity,
377 animations: [],
378 includeCustomExtensions: false
379 }, options );
380 if ( this.options.animations.length > 0 ) {
381
382 // Only TRS properties, and not matrices, may be targeted by animation.
383 this.options.trs = true;
384
385 }
386
387 this.processInput( input );
388 await Promise.all( this.pending );
389 const writer = this;
390 const buffers = writer.buffers;
391 const json = writer.json;
392 options = writer.options;
393 const extensionsUsed = writer.extensionsUsed;
394
395 // Merge buffers.
396 const blob = new Blob( buffers, {
397 type: 'application/octet-stream'
398 } );
399
400 // Declare extensions.
401 const extensionsUsedList = Object.keys( extensionsUsed );
402 if ( extensionsUsedList.length > 0 ) json.extensionsUsed = extensionsUsedList;
403
404 // Update bytelength of the single buffer.
405 if ( json.buffers && json.buffers.length > 0 ) json.buffers[ 0 ].byteLength = blob.size;
406 if ( options.binary === true ) {
407
408 // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification
409
410 const reader = new FileReader();
411 reader.readAsArrayBuffer( blob );
412 reader.onloadend = function () {
413
414 // Binary chunk.
415 const binaryChunk = getPaddedArrayBuffer( reader.result );
416 const binaryChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
417 binaryChunkPrefix.setUint32( 0, binaryChunk.byteLength, true );
418 binaryChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_BIN, true );
419
420 // JSON chunk.
421 const jsonChunk = getPaddedArrayBuffer( stringToArrayBuffer( JSON.stringify( json ) ), 0x20 );
422 const jsonChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) );
423 jsonChunkPrefix.setUint32( 0, jsonChunk.byteLength, true );
424 jsonChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_JSON, true );
425
426 // GLB header.
427 const header = new ArrayBuffer( GLB_HEADER_BYTES );
428 const headerView = new DataView( header );
429 headerView.setUint32( 0, GLB_HEADER_MAGIC, true );
430 headerView.setUint32( 4, GLB_VERSION, true );
431 const totalByteLength = GLB_HEADER_BYTES + jsonChunkPrefix.byteLength + jsonChunk.byteLength + binaryChunkPrefix.byteLength + binaryChunk.byteLength;
432 headerView.setUint32( 8, totalByteLength, true );
433 const glbBlob = new Blob( [ header, jsonChunkPrefix, jsonChunk, binaryChunkPrefix, binaryChunk ], {
434 type: 'application/octet-stream'
435 } );
436 const glbReader = new FileReader();
437 glbReader.readAsArrayBuffer( glbBlob );
438 glbReader.onloadend = function () {
439
440 onDone( glbReader.result );
441
442 };
443
444 };
445
446 } else {
447
448 if ( json.buffers && json.buffers.length > 0 ) {
449
450 const reader = new FileReader();
451 reader.readAsDataURL( blob );
452 reader.onloadend = function () {
453
454 const base64data = reader.result;
455 json.buffers[ 0 ].uri = base64data;
456 onDone( json );
457
458 };
459
460 } else {
461
462 onDone( json );
463
464 }
465
466 }
467
468 }
469
470 /**
471 * Serializes a userData.
472 *
473 * @param {THREE.Object3D|THREE.Material} object
474 * @param {Object} objectDef
475 */
476 serializeUserData( object, objectDef ) {
477
478 if ( Object.keys( object.userData ).length === 0 ) return;
479 const options = this.options;
480 const extensionsUsed = this.extensionsUsed;
481 try {
482
483 const json = JSON.parse( JSON.stringify( object.userData ) );
484 if ( options.includeCustomExtensions && json.gltfExtensions ) {
485
486 if ( objectDef.extensions === undefined ) objectDef.extensions = {};
487 for ( const extensionName in json.gltfExtensions ) {
488
489 objectDef.extensions[ extensionName ] = json.gltfExtensions[ extensionName ];
490 extensionsUsed[ extensionName ] = true;
491
492 }
493
494 delete json.gltfExtensions;
495
496 }
497
498 if ( Object.keys( json ).length > 0 ) objectDef.extras = json;
499
500 } catch ( error ) {
501
502 console.warn( 'THREE.GLTFExporter: userData of \'' + object.name + '\' ' + 'won\'t be serialized because of JSON.stringify error - ' + error.message );
503
504 }
505
506 }
507
508 /**
509 * Returns ids for buffer attributes.
510 * @param {Object} object
511 * @return {Integer}
512 */
513 getUID( attribute, isRelativeCopy = false ) {
514
515 if ( this.uids.has( attribute ) === false ) {
516
517 const uids = new Map();
518 uids.set( true, this.uid ++ );
519 uids.set( false, this.uid ++ );
520 this.uids.set( attribute, uids );
521
522 }
523
524 const uids = this.uids.get( attribute );
525 return uids.get( isRelativeCopy );
526
527 }
528
529 /**
530 * Checks if normal attribute values are normalized.
531 *
532 * @param {BufferAttribute} normal
533 * @returns {Boolean}
534 */
535 isNormalizedNormalAttribute( normal ) {
536
537 const cache = this.cache;
538 if ( cache.attributesNormalized.has( normal ) ) return false;
539 const v = new THREE.Vector3();
540 for ( let i = 0, il = normal.count; i < il; i ++ ) {
541
542 // 0.0005 is from glTF-validator
543 if ( Math.abs( v.fromBufferAttribute( normal, i ).length() - 1.0 ) > 0.0005 ) return false;
544
545 }
546
547 return true;
548
549 }
550
551 /**
552 * Creates normalized normal buffer attribute.
553 *
554 * @param {BufferAttribute} normal
555 * @returns {BufferAttribute}
556 *
557 */
558 createNormalizedNormalAttribute( normal ) {
559
560 const cache = this.cache;
561 if ( cache.attributesNormalized.has( normal ) ) return cache.attributesNormalized.get( normal );
562 const attribute = normal.clone();
563 const v = new THREE.Vector3();
564 for ( let i = 0, il = attribute.count; i < il; i ++ ) {
565
566 v.fromBufferAttribute( attribute, i );
567 if ( v.x === 0 && v.y === 0 && v.z === 0 ) {
568
569 // if values can't be normalized set (1, 0, 0)
570 v.setX( 1.0 );
571
572 } else {
573
574 v.normalize();
575
576 }
577
578 attribute.setXYZ( i, v.x, v.y, v.z );
579
580 }
581
582 cache.attributesNormalized.set( normal, attribute );
583 return attribute;
584
585 }
586
587 /**
588 * Applies a texture transform, if present, to the map definition. Requires
589 * the KHR_texture_transform extension.
590 *
591 * @param {Object} mapDef
592 * @param {THREE.Texture} texture
593 */
594 applyTextureTransform( mapDef, texture ) {
595
596 let didTransform = false;
597 const transformDef = {};
598 if ( texture.offset.x !== 0 || texture.offset.y !== 0 ) {
599
600 transformDef.offset = texture.offset.toArray();
601 didTransform = true;
602
603 }
604
605 if ( texture.rotation !== 0 ) {
606
607 transformDef.rotation = texture.rotation;
608 didTransform = true;
609
610 }
611
612 if ( texture.repeat.x !== 1 || texture.repeat.y !== 1 ) {
613
614 transformDef.scale = texture.repeat.toArray();
615 didTransform = true;
616
617 }
618
619 if ( didTransform ) {
620
621 mapDef.extensions = mapDef.extensions || {};
622 mapDef.extensions[ 'KHR_texture_transform' ] = transformDef;
623 this.extensionsUsed[ 'KHR_texture_transform' ] = true;
624
625 }
626
627 }
628 buildMetalRoughTexture( metalnessMap, roughnessMap ) {
629
630 if ( metalnessMap === roughnessMap ) return metalnessMap;
631 function getEncodingConversion( map ) {
632
633 if ( map.encoding === THREE.sRGBEncoding ) {
634
635 return function SRGBToLinear( c ) {
636
637 return c < 0.04045 ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 );
638
639 };
640
641 }
642
643 return function LinearToLinear( c ) {
644
645 return c;
646
647 };
648
649 }
650
651 console.warn( 'THREE.GLTFExporter: Merged metalnessMap and roughnessMap textures.' );
652 const metalness = metalnessMap?.image;
653 const roughness = roughnessMap?.image;
654 const width = Math.max( metalness?.width || 0, roughness?.width || 0 );
655 const height = Math.max( metalness?.height || 0, roughness?.height || 0 );
656 const canvas = getCanvas();
657 canvas.width = width;
658 canvas.height = height;
659 const context = canvas.getContext( '2d' );
660 context.fillStyle = '#00ffff';
661 context.fillRect( 0, 0, width, height );
662 const composite = context.getImageData( 0, 0, width, height );
663 if ( metalness ) {
664
665 context.drawImage( metalness, 0, 0, width, height );
666 const convert = getEncodingConversion( metalnessMap );
667 const data = context.getImageData( 0, 0, width, height ).data;
668 for ( let i = 2; i < data.length; i += 4 ) {
669
670 composite.data[ i ] = convert( data[ i ] / 256 ) * 256;
671
672 }
673
674 }
675
676 if ( roughness ) {
677
678 context.drawImage( roughness, 0, 0, width, height );
679 const convert = getEncodingConversion( roughnessMap );
680 const data = context.getImageData( 0, 0, width, height ).data;
681 for ( let i = 1; i < data.length; i += 4 ) {
682
683 composite.data[ i ] = convert( data[ i ] / 256 ) * 256;
684
685 }
686
687 }
688
689 context.putImageData( composite, 0, 0 );
690
691 //
692
693 const reference = metalnessMap || roughnessMap;
694 const texture = reference.clone();
695 texture.source = new THREE.Source( canvas );
696 texture.encoding = THREE.LinearEncoding;
697 return texture;
698
699 }
700
701 /**
702 * Process a buffer to append to the default one.
703 * @param {ArrayBuffer} buffer
704 * @return {Integer}
705 */
706 processBuffer( buffer ) {
707
708 const json = this.json;
709 const buffers = this.buffers;
710 if ( ! json.buffers ) json.buffers = [ {
711 byteLength: 0
712 } ];
713
714 // All buffers are merged before export.
715 buffers.push( buffer );
716 return 0;
717
718 }
719
720 /**
721 * Process and generate a BufferView
722 * @param {BufferAttribute} attribute
723 * @param {number} componentType
724 * @param {number} start
725 * @param {number} count
726 * @param {number} target (Optional) Target usage of the BufferView
727 * @return {Object}
728 */
729 processBufferView( attribute, componentType, start, count, target ) {
730
731 const json = this.json;
732 if ( ! json.bufferViews ) json.bufferViews = [];
733
734 // Create a new dataview and dump the attribute's array into it
735
736 let componentSize;
737 if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {
738
739 componentSize = 1;
740
741 } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {
742
743 componentSize = 2;
744
745 } else {
746
747 componentSize = 4;
748
749 }
750
751 const byteLength = getPaddedBufferSize( count * attribute.itemSize * componentSize );
752 const dataView = new DataView( new ArrayBuffer( byteLength ) );
753 let offset = 0;
754 for ( let i = start; i < start + count; i ++ ) {
755
756 for ( let a = 0; a < attribute.itemSize; a ++ ) {
757
758 let value;
759 if ( attribute.itemSize > 4 ) {
760
761 // no support for interleaved data for itemSize > 4
762
763 value = attribute.array[ i * attribute.itemSize + a ];
764
765 } else {
766
767 if ( a === 0 ) value = attribute.getX( i ); else if ( a === 1 ) value = attribute.getY( i ); else if ( a === 2 ) value = attribute.getZ( i ); else if ( a === 3 ) value = attribute.getW( i );
768
769 }
770
771 if ( componentType === WEBGL_CONSTANTS.FLOAT ) {
772
773 dataView.setFloat32( offset, value, true );
774
775 } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_INT ) {
776
777 dataView.setUint32( offset, value, true );
778
779 } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) {
780
781 dataView.setUint16( offset, value, true );
782
783 } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) {
784
785 dataView.setUint8( offset, value );
786
787 }
788
789 offset += componentSize;
790
791 }
792
793 }
794
795 const bufferViewDef = {
796 buffer: this.processBuffer( dataView.buffer ),
797 byteOffset: this.byteOffset,
798 byteLength: byteLength
799 };
800 if ( target !== undefined ) bufferViewDef.target = target;
801 if ( target === WEBGL_CONSTANTS.ARRAY_BUFFER ) {
802
803 // Only define byteStride for vertex attributes.
804 bufferViewDef.byteStride = attribute.itemSize * componentSize;
805
806 }
807
808 this.byteOffset += byteLength;
809 json.bufferViews.push( bufferViewDef );
810
811 // @TODO Merge bufferViews where possible.
812 const output = {
813 id: json.bufferViews.length - 1,
814 byteLength: 0
815 };
816 return output;
817
818 }
819
820 /**
821 * Process and generate a BufferView from an image Blob.
822 * @param {Blob} blob
823 * @return {Promise<Integer>}
824 */
825 processBufferViewImage( blob ) {
826
827 const writer = this;
828 const json = writer.json;
829 if ( ! json.bufferViews ) json.bufferViews = [];
830 return new Promise( function ( resolve ) {
831
832 const reader = new FileReader();
833 reader.readAsArrayBuffer( blob );
834 reader.onloadend = function () {
835
836 const buffer = getPaddedArrayBuffer( reader.result );
837 const bufferViewDef = {
838 buffer: writer.processBuffer( buffer ),
839 byteOffset: writer.byteOffset,
840 byteLength: buffer.byteLength
841 };
842 writer.byteOffset += buffer.byteLength;
843 resolve( json.bufferViews.push( bufferViewDef ) - 1 );
844
845 };
846
847 } );
848
849 }
850
851 /**
852 * Process attribute to generate an accessor
853 * @param {BufferAttribute} attribute Attribute to process
854 * @param {THREE.BufferGeometry} geometry (Optional) Geometry used for truncated draw range
855 * @param {Integer} start (Optional)
856 * @param {Integer} count (Optional)
857 * @return {Integer|null} Index of the processed accessor on the "accessors" array
858 */
859 processAccessor( attribute, geometry, start, count ) {
860
861 const json = this.json;
862 const types = {
863 1: 'SCALAR',
864 2: 'VEC2',
865 3: 'VEC3',
866 4: 'VEC4',
867 16: 'MAT4'
868 };
869 let componentType;
870
871 // Detect the component type of the attribute array (float, uint or ushort)
872 if ( attribute.array.constructor === Float32Array ) {
873
874 componentType = WEBGL_CONSTANTS.FLOAT;
875
876 } else if ( attribute.array.constructor === Uint32Array ) {
877
878 componentType = WEBGL_CONSTANTS.UNSIGNED_INT;
879
880 } else if ( attribute.array.constructor === Uint16Array ) {
881
882 componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT;
883
884 } else if ( attribute.array.constructor === Uint8Array ) {
885
886 componentType = WEBGL_CONSTANTS.UNSIGNED_BYTE;
887
888 } else {
889
890 throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type.' );
891
892 }
893
894 if ( start === undefined ) start = 0;
895 if ( count === undefined ) count = attribute.count;
896
897 // Skip creating an accessor if the attribute doesn't have data to export
898 if ( count === 0 ) return null;
899 const minMax = getMinMax( attribute, start, count );
900 let bufferViewTarget;
901
902 // If geometry isn't provided, don't infer the target usage of the bufferView. For
903 // animation samplers, target must not be set.
904 if ( geometry !== undefined ) {
905
906 bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER;
907
908 }
909
910 const bufferView = this.processBufferView( attribute, componentType, start, count, bufferViewTarget );
911 const accessorDef = {
912 bufferView: bufferView.id,
913 byteOffset: bufferView.byteOffset,
914 componentType: componentType,
915 count: count,
916 max: minMax.max,
917 min: minMax.min,
918 type: types[ attribute.itemSize ]
919 };
920 if ( attribute.normalized === true ) accessorDef.normalized = true;
921 if ( ! json.accessors ) json.accessors = [];
922 return json.accessors.push( accessorDef ) - 1;
923
924 }
925
926 /**
927 * Process image
928 * @param {Image} image to process
929 * @param {Integer} format of the image (THREE.RGBAFormat)
930 * @param {Boolean} flipY before writing out the image
931 * @param {String} mimeType export format
932 * @return {Integer} Index of the processed texture in the "images" array
933 */
934 processImage( image, format, flipY, mimeType = 'image/png' ) {
935
936 const writer = this;
937 const cache = writer.cache;
938 const json = writer.json;
939 const options = writer.options;
940 const pending = writer.pending;
941 if ( ! cache.images.has( image ) ) cache.images.set( image, {} );
942 const cachedImages = cache.images.get( image );
943 const key = mimeType + ':flipY/' + flipY.toString();
944 if ( cachedImages[ key ] !== undefined ) return cachedImages[ key ];
945 if ( ! json.images ) json.images = [];
946 const imageDef = {
947 mimeType: mimeType
948 };
949 const canvas = getCanvas();
950 canvas.width = Math.min( image.width, options.maxTextureSize );
951 canvas.height = Math.min( image.height, options.maxTextureSize );
952 const ctx = canvas.getContext( '2d' );
953 if ( flipY === true ) {
954
955 ctx.translate( 0, canvas.height );
956 ctx.scale( 1, - 1 );
957
958 }
959
960 if ( image.data !== undefined ) {
961
962 // THREE.DataTexture
963
964 if ( format !== THREE.RGBAFormat ) {
965
966 console.error( 'GLTFExporter: Only THREE.RGBAFormat is supported.' );
967
968 }
969
970 if ( image.width > options.maxTextureSize || image.height > options.maxTextureSize ) {
971
972 console.warn( 'GLTFExporter: Image size is bigger than maxTextureSize', image );
973
974 }
975
976 const data = new Uint8ClampedArray( image.height * image.width * 4 );
977 for ( let i = 0; i < data.length; i += 4 ) {
978
979 data[ i + 0 ] = image.data[ i + 0 ];
980 data[ i + 1 ] = image.data[ i + 1 ];
981 data[ i + 2 ] = image.data[ i + 2 ];
982 data[ i + 3 ] = image.data[ i + 3 ];
983
984 }
985
986 ctx.putImageData( new ImageData( data, image.width, image.height ), 0, 0 );
987
988 } else {
989
990 ctx.drawImage( image, 0, 0, canvas.width, canvas.height );
991
992 }
993
994 if ( options.binary === true ) {
995
996 pending.push( getToBlobPromise( canvas, mimeType ).then( blob => writer.processBufferViewImage( blob ) ).then( bufferViewIndex => {
997
998 imageDef.bufferView = bufferViewIndex;
999
1000 } ) );
1001
1002 } else {
1003
1004 if ( canvas.toDataURL !== undefined ) {
1005
1006 imageDef.uri = canvas.toDataURL( mimeType );
1007
1008 } else {
1009
1010 pending.push( getToBlobPromise( canvas, mimeType ).then( blob => new FileReader().readAsDataURL( blob ) ).then( dataURL => {
1011
1012 imageDef.uri = dataURL;
1013
1014 } ) );
1015
1016 }
1017
1018 }
1019
1020 const index = json.images.push( imageDef ) - 1;
1021 cachedImages[ key ] = index;
1022 return index;
1023
1024 }
1025
1026 /**
1027 * Process sampler
1028 * @param {Texture} map Texture to process
1029 * @return {Integer} Index of the processed texture in the "samplers" array
1030 */
1031 processSampler( map ) {
1032
1033 const json = this.json;
1034 if ( ! json.samplers ) json.samplers = [];
1035 const samplerDef = {
1036 magFilter: THREE_TO_WEBGL[ map.magFilter ],
1037 minFilter: THREE_TO_WEBGL[ map.minFilter ],
1038 wrapS: THREE_TO_WEBGL[ map.wrapS ],
1039 wrapT: THREE_TO_WEBGL[ map.wrapT ]
1040 };
1041 return json.samplers.push( samplerDef ) - 1;
1042
1043 }
1044
1045 /**
1046 * Process texture
1047 * @param {Texture} map Map to process
1048 * @return {Integer} Index of the processed texture in the "textures" array
1049 */
1050 processTexture( map ) {
1051
1052 const cache = this.cache;
1053 const json = this.json;
1054 if ( cache.textures.has( map ) ) return cache.textures.get( map );
1055 if ( ! json.textures ) json.textures = [];
1056 let mimeType = map.userData.mimeType;
1057 if ( mimeType === 'image/webp' ) mimeType = 'image/png';
1058 const textureDef = {
1059 sampler: this.processSampler( map ),
1060 source: this.processImage( map.image, map.format, map.flipY, mimeType )
1061 };
1062 if ( map.name ) textureDef.name = map.name;
1063 this._invokeAll( function ( ext ) {
1064
1065 ext.writeTexture && ext.writeTexture( map, textureDef );
1066
1067 } );
1068 const index = json.textures.push( textureDef ) - 1;
1069 cache.textures.set( map, index );
1070 return index;
1071
1072 }
1073
1074 /**
1075 * Process material
1076 * @param {THREE.Material} material Material to process
1077 * @return {Integer|null} Index of the processed material in the "materials" array
1078 */
1079 processMaterial( material ) {
1080
1081 const cache = this.cache;
1082 const json = this.json;
1083 if ( cache.materials.has( material ) ) return cache.materials.get( material );
1084 if ( material.isShaderMaterial ) {
1085
1086 console.warn( 'GLTFExporter: THREE.ShaderMaterial not supported.' );
1087 return null;
1088
1089 }
1090
1091 if ( ! json.materials ) json.materials = [];
1092
1093 // @QUESTION Should we avoid including any attribute that has the default value?
1094 const materialDef = {
1095 pbrMetallicRoughness: {}
1096 };
1097 if ( material.isMeshStandardMaterial !== true && material.isMeshBasicMaterial !== true ) {
1098
1099 console.warn( 'GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results.' );
1100
1101 }
1102
1103 // pbrMetallicRoughness.baseColorFactor
1104 const color = material.color.toArray().concat( [ material.opacity ] );
1105 if ( ! equalArray( color, [ 1, 1, 1, 1 ] ) ) {
1106
1107 materialDef.pbrMetallicRoughness.baseColorFactor = color;
1108
1109 }
1110
1111 if ( material.isMeshStandardMaterial ) {
1112
1113 materialDef.pbrMetallicRoughness.metallicFactor = material.metalness;
1114 materialDef.pbrMetallicRoughness.roughnessFactor = material.roughness;
1115
1116 } else {
1117
1118 materialDef.pbrMetallicRoughness.metallicFactor = 0.5;
1119 materialDef.pbrMetallicRoughness.roughnessFactor = 0.5;
1120
1121 }
1122
1123 // pbrMetallicRoughness.metallicRoughnessTexture
1124 if ( material.metalnessMap || material.roughnessMap ) {
1125
1126 const metalRoughTexture = this.buildMetalRoughTexture( material.metalnessMap, material.roughnessMap );
1127 const metalRoughMapDef = {
1128 index: this.processTexture( metalRoughTexture )
1129 };
1130 this.applyTextureTransform( metalRoughMapDef, metalRoughTexture );
1131 materialDef.pbrMetallicRoughness.metallicRoughnessTexture = metalRoughMapDef;
1132
1133 }
1134
1135 // pbrMetallicRoughness.baseColorTexture or pbrSpecularGlossiness diffuseTexture
1136 if ( material.map ) {
1137
1138 const baseColorMapDef = {
1139 index: this.processTexture( material.map )
1140 };
1141 this.applyTextureTransform( baseColorMapDef, material.map );
1142 materialDef.pbrMetallicRoughness.baseColorTexture = baseColorMapDef;
1143
1144 }
1145
1146 if ( material.emissive ) {
1147
1148 // note: emissive components are limited to stay within the 0 - 1 range to accommodate glTF spec. see #21849 and #22000.
1149 const emissive = material.emissive.clone().multiplyScalar( material.emissiveIntensity );
1150 const maxEmissiveComponent = Math.max( emissive.r, emissive.g, emissive.b );
1151 if ( maxEmissiveComponent > 1 ) {
1152
1153 emissive.multiplyScalar( 1 / maxEmissiveComponent );
1154 console.warn( 'THREE.GLTFExporter: Some emissive components exceed 1; emissive has been limited' );
1155
1156 }
1157
1158 if ( maxEmissiveComponent > 0 ) {
1159
1160 materialDef.emissiveFactor = emissive.toArray();
1161
1162 }
1163
1164 // emissiveTexture
1165 if ( material.emissiveMap ) {
1166
1167 const emissiveMapDef = {
1168 index: this.processTexture( material.emissiveMap )
1169 };
1170 this.applyTextureTransform( emissiveMapDef, material.emissiveMap );
1171 materialDef.emissiveTexture = emissiveMapDef;
1172
1173 }
1174
1175 }
1176
1177 // normalTexture
1178 if ( material.normalMap ) {
1179
1180 const normalMapDef = {
1181 index: this.processTexture( material.normalMap )
1182 };
1183 if ( material.normalScale && material.normalScale.x !== 1 ) {
1184
1185 // glTF normal scale is univariate. Ignore `y`, which may be flipped.
1186 // Context: https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995
1187 normalMapDef.scale = material.normalScale.x;
1188
1189 }
1190
1191 this.applyTextureTransform( normalMapDef, material.normalMap );
1192 materialDef.normalTexture = normalMapDef;
1193
1194 }
1195
1196 // occlusionTexture
1197 if ( material.aoMap ) {
1198
1199 const occlusionMapDef = {
1200 index: this.processTexture( material.aoMap ),
1201 texCoord: 1
1202 };
1203 if ( material.aoMapIntensity !== 1.0 ) {
1204
1205 occlusionMapDef.strength = material.aoMapIntensity;
1206
1207 }
1208
1209 this.applyTextureTransform( occlusionMapDef, material.aoMap );
1210 materialDef.occlusionTexture = occlusionMapDef;
1211
1212 }
1213
1214 // alphaMode
1215 if ( material.transparent ) {
1216
1217 materialDef.alphaMode = 'BLEND';
1218
1219 } else {
1220
1221 if ( material.alphaTest > 0.0 ) {
1222
1223 materialDef.alphaMode = 'MASK';
1224 materialDef.alphaCutoff = material.alphaTest;
1225
1226 }
1227
1228 }
1229
1230 // doubleSided
1231 if ( material.side === THREE.DoubleSide ) materialDef.doubleSided = true;
1232 if ( material.name !== '' ) materialDef.name = material.name;
1233 this.serializeUserData( material, materialDef );
1234 this._invokeAll( function ( ext ) {
1235
1236 ext.writeMaterial && ext.writeMaterial( material, materialDef );
1237
1238 } );
1239 const index = json.materials.push( materialDef ) - 1;
1240 cache.materials.set( material, index );
1241 return index;
1242
1243 }
1244
1245 /**
1246 * Process mesh
1247 * @param {THREE.Mesh} mesh Mesh to process
1248 * @return {Integer|null} Index of the processed mesh in the "meshes" array
1249 */
1250 processMesh( mesh ) {
1251
1252 const cache = this.cache;
1253 const json = this.json;
1254 const meshCacheKeyParts = [ mesh.geometry.uuid ];
1255 if ( Array.isArray( mesh.material ) ) {
1256
1257 for ( let i = 0, l = mesh.material.length; i < l; i ++ ) {
1258
1259 meshCacheKeyParts.push( mesh.material[ i ].uuid );
1260
1261 }
1262
1263 } else {
1264
1265 meshCacheKeyParts.push( mesh.material.uuid );
1266
1267 }
1268
1269 const meshCacheKey = meshCacheKeyParts.join( ':' );
1270 if ( cache.meshes.has( meshCacheKey ) ) return cache.meshes.get( meshCacheKey );
1271 const geometry = mesh.geometry;
1272 let mode;
1273
1274 // Use the correct mode
1275 if ( mesh.isLineSegments ) {
1276
1277 mode = WEBGL_CONSTANTS.LINES;
1278
1279 } else if ( mesh.isLineLoop ) {
1280
1281 mode = WEBGL_CONSTANTS.LINE_LOOP;
1282
1283 } else if ( mesh.isLine ) {
1284
1285 mode = WEBGL_CONSTANTS.LINE_STRIP;
1286
1287 } else if ( mesh.isPoints ) {
1288
1289 mode = WEBGL_CONSTANTS.POINTS;
1290
1291 } else {
1292
1293 mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES;
1294
1295 }
1296
1297 const meshDef = {};
1298 const attributes = {};
1299 const primitives = [];
1300 const targets = [];
1301
1302 // Conversion between attributes names in threejs and gltf spec
1303 const nameConversion = {
1304 uv: 'TEXCOORD_0',
1305 uv2: 'TEXCOORD_1',
1306 color: 'COLOR_0',
1307 skinWeight: 'WEIGHTS_0',
1308 skinIndex: 'JOINTS_0'
1309 };
1310 const originalNormal = geometry.getAttribute( 'normal' );
1311 if ( originalNormal !== undefined && ! this.isNormalizedNormalAttribute( originalNormal ) ) {
1312
1313 console.warn( 'THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.' );
1314 geometry.setAttribute( 'normal', this.createNormalizedNormalAttribute( originalNormal ) );
1315
1316 }
1317
1318 // @QUESTION Detect if .vertexColors = true?
1319 // For every attribute create an accessor
1320 let modifiedAttribute = null;
1321 for ( let attributeName in geometry.attributes ) {
1322
1323 // Ignore morph target attributes, which are exported later.
1324 if ( attributeName.slice( 0, 5 ) === 'morph' ) continue;
1325 const attribute = geometry.attributes[ attributeName ];
1326 attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();
1327
1328 // Prefix all geometry attributes except the ones specifically
1329 // listed in the spec; non-spec attributes are considered custom.
1330 const validVertexAttributes = /^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/;
1331 if ( ! validVertexAttributes.test( attributeName ) ) attributeName = '_' + attributeName;
1332 if ( cache.attributes.has( this.getUID( attribute ) ) ) {
1333
1334 attributes[ attributeName ] = cache.attributes.get( this.getUID( attribute ) );
1335 continue;
1336
1337 }
1338
1339 // JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT.
1340 modifiedAttribute = null;
1341 const array = attribute.array;
1342 if ( attributeName === 'JOINTS_0' && ! ( array instanceof Uint16Array ) && ! ( array instanceof Uint8Array ) ) {
1343
1344 console.warn( 'GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.' );
1345 modifiedAttribute = new THREE.BufferAttribute( new Uint16Array( array ), attribute.itemSize, attribute.normalized );
1346
1347 }
1348
1349 const accessor = this.processAccessor( modifiedAttribute || attribute, geometry );
1350 if ( accessor !== null ) {
1351
1352 attributes[ attributeName ] = accessor;
1353 cache.attributes.set( this.getUID( attribute ), accessor );
1354
1355 }
1356
1357 }
1358
1359 if ( originalNormal !== undefined ) geometry.setAttribute( 'normal', originalNormal );
1360
1361 // Skip if no exportable attributes found
1362 if ( Object.keys( attributes ).length === 0 ) return null;
1363
1364 // Morph targets
1365 if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) {
1366
1367 const weights = [];
1368 const targetNames = [];
1369 const reverseDictionary = {};
1370 if ( mesh.morphTargetDictionary !== undefined ) {
1371
1372 for ( const key in mesh.morphTargetDictionary ) {
1373
1374 reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key;
1375
1376 }
1377
1378 }
1379
1380 for ( let i = 0; i < mesh.morphTargetInfluences.length; ++ i ) {
1381
1382 const target = {};
1383 let warned = false;
1384 for ( const attributeName in geometry.morphAttributes ) {
1385
1386 // glTF 2.0 morph supports only POSITION/NORMAL/TANGENT.
1387 // Three.js doesn't support TANGENT yet.
1388
1389 if ( attributeName !== 'position' && attributeName !== 'normal' ) {
1390
1391 if ( ! warned ) {
1392
1393 console.warn( 'GLTFExporter: Only POSITION and NORMAL morph are supported.' );
1394 warned = true;
1395
1396 }
1397
1398 continue;
1399
1400 }
1401
1402 const attribute = geometry.morphAttributes[ attributeName ][ i ];
1403 const gltfAttributeName = attributeName.toUpperCase();
1404
1405 // Three.js morph attribute has absolute values while the one of glTF has relative values.
1406 //
1407 // glTF 2.0 Specification:
1408 // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets
1409
1410 const baseAttribute = geometry.attributes[ attributeName ];
1411 if ( cache.attributes.has( this.getUID( attribute, true ) ) ) {
1412
1413 target[ gltfAttributeName ] = cache.attributes.get( this.getUID( attribute, true ) );
1414 continue;
1415
1416 }
1417
1418 // Clones attribute not to override
1419 const relativeAttribute = attribute.clone();
1420 if ( ! geometry.morphTargetsRelative ) {
1421
1422 for ( let j = 0, jl = attribute.count; j < jl; j ++ ) {
1423
1424 relativeAttribute.setXYZ( j, attribute.getX( j ) - baseAttribute.getX( j ), attribute.getY( j ) - baseAttribute.getY( j ), attribute.getZ( j ) - baseAttribute.getZ( j ) );
1425
1426 }
1427
1428 }
1429
1430 target[ gltfAttributeName ] = this.processAccessor( relativeAttribute, geometry );
1431 cache.attributes.set( this.getUID( baseAttribute, true ), target[ gltfAttributeName ] );
1432
1433 }
1434
1435 targets.push( target );
1436 weights.push( mesh.morphTargetInfluences[ i ] );
1437 if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] );
1438
1439 }
1440
1441 meshDef.weights = weights;
1442 if ( targetNames.length > 0 ) {
1443
1444 meshDef.extras = {};
1445 meshDef.extras.targetNames = targetNames;
1446
1447 }
1448
1449 }
1450
1451 const isMultiMaterial = Array.isArray( mesh.material );
1452 if ( isMultiMaterial && geometry.groups.length === 0 ) return null;
1453 const materials = isMultiMaterial ? mesh.material : [ mesh.material ];
1454 const groups = isMultiMaterial ? geometry.groups : [ {
1455 materialIndex: 0,
1456 start: undefined,
1457 count: undefined
1458 } ];
1459 for ( let i = 0, il = groups.length; i < il; i ++ ) {
1460
1461 const primitive = {
1462 mode: mode,
1463 attributes: attributes
1464 };
1465 this.serializeUserData( geometry, primitive );
1466 if ( targets.length > 0 ) primitive.targets = targets;
1467 if ( geometry.index !== null ) {
1468
1469 let cacheKey = this.getUID( geometry.index );
1470 if ( groups[ i ].start !== undefined || groups[ i ].count !== undefined ) {
1471
1472 cacheKey += ':' + groups[ i ].start + ':' + groups[ i ].count;
1473
1474 }
1475
1476 if ( cache.attributes.has( cacheKey ) ) {
1477
1478 primitive.indices = cache.attributes.get( cacheKey );
1479
1480 } else {
1481
1482 primitive.indices = this.processAccessor( geometry.index, geometry, groups[ i ].start, groups[ i ].count );
1483 cache.attributes.set( cacheKey, primitive.indices );
1484
1485 }
1486
1487 if ( primitive.indices === null ) delete primitive.indices;
1488
1489 }
1490
1491 const material = this.processMaterial( materials[ groups[ i ].materialIndex ] );
1492 if ( material !== null ) primitive.material = material;
1493 primitives.push( primitive );
1494
1495 }
1496
1497 meshDef.primitives = primitives;
1498 if ( ! json.meshes ) json.meshes = [];
1499 this._invokeAll( function ( ext ) {
1500
1501 ext.writeMesh && ext.writeMesh( mesh, meshDef );
1502
1503 } );
1504 const index = json.meshes.push( meshDef ) - 1;
1505 cache.meshes.set( meshCacheKey, index );
1506 return index;
1507
1508 }
1509
1510 /**
1511 * Process camera
1512 * @param {THREE.Camera} camera Camera to process
1513 * @return {Integer} Index of the processed mesh in the "camera" array
1514 */
1515 processCamera( camera ) {
1516
1517 const json = this.json;
1518 if ( ! json.cameras ) json.cameras = [];
1519 const isOrtho = camera.isOrthographicCamera;
1520 const cameraDef = {
1521 type: isOrtho ? 'orthographic' : 'perspective'
1522 };
1523 if ( isOrtho ) {
1524
1525 cameraDef.orthographic = {
1526 xmag: camera.right * 2,
1527 ymag: camera.top * 2,
1528 zfar: camera.far <= 0 ? 0.001 : camera.far,
1529 znear: camera.near < 0 ? 0 : camera.near
1530 };
1531
1532 } else {
1533
1534 cameraDef.perspective = {
1535 aspectRatio: camera.aspect,
1536 yfov: THREE.MathUtils.degToRad( camera.fov ),
1537 zfar: camera.far <= 0 ? 0.001 : camera.far,
1538 znear: camera.near < 0 ? 0 : camera.near
1539 };
1540
1541 }
1542
1543 // Question: Is saving "type" as name intentional?
1544 if ( camera.name !== '' ) cameraDef.name = camera.type;
1545 return json.cameras.push( cameraDef ) - 1;
1546
1547 }
1548
1549 /**
1550 * Creates glTF animation entry from AnimationClip object.
1551 *
1552 * Status:
1553 * - Only properties listed in PATH_PROPERTIES may be animated.
1554 *
1555 * @param {THREE.AnimationClip} clip
1556 * @param {THREE.Object3D} root
1557 * @return {number|null}
1558 */
1559 processAnimation( clip, root ) {
1560
1561 const json = this.json;
1562 const nodeMap = this.nodeMap;
1563 if ( ! json.animations ) json.animations = [];
1564 clip = GLTFExporter.Utils.mergeMorphTargetTracks( clip.clone(), root );
1565 const tracks = clip.tracks;
1566 const channels = [];
1567 const samplers = [];
1568 for ( let i = 0; i < tracks.length; ++ i ) {
1569
1570 const track = tracks[ i ];
1571 const trackBinding = THREE.PropertyBinding.parseTrackName( track.name );
1572 let trackNode = THREE.PropertyBinding.findNode( root, trackBinding.nodeName );
1573 const trackProperty = PATH_PROPERTIES[ trackBinding.propertyName ];
1574 if ( trackBinding.objectName === 'bones' ) {
1575
1576 if ( trackNode.isSkinnedMesh === true ) {
1577
1578 trackNode = trackNode.skeleton.getBoneByName( trackBinding.objectIndex );
1579
1580 } else {
1581
1582 trackNode = undefined;
1583
1584 }
1585
1586 }
1587
1588 if ( ! trackNode || ! trackProperty ) {
1589
1590 console.warn( 'THREE.GLTFExporter: Could not export animation track "%s".', track.name );
1591 return null;
1592
1593 }
1594
1595 const inputItemSize = 1;
1596 let outputItemSize = track.values.length / track.times.length;
1597 if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) {
1598
1599 outputItemSize /= trackNode.morphTargetInfluences.length;
1600
1601 }
1602
1603 let interpolation;
1604
1605 // @TODO export CubicInterpolant(InterpolateSmooth) as CUBICSPLINE
1606
1607 // Detecting glTF cubic spline interpolant by checking factory method's special property
1608 // GLTFCubicSplineInterpolant is a custom interpolant and track doesn't return
1609 // valid value from .getInterpolation().
1610 if ( track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true ) {
1611
1612 interpolation = 'CUBICSPLINE';
1613
1614 // itemSize of CUBICSPLINE keyframe is 9
1615 // (VEC3 * 3: inTangent, splineVertex, and outTangent)
1616 // but needs to be stored as VEC3 so dividing by 3 here.
1617 outputItemSize /= 3;
1618
1619 } else if ( track.getInterpolation() === THREE.InterpolateDiscrete ) {
1620
1621 interpolation = 'STEP';
1622
1623 } else {
1624
1625 interpolation = 'LINEAR';
1626
1627 }
1628
1629 samplers.push( {
1630 input: this.processAccessor( new THREE.BufferAttribute( track.times, inputItemSize ) ),
1631 output: this.processAccessor( new THREE.BufferAttribute( track.values, outputItemSize ) ),
1632 interpolation: interpolation
1633 } );
1634 channels.push( {
1635 sampler: samplers.length - 1,
1636 target: {
1637 node: nodeMap.get( trackNode ),
1638 path: trackProperty
1639 }
1640 } );
1641
1642 }
1643
1644 json.animations.push( {
1645 name: clip.name || 'clip_' + json.animations.length,
1646 samplers: samplers,
1647 channels: channels
1648 } );
1649 return json.animations.length - 1;
1650
1651 }
1652
1653 /**
1654 * @param {THREE.Object3D} object
1655 * @return {number|null}
1656 */
1657 processSkin( object ) {
1658
1659 const json = this.json;
1660 const nodeMap = this.nodeMap;
1661 const node = json.nodes[ nodeMap.get( object ) ];
1662 const skeleton = object.skeleton;
1663 if ( skeleton === undefined ) return null;
1664 const rootJoint = object.skeleton.bones[ 0 ];
1665 if ( rootJoint === undefined ) return null;
1666 const joints = [];
1667 const inverseBindMatrices = new Float32Array( skeleton.bones.length * 16 );
1668 const temporaryBoneInverse = new THREE.Matrix4();
1669 for ( let i = 0; i < skeleton.bones.length; ++ i ) {
1670
1671 joints.push( nodeMap.get( skeleton.bones[ i ] ) );
1672 temporaryBoneInverse.copy( skeleton.boneInverses[ i ] );
1673 temporaryBoneInverse.multiply( object.bindMatrix ).toArray( inverseBindMatrices, i * 16 );
1674
1675 }
1676
1677 if ( json.skins === undefined ) json.skins = [];
1678 json.skins.push( {
1679 inverseBindMatrices: this.processAccessor( new THREE.BufferAttribute( inverseBindMatrices, 16 ) ),
1680 joints: joints,
1681 skeleton: nodeMap.get( rootJoint )
1682 } );
1683 const skinIndex = node.skin = json.skins.length - 1;
1684 return skinIndex;
1685
1686 }
1687
1688 /**
1689 * Process Object3D node
1690 * @param {THREE.Object3D} node Object3D to processNode
1691 * @return {Integer} Index of the node in the nodes list
1692 */
1693 processNode( object ) {
1694
1695 const json = this.json;
1696 const options = this.options;
1697 const nodeMap = this.nodeMap;
1698 if ( ! json.nodes ) json.nodes = [];
1699 const nodeDef = {};
1700 if ( options.trs ) {
1701
1702 const rotation = object.quaternion.toArray();
1703 const position = object.position.toArray();
1704 const scale = object.scale.toArray();
1705 if ( ! equalArray( rotation, [ 0, 0, 0, 1 ] ) ) {
1706
1707 nodeDef.rotation = rotation;
1708
1709 }
1710
1711 if ( ! equalArray( position, [ 0, 0, 0 ] ) ) {
1712
1713 nodeDef.translation = position;
1714
1715 }
1716
1717 if ( ! equalArray( scale, [ 1, 1, 1 ] ) ) {
1718
1719 nodeDef.scale = scale;
1720
1721 }
1722
1723 } else {
1724
1725 if ( object.matrixAutoUpdate ) {
1726
1727 object.updateMatrix();
1728
1729 }
1730
1731 if ( isIdentityMatrix( object.matrix ) === false ) {
1732
1733 nodeDef.matrix = object.matrix.elements;
1734
1735 }
1736
1737 }
1738
1739 // We don't export empty strings name because it represents no-name in Three.js.
1740 if ( object.name !== '' ) nodeDef.name = String( object.name );
1741 this.serializeUserData( object, nodeDef );
1742 if ( object.isMesh || object.isLine || object.isPoints ) {
1743
1744 const meshIndex = this.processMesh( object );
1745 if ( meshIndex !== null ) nodeDef.mesh = meshIndex;
1746
1747 } else if ( object.isCamera ) {
1748
1749 nodeDef.camera = this.processCamera( object );
1750
1751 }
1752
1753 if ( object.isSkinnedMesh ) this.skins.push( object );
1754 if ( object.children.length > 0 ) {
1755
1756 const children = [];
1757 for ( let i = 0, l = object.children.length; i < l; i ++ ) {
1758
1759 const child = object.children[ i ];
1760 if ( child.visible || options.onlyVisible === false ) {
1761
1762 const nodeIndex = this.processNode( child );
1763 if ( nodeIndex !== null ) children.push( nodeIndex );
1764
1765 }
1766
1767 }
1768
1769 if ( children.length > 0 ) nodeDef.children = children;
1770
1771 }
1772
1773 this._invokeAll( function ( ext ) {
1774
1775 ext.writeNode && ext.writeNode( object, nodeDef );
1776
1777 } );
1778 const nodeIndex = json.nodes.push( nodeDef ) - 1;
1779 nodeMap.set( object, nodeIndex );
1780 return nodeIndex;
1781
1782 }
1783
1784 /**
1785 * Process THREE.Scene
1786 * @param {Scene} node THREE.Scene to process
1787 */
1788 processScene( scene ) {
1789
1790 const json = this.json;
1791 const options = this.options;
1792 if ( ! json.scenes ) {
1793
1794 json.scenes = [];
1795 json.scene = 0;
1796
1797 }
1798
1799 const sceneDef = {};
1800 if ( scene.name !== '' ) sceneDef.name = scene.name;
1801 json.scenes.push( sceneDef );
1802 const nodes = [];
1803 for ( let i = 0, l = scene.children.length; i < l; i ++ ) {
1804
1805 const child = scene.children[ i ];
1806 if ( child.visible || options.onlyVisible === false ) {
1807
1808 const nodeIndex = this.processNode( child );
1809 if ( nodeIndex !== null ) nodes.push( nodeIndex );
1810
1811 }
1812
1813 }
1814
1815 if ( nodes.length > 0 ) sceneDef.nodes = nodes;
1816 this.serializeUserData( scene, sceneDef );
1817
1818 }
1819
1820 /**
1821 * Creates a THREE.Scene to hold a list of objects and parse it
1822 * @param {Array} objects List of objects to process
1823 */
1824 processObjects( objects ) {
1825
1826 const scene = new THREE.Scene();
1827 scene.name = 'AuxScene';
1828 for ( let i = 0; i < objects.length; i ++ ) {
1829
1830 // We push directly to children instead of calling `add` to prevent
1831 // modify the .parent and break its original scene and hierarchy
1832 scene.children.push( objects[ i ] );
1833
1834 }
1835
1836 this.processScene( scene );
1837
1838 }
1839
1840 /**
1841 * @param {THREE.Object3D|Array<THREE.Object3D>} input
1842 */
1843 processInput( input ) {
1844
1845 const options = this.options;
1846 input = input instanceof Array ? input : [ input ];
1847 this._invokeAll( function ( ext ) {
1848
1849 ext.beforeParse && ext.beforeParse( input );
1850
1851 } );
1852 const objectsWithoutScene = [];
1853 for ( let i = 0; i < input.length; i ++ ) {
1854
1855 if ( input[ i ] instanceof THREE.Scene ) {
1856
1857 this.processScene( input[ i ] );
1858
1859 } else {
1860
1861 objectsWithoutScene.push( input[ i ] );
1862
1863 }
1864
1865 }
1866
1867 if ( objectsWithoutScene.length > 0 ) this.processObjects( objectsWithoutScene );
1868 for ( let i = 0; i < this.skins.length; ++ i ) {
1869
1870 this.processSkin( this.skins[ i ] );
1871
1872 }
1873
1874 for ( let i = 0; i < options.animations.length; ++ i ) {
1875
1876 this.processAnimation( options.animations[ i ], input[ 0 ] );
1877
1878 }
1879
1880 this._invokeAll( function ( ext ) {
1881
1882 ext.afterParse && ext.afterParse( input );
1883
1884 } );
1885
1886 }
1887 _invokeAll( func ) {
1888
1889 for ( let i = 0, il = this.plugins.length; i < il; i ++ ) {
1890
1891 func( this.plugins[ i ] );
1892
1893 }
1894
1895 }
1896
1897 }
1898
1899 /**
1900 * Punctual Lights Extension
1901 *
1902 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
1903 */
1904 class GLTFLightExtension {
1905
1906 constructor( writer ) {
1907
1908 this.writer = writer;
1909 this.name = 'KHR_lights_punctual';
1910
1911 }
1912 writeNode( light, nodeDef ) {
1913
1914 if ( ! light.isLight ) return;
1915 if ( ! light.isDirectionalLight && ! light.isPointLight && ! light.isSpotLight ) {
1916
1917 console.warn( 'THREE.GLTFExporter: Only directional, point, and spot lights are supported.', light );
1918 return;
1919
1920 }
1921
1922 const writer = this.writer;
1923 const json = writer.json;
1924 const extensionsUsed = writer.extensionsUsed;
1925 const lightDef = {};
1926 if ( light.name ) lightDef.name = light.name;
1927 lightDef.color = light.color.toArray();
1928 lightDef.intensity = light.intensity;
1929 if ( light.isDirectionalLight ) {
1930
1931 lightDef.type = 'directional';
1932
1933 } else if ( light.isPointLight ) {
1934
1935 lightDef.type = 'point';
1936 if ( light.distance > 0 ) lightDef.range = light.distance;
1937
1938 } else if ( light.isSpotLight ) {
1939
1940 lightDef.type = 'spot';
1941 if ( light.distance > 0 ) lightDef.range = light.distance;
1942 lightDef.spot = {};
1943 lightDef.spot.innerConeAngle = ( light.penumbra - 1.0 ) * light.angle * - 1.0;
1944 lightDef.spot.outerConeAngle = light.angle;
1945
1946 }
1947
1948 if ( light.decay !== undefined && light.decay !== 2 ) {
1949
1950 console.warn( 'THREE.GLTFExporter: Light decay may be lost. glTF is physically-based, ' + 'and expects light.decay=2.' );
1951
1952 }
1953
1954 if ( light.target && ( light.target.parent !== light || light.target.position.x !== 0 || light.target.position.y !== 0 || light.target.position.z !== - 1 ) ) {
1955
1956 console.warn( 'THREE.GLTFExporter: Light direction may be lost. For best results, ' + 'make light.target a child of the light with position 0,0,-1.' );
1957
1958 }
1959
1960 if ( ! extensionsUsed[ this.name ] ) {
1961
1962 json.extensions = json.extensions || {};
1963 json.extensions[ this.name ] = {
1964 lights: []
1965 };
1966 extensionsUsed[ this.name ] = true;
1967
1968 }
1969
1970 const lights = json.extensions[ this.name ].lights;
1971 lights.push( lightDef );
1972 nodeDef.extensions = nodeDef.extensions || {};
1973 nodeDef.extensions[ this.name ] = {
1974 light: lights.length - 1
1975 };
1976
1977 }
1978
1979 }
1980
1981 /**
1982 * Unlit Materials Extension
1983 *
1984 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit
1985 */
1986 class GLTFMaterialsUnlitExtension {
1987
1988 constructor( writer ) {
1989
1990 this.writer = writer;
1991 this.name = 'KHR_materials_unlit';
1992
1993 }
1994 writeMaterial( material, materialDef ) {
1995
1996 if ( ! material.isMeshBasicMaterial ) return;
1997 const writer = this.writer;
1998 const extensionsUsed = writer.extensionsUsed;
1999 materialDef.extensions = materialDef.extensions || {};
2000 materialDef.extensions[ this.name ] = {};
2001 extensionsUsed[ this.name ] = true;
2002 materialDef.pbrMetallicRoughness.metallicFactor = 0.0;
2003 materialDef.pbrMetallicRoughness.roughnessFactor = 0.9;
2004
2005 }
2006
2007 }
2008
2009 /**
2010 * Clearcoat Materials Extension
2011 *
2012 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat
2013 */
2014 class GLTFMaterialsClearcoatExtension {
2015
2016 constructor( writer ) {
2017
2018 this.writer = writer;
2019 this.name = 'KHR_materials_clearcoat';
2020
2021 }
2022 writeMaterial( material, materialDef ) {
2023
2024 if ( ! material.isMeshPhysicalMaterial ) return;
2025 const writer = this.writer;
2026 const extensionsUsed = writer.extensionsUsed;
2027 const extensionDef = {};
2028 extensionDef.clearcoatFactor = material.clearcoat;
2029 if ( material.clearcoatMap ) {
2030
2031 const clearcoatMapDef = {
2032 index: writer.processTexture( material.clearcoatMap )
2033 };
2034 writer.applyTextureTransform( clearcoatMapDef, material.clearcoatMap );
2035 extensionDef.clearcoatTexture = clearcoatMapDef;
2036
2037 }
2038
2039 extensionDef.clearcoatRoughnessFactor = material.clearcoatRoughness;
2040 if ( material.clearcoatRoughnessMap ) {
2041
2042 const clearcoatRoughnessMapDef = {
2043 index: writer.processTexture( material.clearcoatRoughnessMap )
2044 };
2045 writer.applyTextureTransform( clearcoatRoughnessMapDef, material.clearcoatRoughnessMap );
2046 extensionDef.clearcoatRoughnessTexture = clearcoatRoughnessMapDef;
2047
2048 }
2049
2050 if ( material.clearcoatNormalMap ) {
2051
2052 const clearcoatNormalMapDef = {
2053 index: writer.processTexture( material.clearcoatNormalMap )
2054 };
2055 writer.applyTextureTransform( clearcoatNormalMapDef, material.clearcoatNormalMap );
2056 extensionDef.clearcoatNormalTexture = clearcoatNormalMapDef;
2057
2058 }
2059
2060 materialDef.extensions = materialDef.extensions || {};
2061 materialDef.extensions[ this.name ] = extensionDef;
2062 extensionsUsed[ this.name ] = true;
2063
2064 }
2065
2066 }
2067
2068 /**
2069 * Iridescence Materials Extension
2070 *
2071 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence
2072 */
2073 class GLTFMaterialsIridescenceExtension {
2074
2075 constructor( writer ) {
2076
2077 this.writer = writer;
2078 this.name = 'KHR_materials_iridescence';
2079
2080 }
2081 writeMaterial( material, materialDef ) {
2082
2083 if ( ! material.isMeshPhysicalMaterial ) return;
2084 const writer = this.writer;
2085 const extensionsUsed = writer.extensionsUsed;
2086 const extensionDef = {};
2087 extensionDef.iridescenceFactor = material.iridescence;
2088 if ( material.iridescenceMap ) {
2089
2090 const iridescenceMapDef = {
2091 index: writer.processTexture( material.iridescenceMap )
2092 };
2093 writer.applyTextureTransform( iridescenceMapDef, material.iridescenceMap );
2094 extensionDef.iridescenceTexture = iridescenceMapDef;
2095
2096 }
2097
2098 extensionDef.iridescenceIor = material.iridescenceIOR;
2099 extensionDef.iridescenceThicknessMinimum = material.iridescenceThicknessRange[ 0 ];
2100 extensionDef.iridescenceThicknessMaximum = material.iridescenceThicknessRange[ 1 ];
2101 if ( material.iridescenceThicknessMap ) {
2102
2103 const iridescenceThicknessMapDef = {
2104 index: writer.processTexture( material.iridescenceThicknessMap )
2105 };
2106 writer.applyTextureTransform( iridescenceThicknessMapDef, material.iridescenceThicknessMap );
2107 extensionDef.iridescenceThicknessTexture = iridescenceThicknessMapDef;
2108
2109 }
2110
2111 materialDef.extensions = materialDef.extensions || {};
2112 materialDef.extensions[ this.name ] = extensionDef;
2113 extensionsUsed[ this.name ] = true;
2114
2115 }
2116
2117 }
2118
2119 /**
2120 * Transmission Materials Extension
2121 *
2122 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission
2123 */
2124 class GLTFMaterialsTransmissionExtension {
2125
2126 constructor( writer ) {
2127
2128 this.writer = writer;
2129 this.name = 'KHR_materials_transmission';
2130
2131 }
2132 writeMaterial( material, materialDef ) {
2133
2134 if ( ! material.isMeshPhysicalMaterial || material.transmission === 0 ) return;
2135 const writer = this.writer;
2136 const extensionsUsed = writer.extensionsUsed;
2137 const extensionDef = {};
2138 extensionDef.transmissionFactor = material.transmission;
2139 if ( material.transmissionMap ) {
2140
2141 const transmissionMapDef = {
2142 index: writer.processTexture( material.transmissionMap )
2143 };
2144 writer.applyTextureTransform( transmissionMapDef, material.transmissionMap );
2145 extensionDef.transmissionTexture = transmissionMapDef;
2146
2147 }
2148
2149 materialDef.extensions = materialDef.extensions || {};
2150 materialDef.extensions[ this.name ] = extensionDef;
2151 extensionsUsed[ this.name ] = true;
2152
2153 }
2154
2155 }
2156
2157 /**
2158 * Materials Volume Extension
2159 *
2160 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_volume
2161 */
2162 class GLTFMaterialsVolumeExtension {
2163
2164 constructor( writer ) {
2165
2166 this.writer = writer;
2167 this.name = 'KHR_materials_volume';
2168
2169 }
2170 writeMaterial( material, materialDef ) {
2171
2172 if ( ! material.isMeshPhysicalMaterial || material.transmission === 0 ) return;
2173 const writer = this.writer;
2174 const extensionsUsed = writer.extensionsUsed;
2175 const extensionDef = {};
2176 extensionDef.thicknessFactor = material.thickness;
2177 if ( material.thicknessMap ) {
2178
2179 const thicknessMapDef = {
2180 index: writer.processTexture( material.thicknessMap )
2181 };
2182 writer.applyTextureTransform( thicknessMapDef, material.thicknessMap );
2183 extensionDef.thicknessTexture = thicknessMapDef;
2184
2185 }
2186
2187 extensionDef.attenuationDistance = material.attenuationDistance;
2188 extensionDef.attenuationColor = material.attenuationColor.toArray();
2189 materialDef.extensions = materialDef.extensions || {};
2190 materialDef.extensions[ this.name ] = extensionDef;
2191 extensionsUsed[ this.name ] = true;
2192
2193 }
2194
2195 }
2196
2197 /**
2198 * Static utility functions
2199 */
2200 GLTFExporter.Utils = {
2201 insertKeyframe: function ( track, time ) {
2202
2203 const tolerance = 0.001; // 1ms
2204 const valueSize = track.getValueSize();
2205 const times = new track.TimeBufferType( track.times.length + 1 );
2206 const values = new track.ValueBufferType( track.values.length + valueSize );
2207 const interpolant = track.createInterpolant( new track.ValueBufferType( valueSize ) );
2208 let index;
2209 if ( track.times.length === 0 ) {
2210
2211 times[ 0 ] = time;
2212 for ( let i = 0; i < valueSize; i ++ ) {
2213
2214 values[ i ] = 0;
2215
2216 }
2217
2218 index = 0;
2219
2220 } else if ( time < track.times[ 0 ] ) {
2221
2222 if ( Math.abs( track.times[ 0 ] - time ) < tolerance ) return 0;
2223 times[ 0 ] = time;
2224 times.set( track.times, 1 );
2225 values.set( interpolant.evaluate( time ), 0 );
2226 values.set( track.values, valueSize );
2227 index = 0;
2228
2229 } else if ( time > track.times[ track.times.length - 1 ] ) {
2230
2231 if ( Math.abs( track.times[ track.times.length - 1 ] - time ) < tolerance ) {
2232
2233 return track.times.length - 1;
2234
2235 }
2236
2237 times[ times.length - 1 ] = time;
2238 times.set( track.times, 0 );
2239 values.set( track.values, 0 );
2240 values.set( interpolant.evaluate( time ), track.values.length );
2241 index = times.length - 1;
2242
2243 } else {
2244
2245 for ( let i = 0; i < track.times.length; i ++ ) {
2246
2247 if ( Math.abs( track.times[ i ] - time ) < tolerance ) return i;
2248 if ( track.times[ i ] < time && track.times[ i + 1 ] > time ) {
2249
2250 times.set( track.times.slice( 0, i + 1 ), 0 );
2251 times[ i + 1 ] = time;
2252 times.set( track.times.slice( i + 1 ), i + 2 );
2253 values.set( track.values.slice( 0, ( i + 1 ) * valueSize ), 0 );
2254 values.set( interpolant.evaluate( time ), ( i + 1 ) * valueSize );
2255 values.set( track.values.slice( ( i + 1 ) * valueSize ), ( i + 2 ) * valueSize );
2256 index = i + 1;
2257 break;
2258
2259 }
2260
2261 }
2262
2263 }
2264
2265 track.times = times;
2266 track.values = values;
2267 return index;
2268
2269 },
2270 mergeMorphTargetTracks: function ( clip, root ) {
2271
2272 const tracks = [];
2273 const mergedTracks = {};
2274 const sourceTracks = clip.tracks;
2275 for ( let i = 0; i < sourceTracks.length; ++ i ) {
2276
2277 let sourceTrack = sourceTracks[ i ];
2278 const sourceTrackBinding = THREE.PropertyBinding.parseTrackName( sourceTrack.name );
2279 const sourceTrackNode = THREE.PropertyBinding.findNode( root, sourceTrackBinding.nodeName );
2280 if ( sourceTrackBinding.propertyName !== 'morphTargetInfluences' || sourceTrackBinding.propertyIndex === undefined ) {
2281
2282 // Tracks that don't affect morph targets, or that affect all morph targets together, can be left as-is.
2283 tracks.push( sourceTrack );
2284 continue;
2285
2286 }
2287
2288 if ( sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodDiscrete && sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodLinear ) {
2289
2290 if ( sourceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
2291
2292 // This should never happen, because glTF morph target animations
2293 // affect all targets already.
2294 throw new Error( 'THREE.GLTFExporter: Cannot merge tracks with glTF CUBICSPLINE interpolation.' );
2295
2296 }
2297
2298 console.warn( 'THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead.' );
2299 sourceTrack = sourceTrack.clone();
2300 sourceTrack.setInterpolation( THREE.InterpolateLinear );
2301
2302 }
2303
2304 const targetCount = sourceTrackNode.morphTargetInfluences.length;
2305 const targetIndex = sourceTrackNode.morphTargetDictionary[ sourceTrackBinding.propertyIndex ];
2306 if ( targetIndex === undefined ) {
2307
2308 throw new Error( 'THREE.GLTFExporter: Morph target name not found: ' + sourceTrackBinding.propertyIndex );
2309
2310 }
2311
2312 let mergedTrack;
2313
2314 // If this is the first time we've seen this object, create a new
2315 // track to store merged keyframe data for each morph target.
2316 if ( mergedTracks[ sourceTrackNode.uuid ] === undefined ) {
2317
2318 mergedTrack = sourceTrack.clone();
2319 const values = new mergedTrack.ValueBufferType( targetCount * mergedTrack.times.length );
2320 for ( let j = 0; j < mergedTrack.times.length; j ++ ) {
2321
2322 values[ j * targetCount + targetIndex ] = mergedTrack.values[ j ];
2323
2324 }
2325
2326 // We need to take into consideration the intended target node
2327 // of our original un-merged morphTarget animation.
2328 mergedTrack.name = ( sourceTrackBinding.nodeName || '' ) + '.morphTargetInfluences';
2329 mergedTrack.values = values;
2330 mergedTracks[ sourceTrackNode.uuid ] = mergedTrack;
2331 tracks.push( mergedTrack );
2332 continue;
2333
2334 }
2335
2336 const sourceInterpolant = sourceTrack.createInterpolant( new sourceTrack.ValueBufferType( 1 ) );
2337 mergedTrack = mergedTracks[ sourceTrackNode.uuid ];
2338
2339 // For every existing keyframe of the merged track, write a (possibly
2340 // interpolated) value from the source track.
2341 for ( let j = 0; j < mergedTrack.times.length; j ++ ) {
2342
2343 mergedTrack.values[ j * targetCount + targetIndex ] = sourceInterpolant.evaluate( mergedTrack.times[ j ] );
2344
2345 }
2346
2347 // For every existing keyframe of the source track, write a (possibly
2348 // new) keyframe to the merged track. Values from the previous loop may
2349 // be written again, but keyframes are de-duplicated.
2350 for ( let j = 0; j < sourceTrack.times.length; j ++ ) {
2351
2352 const keyframeIndex = this.insertKeyframe( mergedTrack, sourceTrack.times[ j ] );
2353 mergedTrack.values[ keyframeIndex * targetCount + targetIndex ] = sourceTrack.values[ j ];
2354
2355 }
2356
2357 }
2358
2359 clip.tracks = tracks;
2360 return clip;
2361
2362 }
2363 };
2364
2365 THREE.GLTFExporter = GLTFExporter;
2366
2367} )();