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.