PageSourceSearch

https://rippey.ai/assets/threejs/jsm/exporters/ColladaExporter.js

js rippey.ai collected 2026-10-05 12:33:52 UTC 18,790 bytes, 713 lines download raw bytes

1import {
2	Color,
3	DoubleSide,
4	Matrix4,
5	MeshBasicMaterial
6} from 'three';
7
8/**
9 * https://github.com/gkjohnson/collada-exporter-js
10 *
11 * Usage:
12 *  const exporter = new ColladaExporter();
13 *
14 *  const data = exporter.parse(mesh);
15 *
16 * Format Definition:
17 *  https://www.khronos.org/collada/
18 */
19
20class ColladaExporter {
21
22	parse( object, onDone, options = {} ) {
23
24		options = Object.assign( {
25			version: '1.4.1',
26			author: null,
27			textureDirectory: '',
28			upAxis: 'Y_UP',
29			unitName: null,
30			unitMeter: null,
31		}, options );
32
33		if ( options.upAxis.match( /^[XYZ]_UP$/ ) === null ) {
34
35			console.error( 'ColladaExporter: Invalid upAxis: valid values are X_UP, Y_UP or Z_UP.' );
36			return null;
37
38		}
39
40		if ( options.unitName !== null && options.unitMeter === null ) {
41
42			console.error( 'ColladaExporter: unitMeter needs to be specified if unitName is specified.' );
43			return null;
44
45		}
46
47		if ( options.unitMeter !== null && options.unitName === null ) {
48
49			console.error( 'ColladaExporter: unitName needs to be specified if unitMeter is specified.' );
50			return null;
51
52		}
53
54		if ( options.textureDirectory !== '' ) {
55
56			options.textureDirectory = `${ options.textureDirectory }/`
57				.replace( /\\/g, '/' )
58				.replace( /\/+/g, '/' );
59
60		}
61
62		const version = options.version;
63
64		if ( version !== '1.4.1' && version !== '1.5.0' ) {
65
66			console.warn( `ColladaExporter : Version ${ version } not supported for export. Only 1.4.1 and 1.5.0.` );
67			return null;
68
69		}
70
71		// Convert the urdf xml into a well-formatted, indented format
72		function format( urdf ) {
73
74			const IS_END_TAG = /^<\//;
75			const IS_SELF_CLOSING = /(\?>$)|(\/>$)/;
76			const HAS_TEXT = /<[^>]+>[^<]*<\/[^<]+>/;
77
78			const pad = ( ch, num ) => ( num > 0 ? ch + pad( ch, num - 1 ) : '' );
79
80			let tagnum = 0;
81
82			return urdf
83				.match( /(<[^>]+>[^<]+<\/[^<]+>)|(<[^>]+>)/g )
84				.map( tag => {
85
86					if ( ! HAS_TEXT.test( tag ) && ! IS_SELF_CLOSING.test( tag ) && IS_END_TAG.test( tag ) ) {
87
88						tagnum --;
89
90					}
91
92					const res = `${ pad( '  ', tagnum ) }${ tag }`;
93
94					if ( ! HAS_TEXT.test( tag ) && ! IS_SELF_CLOSING.test( tag ) && ! IS_END_TAG.test( tag ) ) {
95
96						tagnum ++;
97
98					}
99
100					return res;
101
102				} )
103				.join( '\n' );
104
105		}
106
107		// Convert an image into a png format for saving
108		function base64ToBuffer( str ) {
109
110			const b = atob( str );
111			const buf = new Uint8Array( b.length );
112
113			for ( let i = 0, l = buf.length; i < l; i ++ ) {
114
115				buf[ i ] = b.charCodeAt( i );
116
117			}
118
119			return buf;
120
121		}
122
123		let canvas, ctx;
124
125		function imageToData( image, ext ) {
126
127			canvas = canvas || document.createElement( 'canvas' );
128			ctx = ctx || canvas.getContext( '2d' );
129
130			canvas.width = image.width;
131			canvas.height = image.height;
132
133			ctx.drawImage( image, 0, 0 );
134
135			// Get the base64 encoded data
136			const base64data = canvas
137				.toDataURL( `image/${ ext }`, 1 )
138				.replace( /^data:image\/(png|jpg);base64,/, '' );
139
140			// Convert to a uint8 array
141			return base64ToBuffer( base64data );
142
143		}
144
145		// gets the attribute array. Generate a new array if the attribute is interleaved
146		const getFuncs = [ 'getX', 'getY', 'getZ', 'getW' ];
147		const tempColor = new Color();
148
149		function attrBufferToArray( attr, isColor = false ) {
150
151			if ( isColor ) {
152
153				// convert the colors to srgb before export
154				// colors are always written as floats
155				const arr = new Float32Array( attr.count * 3 );
156				for ( let i = 0, l = attr.count; i < l; i ++ ) {
157
158					tempColor
159						.fromBufferAttribute( attr, i )
160						.convertLinearToSRGB();
161
162					arr[ 3 * i + 0 ] = tempColor.r;
163					arr[ 3 * i + 1 ] = tempColor.g;
164					arr[ 3 * i + 2 ] = tempColor.b;
165
166				}
167
168				return arr;
169
170			} else if ( attr.isInterleavedBufferAttribute ) {
171
172				// use the typed array constructor to save on memory
173				const arr = new attr.array.constructor( attr.count * attr.itemSize );
174				const size = attr.itemSize;
175
176				for ( let i = 0, l = attr.count; i < l; i ++ ) {
177
178					for ( let j = 0; j < size; j ++ ) {
179
180						arr[ i * size + j ] = attr[ getFuncs[ j ] ]( i );
181
182					}
183
184				}
185
186				return arr;
187
188			} else {
189
190				return attr.array;
191
192			}
193
194		}
195
196		// Returns an array of the same type starting at the `st` index,
197		// and `ct` length
198		function subArray( arr, st, ct ) {
199
200			if ( Array.isArray( arr ) ) return arr.slice( st, st + ct );
201			else return new arr.constructor( arr.buffer, st * arr.BYTES_PER_ELEMENT, 
201ct );
202
203		}
204
205		// Returns the string for a geometry's attribute
206		function getAttribute( attr, name, params, type, isColor = false ) {
207
208			const array = attrBufferToArray( attr, isColor );
209			const res =
210					`<source id="${ name }">` +
211
212					`<float_array id="${ name }-array" count="${ array.length }">` +
213					array.join( ' ' ) +
214					'</float_array>' +
215
216					'<technique_common>' +
217					`<accessor source="#${ name }-array" count="${ Math.floor( array.length / attr.itemSize ) }" stride="${ attr.itemSize }">` +
218
219					params.map( n => `<param name="${ n }" type="${ type }" />` ).join( '' ) +
220
221					'</accessor>' +
222					'</technique_common>' +
223					'</source>';
224
225			return res;
226
227		}
228
229		// Returns the string for a node's transform information
230		let transMat;
231		function getTransform( o ) {
232
233			// ensure the object's matrix is up to date
234			// before saving the transform
235			o.updateMatrix();
236
237			transMat = transMat || new Matrix4();
238			transMat.copy( o.matrix );
239			transMat.transpose();
240			return `<matrix>${ transMat.toArray().join( ' ' ) }</matrix>`;
241
242		}
243
244		// Process the given piece of geometry into the geometry library
245		// Returns the mesh id
246		function processGeometry( bufferGeometry ) {
247
248			let info = geometryInfo.get( bufferGeometry );
249
250			if ( ! info ) {
251
252				const meshid = `Mesh${ libraryGeometries.length + 1 }`;
253
254				const indexCount =
255					bufferGeometry.index ?
256						bufferGeometry.index.count * bufferGeometry.index.itemSize :
257						bufferGeometry.attributes.position.count;
258
259				const groups =
260					bufferGeometry.groups != null && bufferGeometry.groups.length !== 0 ?
261						bufferGeometry.groups :
262						[ { start: 0, count: indexCount, materialIndex: 0 } ];
263
264
265				const gname = bufferGeometry.name ? ` name="${ bufferGeometry.name }"` : '';
266				let gnode = `<geometry id="${ meshid }"${ gname }><mesh>`;
267
268				// define the geometry node and the vertices for the geometry
269				const posName = `${ meshid }-position`;
270				const vertName = `${ meshid }-vertices`;
271				gnode += getAttribute( bufferGeometry.attributes.position, posName, [ 'X', 'Y', 'Z' ], 'float' );
272				gnode += `<vertices id="${ vertName }"><input semantic="POSITION" source="#${ posName }" /></vertices>`;
273
274				// NOTE: We're not optimizing the attribute arrays here, so they're all the same length and
275				// can therefore share the same triangle indices. However, MeshLab seems to have trouble opening
276				// models with attributes that share an offset.
277				// MeshLab Bug#424: https://sourceforge.net/p/meshlab/bugs/424/
278
279				// serialize normals
280				let triangleInputs = `<input semantic="VERTEX" source="#${ vertName }" offset="0" />`;
281				if ( 'normal' in bufferGeometry.attributes ) {
282
283					const normName = `${ meshid }-normal`;
284					gnode += getAttribute( bufferGeometry.attributes.normal, normName, [ 'X', 'Y', 'Z' ], 'float' );
285					triangleInputs += `<input semantic="NORMAL" source="#${ normName }" offset="0" />`;
286
287				}
288
289				// serialize uvs
290				if ( 'uv' in bufferGeometry.attributes ) {
291
292					const uvName = `${ meshid }-texcoord`;
293					gnode += getAttribute( bufferGeometry.attributes.uv, uvName, [ 'S', 'T' ], 'float' );
294					triangleInputs += `<input semantic="TEXCOORD" source="#${ uvName }" offset="0" set="0" />`;
295
296				}
297
298				// serialize lightmap uvs
299				if ( 'uv2' in bufferGeometry.attributes ) {
300
301					const uvName = `${ meshid }-texcoord2`;
302					gnode += getAttribute( bufferGeometry.attributes.uv2, uvName, [ 'S', 'T' ], 'float' );
303					triangleInputs += `<input semantic="TEXCOORD" source="#${ uvName }" offset="0" set="1" />`;
304
305				}
306
307				// serialize colors
308				if ( 'color' in bufferGeometry.attributes ) {
309
310					// colors are always written as floats
311					const colName = `${ meshid }-color`;
312					gnode += getAttribute( bufferGeometry.attributes.color, colName, [ 'R', 'G', 'B' ], 'float', true );
313					triangleInputs += `<input semantic="COLOR" source="#${ colName }" offset="0" />`;
314
315				}
316
317				let indexArray = null;
318				if ( bufferGeometry.index ) {
319
320					indexArray = attrBufferToArray( bufferGeometry.index );
321
322				} else {
323
324					indexArray = new Array( indexCount );
325					for ( let i = 0, l = indexArray.length; i < l; i ++ ) indexArray[ i ] = i;
326
327				}
328
329				for ( let i = 0, l = groups.length; i < l; i ++ ) {
330
331					const group = groups[ i ];
332					const subarr = subArray( indexArray, group.start, group.count );
333					const polycount = subarr.length / 3;
334					gnode += `<triangles material="MESH_MATERIAL_${ group.materialIndex }" count="${ polycount }">`;
335					gnode += triangleInputs;
336
337					gnode += `<p>${ subarr.join( ' ' ) }</p>`;
338					gnode += '</triangles>';
339
340				}
341
342				gnode += '</mesh></geometry>';
343
344				libraryGeometries.push( gnode );
345
346				info = { meshid: meshid, bufferGeometry: bufferGeometry };
347				geometryInfo.set( bufferGeometry, info );
348
349			}
350
351			return info;
352
353		}
354
355		// Process the given texture into the image library
356		// Returns the image library
357		function processTexture( tex ) {
358
359			let texid = imageMap.get( tex );
360			if ( texid == null ) {
361
362				texid = `image-${ libraryImages.length + 1 }`;
363
364				const ext = 'png';
365				const name = tex.name || texid;
366				let imageNode = `<image id="${ texid }" name="${ name }">`;
367
368				if ( version === '1.5.0' ) {
369
370					imageNode += `<init_from><ref>${ options.textureDirectory }${ name }.${ ext }</ref></init_from>`;
371
372				} else {
373
374					// version image node 1.4.1
375					imageNode += `<init_from>${ options.textureDirectory }${ name }.${ ext }</init_from>`;
376
377				}
378
379				imageNode += '</image>';
380
381				libraryImages.push( imageNode );
382				imageMap.set( tex, texid );
383				textures.push( {
384					directory: options.textureDirectory,
385					name,
386					ext,
387					data: imageToData( tex.image, ext ),
388					original: tex
389				} );
390
391			}
392
393			return texid;
394
395		}
396
397		// Process the given material into the material and effect libraries
398		// Returns the material id
399		function processMaterial( m ) {
400
401			let matid = materialMap.get( m );
402
403			if ( matid == null ) {
404
405				matid = `Mat${ libraryEffects.length + 1 }`;
406
407				let type = 'phong';
408
409				if ( m.isMeshLambertMaterial === true ) {
410
411					type = 'lambert';
412
413				} else if ( m.isMeshBasicMaterial === true ) {
414
415					type = 'constant';
416
417					if ( m.map !== null ) {
418
419						// The Collada spec does not support diffuse texture maps with the
420						// constant shader type.
421						// mrdoob/three.js#15469
422						console.warn( 'ColladaExporter: Texture maps not supported with MeshBasicMaterial.' );
423
424					}
425
426				}
427
428				const emissive = m.emissive ? m.emissive : new Color( 0, 0, 0 );
429				const diffuse = m.color ? m.color : new Color( 0, 0, 0 );
430				const specular = m.specular ? m.specular : new Color( 1, 1, 1 );
431				const shininess = m.shininess || 0;
432				const reflectivity = m.reflectivity || 0;
433
434				emissive.convertLinearToSRGB();
435				specular.convertLinearToSRGB();
436				diffuse.convertLinearToSRGB();
437
438				// Do not export and alpha map for the reasons mentioned in issue (#13792)
439				// in three.js alpha maps are black and white, but collada expects the alpha
440				// channel to specify the transparency
441				let transparencyNode = '';
442				if ( m.transparent === true ) {
443
444					transparencyNode +=
445						'<transparent>' +
446						(
447							m.map ?
448								'<texture texture="diffuse-sampler"></texture>' :
449								'<float>1</float>'
450						) +
451						'</transparent>';
452
453					if ( m.opacity < 1 ) {
454
455						transparencyNode += `<transparency><float>${ m.opacity }</float></transparency>`;
456
457					}
458
459				}
460
461				const techniqueNode = `<technique sid="common"><${ type }>` +
462
463					'<emission>' +
464
465					(
466						m.emissiveMap ?
467							'<texture texture="emissive-sampler" texcoord="TEXCOORD" />' :
468							`<color sid="emission">${ emissive.r } ${ emissive.g } ${ emissive.b } 1</color>`
469					) +
470
471					'</emission>' +
472
473					(
474						type !== 'constant' ?
475							'<diffuse>' +
476
477						(
478							m.map ?
479								'<texture texture="diffuse-sampler" texcoord="TEXCOORD" />' :
480								`<color sid="diffuse">${ diffuse.r } ${ diffuse.g } ${ diffuse.b } 1</color>`
481						) +
482						'</diffuse>'
483							: ''
484					) +
485
486					(
487						type !== 'constant' ?
488							'<bump>' +
489
490						(
491							m.normalMap ? '<texture texture="bump-sampler" texcoord="TEXCOORD" />' : ''
492						) +
493						'</bump>'
494							: ''
495					) +
496
497					(
498						type === 'phong' ?
499							`<specular><color sid="specular">${ specular.r } ${ specular.g } ${ specular.b } 1</color></specular>` +
500
501						'<shininess>' +
502
503						(
504							m.specularMap ?
505								'<texture texture="specular-sampler" texcoord="TEXCOORD" />' :
506								`<float sid="shininess">${ shininess }</float>`
507						) +
508
509						'</shininess>'
510							: ''
511					) +
512
513					`<reflective><color>${ diffuse.r } ${ diffuse.g } ${ diffuse.b } 1</color></reflective>` +
514
515					`<reflectivity><float>${ reflectivity }</float></reflectivity>` +
516
517					transparencyNode +
518
519					`</${ type }></technique>`;
520
521				const effectnode =
522					`<effect id="${ matid }-effect">` +
523					'<profile_COMMON>' +
524
525					(
526						m.map ?
527							'<newparam sid="diffuse-surface"><surface type="2D">' +
528							`<init_from>${ processTexture( m.map ) }</init_from>` +
529							'</surface></newparam>' +
530							'<newparam sid="diffuse-sampler"><sampler2D><source>diffuse-surface</source></sampler2D></newparam>' :
531							''
532					) +
533
534					(
535						m.specularMap ?
536							'<newparam sid="specular-surface"><surface type="2D">' +
537							`<init_from>${ processTexture( m.specularMap ) }</init_from>` +
538							'</surface></newparam>' +
539							'<newparam sid="specular-sampler"><sampler2D><source>specular-surface</source></sampler2D></newparam>' :
540							''
541					) +
542
543					(
544						m.emissiveMap ?
545							'<newparam sid="emissive-surface"><surface type="2D">' +
546							`<init_from>${ processTexture( m.emissiveMap ) }</init_from>` +
547							'</surface></newparam>' +
548							'<newparam sid="emissive-sampler"><sampler2D><source>emissive-surface</source></sampler2D></newparam>' :
549							''
550					) +
551
552					(
553						m.normalMap ?
554							'<newparam sid="bump-surface"><surface type="2D">' +
555							`<init_from>${ processTexture( m.normalMap ) }</init_from>` +
556							'</surface></newparam>' +
557							'<newparam sid="bump-sampler"><sampler2D><source>bump-surface</source></sampler2D></newparam>' :
558							''
559					) +
560
561					techniqueNode +
562
563					(
564						m.side === DoubleSide ?
565							'<extra><technique profile="THREEJS"><double_sided sid="double_sided" type="int">1</double_sided></technique></extra>' :
566							''
567					) +
568
569					'</profile_COMMON>' +
570
571					'</effect>';
572
573				const materialName = m.name ? ` name="${ m.name }"` : '';
574				const materialNode = `<material id="${ matid }"${ materialName }><instance_effect url="#${ matid }-effect" /></material>`;
575
576				libraryMaterials.push( materialNode );
577				libraryEffects.push( effectnode );
578				materialMap.set( m, matid );
579
580			}
581
582			return matid;
583
584		}
585
586		// Recursively process the object into a scene
587		function processObject( o ) {
588
589			let node = `<node name="${ o.name }">`;
590
591			node += getTransform( o );
592
593			if ( o.isMesh === true && o.geometry !== null ) {
594
595				// function returns the id associated with the mesh and a "BufferGeometry" version
596				// of the geometry in case it's not a geometry.
597				const geomInfo = processGeometry( o.geometry );
598				const meshid = geomInfo.meshid;
599				const geometry = geomInfo.bufferGeometry;
600
601				// ids of the materials to bind to the geometry
602				let matids = null;
603				let matidsArray;
604
605				// get a list of materials to bind to the sub groups of the geometry.
606				// If the amount of subgroups is greater than the materials, than reuse
607				// the materials.
608				const mat = o.material || new MeshBasicMaterial();
609				const materials = Array.isArray( mat ) ? mat : [ mat ];
610
611				if ( geometry.groups.length > materials.length ) {
612
613					matidsArray = new Array( geometry.groups.length );
614
615				} else {
616
617					matidsArray = new Array( materials.length );
618
619				}
620
621				matids = matidsArray.fill().map( ( v, i ) => processMaterial( materials[ i % materials.length ] ) );
622
623				node +=
624					`<instance_geometry url="#${ meshid }">` +
625
626					(
627						matids.length > 0 ?
628							'<bind_material><technique_common>' +
629							matids.map( ( id, i ) =>
630
631								`<instance_material symbol="MESH_MATERIAL_${ i }" target="#${ id }" >` +
632
633								'<bind_vertex_input semantic="TEXCOORD" input_semantic="TEXCOORD" input_set="0" />' +
634
635								'</instance_material>'
636							).join( '' ) +
637							'</technique_common></bind_material>' :
638							''
639					) +
640
641					'</instance_geometry>';
642
643			}
644
645			o.children.forEach( c => node += processObject( c ) );
646
647			node += '</node>';
648
649			return node;
650
651		}
652
653		const geometryInfo = new WeakMap();
654		const materialMap = new WeakMap();
655		const imageMap = new WeakMap();
656		const textures = [];
657
658		const libraryImages = [];
659		const libraryGeometries = [];
660		const libraryEffects = [];
661		const libraryMaterials = [];
662		const libraryVisualScenes = processObject( object );
663
664		const specLink = version === '1.4.1' ? 'http://www.collada.org/2005/11/COLLADASchema' : 'https://www.khronos.org/collada/';
665		let dae =
666			'<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' +
667			`<COLLADA xmlns="${ specLink }" version="${ version }">` +
668			'<asset>' +
669			(
670				'<contributor>' +
671				'<authoring_tool>three.js Collada Exporter</authoring_tool>' +
672				( options.author !== null ? `<author>${ options.author }</author>` : '' ) +
673				'</contributor>' +
674				`<created>${ ( new Date() ).toISOString() }</created>` +
675				`<modified>${ ( new Date() ).toISOString() }</modified>` +
676				( options.unitName !== null ? `<unit name="${ options.unitName }" meter="${ options.unitMeter }" />` : '' ) +
677				`<up_axis>${ options.upAxis }</up_axis>`
678			) +
679			'</asset>';
680
681		dae += `<library_images>${ libraryImages.join( '' ) }</library_images>`;
682
683		dae += `<library_effects>${ libraryEffects.join( '' ) }</library_effects>`;
684
685		dae += `<library_materials>${ libraryMaterials.join( '' ) }</library_materials>`;
686
687		dae += `<library_geometries>${ libraryGeometries.join( '' ) }</library_geometries>`;
688
689		dae += `<library_visual_scenes><visual_scene id="Scene" name="scene">${ libraryVisualScenes }</visual_scene></library_visual_scenes>`;
690
691		dae += '<scene><instance_visual_scene url="#Scene"/></scene>';
692
693		dae += '</COLLADA>';
694
695		const res = {
696			data: format( dae ),
697			textures
698		};
699
700		if ( typeof onDone === 'function' ) {
701
702			requestAnimationFrame( () => onDone( res ) );
703
704		}
705
706		return res;
707
708	}
709
710}
711
712
713export { ColladaExporter };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.