import { Color, DoubleSide, Matrix4, MeshBasicMaterial } from '../lib/three.module.js' /** * https://github.com/gkjohnson/collada-exporter-js * * Usage: * const exporter = new ColladaExporter(); * * const data = exporter.parse(mesh); * * Format Definition: * https://www.khronos.org/collada/ */ class ColladaExporter { parse(object, onDone, options = {}) { options = Object.assign( { version: '1.4.1', author: null, textureDirectory: '', upAxis: 'Z_UP', // bimrocket: Z_UP unitName: null, unitMeter: null }, options ) if (options.upAxis.match(/^[XYZ]_UP$/) === null) { console.error('ColladaExporter: Invalid upAxis: valid values are X_UP, Y_UP or Z_UP.') return null } if (options.unitName !== null && options.unitMeter === null) { console.error('ColladaExporter: unitMeter needs to be specified if unitName is specified.') return null } if (options.unitMeter !== null && options.unitName === null) { console.error('ColladaExporter: unitName needs to be specified if unitMeter is specified.') return null } if (options.textureDirectory !== '') { options.textureDirectory = `${options.textureDirectory}/`.replace(/\\/g, '/').replace(/\/+/g, '/') } const version = options.version if (version !== '1.4.1' && version !== '1.5.0') { console.warn(`ColladaExporter : Version ${version} not supported for export. Only 1.4.1 and 1.5.0.`) return null } // Convert the urdf xml into a well-formatted, indented format function format(urdf) { const IS_END_TAG = /^<\// const IS_SELF_CLOSING = /(\?>$)|(\/>$)/ const HAS_TEXT = /<[^>]+>[^<]*<\/[^<]+>/ const pad = (ch, num) => (num > 0 ? ch + pad(ch, num - 1) : '') let tagnum = 0 return urdf .match(/(<[^>]+>[^<]+<\/[^<]+>)|(<[^>]+>)/g) .map(tag => { if (!HAS_TEXT.test(tag) && !IS_SELF_CLOSING.test(tag) && IS_END_TAG.test(tag)) { tagnum-- } const res = `${pad(' ', tagnum)}${tag}` if (!HAS_TEXT.test(tag) && !IS_SELF_CLOSING.test(tag) && !IS_END_TAG.test(tag)) { tagnum++ } return res }) .join('\n') } // Convert an image into a png format for saving function base64ToBuffer(str) { const b = atob(str) const buf = new Uint8Array(b.length) for (let i = 0, l = buf.length; i < l; i++) { buf[i] = b.charCodeAt(i) } return buf } let canvas, ctx function imageToData(image, ext) { canvas = canvas || document.createElement('canvas') ctx = ctx || canvas.getContext('2d') canvas.width = image.width canvas.height = image.height ctx.drawImage(image, 0, 0) // Get the base64 encoded data const base64data = canvas.toDataURL(`image/${ext}`, 1).replace(/^data:image\/(png|jpg);base64,/, '') // Convert to a uint8 array return base64ToBuffer(base64data) } // gets the attribute array. Generate a new array if the attribute is interleaved const getFuncs = ['getX', 'getY', 'getZ', 'getW'] const tempColor = new Color() function attrBufferToArray(attr, isColor = false) { if (isColor) { // convert the colors to srgb before export // colors are always written as floats const arr = new Float32Array(attr.count * 3) for (let i = 0, l = attr.count; i < l; i++) { tempColor.fromBufferAttribute(attr, i).convertLinearToSRGB() arr[3 * i + 0] = tempColor.r arr[3 * i + 1] = tempColor.g arr[3 * i + 2] = tempColor.b } return arr } else if (attr.isInterleavedBufferAttribute) { // use the typed array constructor to save on memory const arr = new attr.array.constructor(attr.count * attr.itemSize) const size = attr.itemSize for (let i = 0, l = attr.count; i < l; i++) { for (let j = 0; j < size; j++) { arr[i * size + j] = attr[getFuncs[j]](i) } } return arr } else { return attr.array } } // Returns an array of the same type starting at the `st` index, // and `ct` length function subArray(arr, st, ct) { if (Array.isArray(arr)) return arr.slice(st, st + ct) else return new arr.constructor(arr.buffer, st * arr.BYTES_PER_ELEMENT, ct) } // Returns the string for a geometry's attribute function getAttribute(attr, name, params, type, isColor = false) { const array = attrBufferToArray(attr, isColor) const res = `` + `` + array.join(' ') + '' + '' + `` + params.map(n => ``).join('') + '' + '' + '' return res } // Returns the string for a node's transform information let transMat function getTransform(o) { // ensure the object's matrix is up to date // before saving the transform o.updateMatrix() transMat = transMat || new Matrix4() transMat.copy(o.matrix) transMat.transpose() return `${transMat.toArray().join(' ')}` } // Process the given piece of geometry into the geometry library // Returns the mesh id function processGeometry(g) { let info = geometryInfo.get(g) if (!info) { // convert the geometry to bufferGeometry if it isn't already const bufferGeometry = g if (bufferGeometry.isBufferGeometry !== true) { throw new Error('THREE.ColladaExporter: Geometry is not of type THREE.BufferGeometry.') } const meshid = `Mesh${libraryGeometries.length + 1}` const indexCount = bufferGeometry.index ? bufferGeometry.index.count * bufferGeometry.index.itemSize : bufferGeometry.attributes.position.count const groups = bufferGeometry.groups != null && bufferGeometry.groups.length !== 0 ? bufferGeometry.groups : [{ start: 0, count: indexCount, materialIndex: 0 }] const gname = g.name ? ` name="${g.name}"` : '' let gnode = `` // define the geometry node and the vertices for the geometry const posName = `${meshid}-position` const vertName = `${meshid}-vertices` gnode += getAttribute(bufferGeometry.attributes.position, posName, ['X', 'Y', 'Z'], 'float') gnode += `` // NOTE: We're not optimizing the attribute arrays here, so they're all the same length and // can therefore share the same triangle indices. However, MeshLab seems to have trouble opening // models with attributes that share an offset. // MeshLab Bug#424: https://sourceforge.net/p/meshlab/bugs/424/ // serialize normals let triangleInputs = `` if ('normal' in bufferGeometry.attributes) { const normName = `${meshid}-normal` gnode += getAttribute(bufferGeometry.attributes.normal, normName, ['X', 'Y', 'Z'], 'float') triangleInputs += `` } // serialize uvs if ('uv' in bufferGeometry.attributes) { const uvName = `${meshid}-texcoord` gnode += getAttribute(bufferGeometry.attributes.uv, uvName, ['S', 'T'], 'float') triangleInputs += `` } // serialize lightmap uvs if ('uv2' in bufferGeometry.attributes) { const uvName = `${meshid}-texcoord2` gnode += getAttribute(bufferGeometry.attributes.uv2, uvName, ['S', 'T'], 'float') triangleInputs += `` } // serialize colors if ('color' in bufferGeometry.attributes) { // colors are always written as floats const colName = `${meshid}-color` gnode += getAttribute(bufferGeometry.attributes.color, colName, ['R', 'G', 'B'], 'float', true) triangleInputs += `` } let indexArray = null if (bufferGeometry.index) { indexArray = attrBufferToArray(bufferGeometry.index) } else { indexArray = new Array(indexCount) for (let i = 0, l = indexArray.length; i < l; i++) indexArray[i] = i } for (let i = 0, l = groups.length; i < l; i++) { const group = groups[i] const subarr = subArray(indexArray, group.start, group.count) const polycount = subarr.length / 3 gnode += `` gnode += triangleInputs gnode += `

${subarr.join(' ')}

` gnode += '
' } gnode += '
' libraryGeometries.push(gnode) info = { meshid: meshid, bufferGeometry: bufferGeometry } geometryInfo.set(g, info) } return info } // Process the given texture into the image library // Returns the image library function processTexture(tex) { let texid = imageMap.get(tex) if (texid == null) { texid = `image-${libraryImages.length + 1}` const ext = 'png' const name = tex.name || texid let imageNode = `` if (version === '1.5.0') { imageNode += `${options.textureDirectory}${name}.${ext}` } else { // version image node 1.4.1 imageNode += `${options.textureDirectory}${name}.${ext}` } imageNode += '' libraryImages.push(imageNode) imageMap.set(tex, texid) textures.push({ directory: options.textureDirectory, name, ext, data: imageToData(tex.image, ext), original: tex }) } return texid } // Process the given material into the material and effect libraries // Returns the material id function processMaterial(m) { let matid = materialMap.get(m) if (matid == null) { matid = `Mat${libraryEffects.length + 1}` let type = 'phong' if (m.isMeshLambertMaterial === true) { type = 'lambert' } else if (m.isMeshBasicMaterial === true) { type = 'constant' if (m.map !== null) { // The Collada spec does not support diffuse texture maps with the // constant shader type. // mrdoob/three.js#15469 console.warn('ColladaExporter: Texture maps not supported with MeshBasicMaterial.') } } const emissive = m.emissive ? m.emissive : new Color(0, 0, 0) const diffuse = m.color ? m.color : new Color(0, 0, 0) const specular = m.specular ? m.specular : new Color(1, 1, 1) const shininess = m.shininess || 0 const reflectivity = m.reflectivity || 0 emissive.convertLinearToSRGB() specular.convertLinearToSRGB() diffuse.convertLinearToSRGB() // Do not export and alpha map for the reasons mentioned in issue (#13792) // in three.js alpha maps are black and white, but collada expects the alpha // channel to specify the transparency let transparencyNode = '' if (m.transparent === true) { transparencyNode += '' + (m.map ? '' : '1') + '' if (m.opacity < 1) { transparencyNode += `${m.opacity}` } } const techniqueNode = `<${type}>` + '' + (m.emissiveMap ? '' : `${emissive.r} ${emissive.g} ${emissive.b} 1`) + '' + (type !== 'constant' ? '' + (m.map ? '' : `${diffuse.r} ${diffuse.g} ${diffuse.b} 1`) + '' : '') + (type !== 'constant' ? '' + (m.normalMap ? '' : '') + '' : '') + (type === 'phong' ? `${specular.r} ${specular.g} ${specular.b} 1` + '' + (m.specularMap ? '' : `${shininess}`) + '' : '') + `${diffuse.r} ${diffuse.g} ${diffuse.b} 1` + `${reflectivity}` + transparencyNode + `` const effectnode = `` + '' + (m.map ? '' + `${processTexture(m.map)}` + '' + 'diffuse-surface' : '') + (m.specularMap ? '' + `${processTexture(m.specularMap)}` + '' + 'specular-surface' : '') + (m.emissiveMap ? '' + `${processTexture(m.emissiveMap)}` + '' + 'emissive-surface' : '') + (m.normalMap ? '' + `${processTexture(m.normalMap)}` + '' + 'bump-surface' : '') + techniqueNode + (m.side === DoubleSide ? '1' : '') + '' + '' const materialName = m.name ? ` name="${m.name}"` : '' const materialNode = `` libraryMaterials.push(materialNode) libraryEffects.push(effectnode) materialMap.set(m, matid) } return matid } // Recursively process the object into a scene function processObject(o) { let node = `` node += getTransform(o) if (o.isMesh === true && o.geometry !== null) { // function returns the id associated with the mesh and a "BufferGeometry" version // of the geometry in case it's not a geometry. const geomInfo = processGeometry(o.geometry) const meshid = geomInfo.meshid const geometry = geomInfo.bufferGeometry // ids of the materials to bind to the geometry let matids = null let matidsArray // get a list of materials to bind to the sub groups of the geometry. // If the amount of subgroups is greater than the materials, than reuse // the materials. const mat = o.material || new MeshBasicMaterial() const materials = Array.isArray(mat) ? mat : [mat] if (geometry.groups.length > materials.length) { matidsArray = new Array(geometry.groups.length) } else { matidsArray = new Array(materials.length) } matids = matidsArray.fill().map((v, i) => processMaterial(materials[i % materials.length])) node += `` + (matids.length > 0 ? '' + matids .map( (id, i) => `` + '' + '' ) .join('') + '' : '') + '' } o.children.forEach(c => (node += processObject(c))) node += '' return node } const geometryInfo = new WeakMap() const materialMap = new WeakMap() const imageMap = new WeakMap() const textures = [] const libraryImages = [] const libraryGeometries = [] const libraryEffects = [] const libraryMaterials = [] const libraryVisualScenes = processObject(object) const specLink = version === '1.4.1' ? 'http://www.collada.org/2005/11/COLLADASchema' : 'https://www.khronos.org/collada/' let dae = '' + `` + '' + ('' + 'three.js Collada Exporter' + (options.author !== null ? `${options.author}` : '') + '' + `${new Date().toISOString()}` + `${new Date().toISOString()}` + (options.unitName !== null ? `` : '') + `${options.upAxis}`) + '' dae += `${libraryImages.join('')}` dae += `${libraryEffects.join('')}` dae += `${libraryMaterials.join('')}` dae += `${libraryGeometries.join('')}` dae += `${libraryVisualScenes}` dae += '' dae += '' const res = { data: format(dae), textures } if (typeof onDone === 'function') { requestAnimationFrame(() => onDone(res)) } return res } } export { ColladaExporter }