ColladaExporter.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. import { Color, DoubleSide, Matrix4, MeshBasicMaterial } from '../lib/three.module.js'
  2. /**
  3. * https://github.com/gkjohnson/collada-exporter-js
  4. *
  5. * Usage:
  6. * const exporter = new ColladaExporter();
  7. *
  8. * const data = exporter.parse(mesh);
  9. *
  10. * Format Definition:
  11. * https://www.khronos.org/collada/
  12. */
  13. class ColladaExporter {
  14. parse(object, onDone, options = {}) {
  15. options = Object.assign(
  16. {
  17. version: '1.4.1',
  18. author: null,
  19. textureDirectory: '',
  20. upAxis: 'Z_UP', // bimrocket: Z_UP
  21. unitName: null,
  22. unitMeter: null
  23. },
  24. options
  25. )
  26. if (options.upAxis.match(/^[XYZ]_UP$/) === null) {
  27. console.error('ColladaExporter: Invalid upAxis: valid values are X_UP, Y_UP or Z_UP.')
  28. return null
  29. }
  30. if (options.unitName !== null && options.unitMeter === null) {
  31. console.error('ColladaExporter: unitMeter needs to be specified if unitName is specified.')
  32. return null
  33. }
  34. if (options.unitMeter !== null && options.unitName === null) {
  35. console.error('ColladaExporter: unitName needs to be specified if unitMeter is specified.')
  36. return null
  37. }
  38. if (options.textureDirectory !== '') {
  39. options.textureDirectory = `${options.textureDirectory}/`.replace(/\\/g, '/').replace(/\/+/g, '/')
  40. }
  41. const version = options.version
  42. if (version !== '1.4.1' && version !== '1.5.0') {
  43. console.warn(`ColladaExporter : Version ${version} not supported for export. Only 1.4.1 and 1.5.0.`)
  44. return null
  45. }
  46. // Convert the urdf xml into a well-formatted, indented format
  47. function format(urdf) {
  48. const IS_END_TAG = /^<\//
  49. const IS_SELF_CLOSING = /(\?>$)|(\/>$)/
  50. const HAS_TEXT = /<[^>]+>[^<]*<\/[^<]+>/
  51. const pad = (ch, num) => (num > 0 ? ch + pad(ch, num - 1) : '')
  52. let tagnum = 0
  53. return urdf
  54. .match(/(<[^>]+>[^<]+<\/[^<]+>)|(<[^>]+>)/g)
  55. .map(tag => {
  56. if (!HAS_TEXT.test(tag) && !IS_SELF_CLOSING.test(tag) && IS_END_TAG.test(tag)) {
  57. tagnum--
  58. }
  59. const res = `${pad(' ', tagnum)}${tag}`
  60. if (!HAS_TEXT.test(tag) && !IS_SELF_CLOSING.test(tag) && !IS_END_TAG.test(tag)) {
  61. tagnum++
  62. }
  63. return res
  64. })
  65. .join('\n')
  66. }
  67. // Convert an image into a png format for saving
  68. function base64ToBuffer(str) {
  69. const b = atob(str)
  70. const buf = new Uint8Array(b.length)
  71. for (let i = 0, l = buf.length; i < l; i++) {
  72. buf[i] = b.charCodeAt(i)
  73. }
  74. return buf
  75. }
  76. let canvas, ctx
  77. function imageToData(image, ext) {
  78. canvas = canvas || document.createElement('canvas')
  79. ctx = ctx || canvas.getContext('2d')
  80. canvas.width = image.width
  81. canvas.height = image.height
  82. ctx.drawImage(image, 0, 0)
  83. // Get the base64 encoded data
  84. const base64data = canvas.toDataURL(`image/${ext}`, 1).replace(/^data:image\/(png|jpg);base64,/, '')
  85. // Convert to a uint8 array
  86. return base64ToBuffer(base64data)
  87. }
  88. // gets the attribute array. Generate a new array if the attribute is interleaved
  89. const getFuncs = ['getX', 'getY', 'getZ', 'getW']
  90. const tempColor = new Color()
  91. function attrBufferToArray(attr, isColor = false) {
  92. if (isColor) {
  93. // convert the colors to srgb before export
  94. // colors are always written as floats
  95. const arr = new Float32Array(attr.count * 3)
  96. for (let i = 0, l = attr.count; i < l; i++) {
  97. tempColor.fromBufferAttribute(attr, i).convertLinearToSRGB()
  98. arr[3 * i + 0] = tempColor.r
  99. arr[3 * i + 1] = tempColor.g
  100. arr[3 * i + 2] = tempColor.b
  101. }
  102. return arr
  103. } else if (attr.isInterleavedBufferAttribute) {
  104. // use the typed array constructor to save on memory
  105. const arr = new attr.array.constructor(attr.count * attr.itemSize)
  106. const size = attr.itemSize
  107. for (let i = 0, l = attr.count; i < l; i++) {
  108. for (let j = 0; j < size; j++) {
  109. arr[i * size + j] = attr[getFuncs[j]](i)
  110. }
  111. }
  112. return arr
  113. } else {
  114. return attr.array
  115. }
  116. }
  117. // Returns an array of the same type starting at the `st` index,
  118. // and `ct` length
  119. function subArray(arr, st, ct) {
  120. if (Array.isArray(arr)) return arr.slice(st, st + ct)
  121. else return new arr.constructor(arr.buffer, st * arr.BYTES_PER_ELEMENT, ct)
  122. }
  123. // Returns the string for a geometry's attribute
  124. function getAttribute(attr, name, params, type, isColor = false) {
  125. const array = attrBufferToArray(attr, isColor)
  126. const res =
  127. `<source id="${name}">` +
  128. `<float_array id="${name}-array" count="${array.length}">` +
  129. array.join(' ') +
  130. '</float_array>' +
  131. '<technique_common>' +
  132. `<accessor source="#${name}-array" count="${Math.floor(array.length / attr.itemSize)}" stride="${attr.itemSize}">` +
  133. params.map(n => `<param name="${n}" type="${type}" />`).join('') +
  134. '</accessor>' +
  135. '</technique_common>' +
  136. '</source>'
  137. return res
  138. }
  139. // Returns the string for a node's transform information
  140. let transMat
  141. function getTransform(o) {
  142. // ensure the object's matrix is up to date
  143. // before saving the transform
  144. o.updateMatrix()
  145. transMat = transMat || new Matrix4()
  146. transMat.copy(o.matrix)
  147. transMat.transpose()
  148. return `<matrix>${transMat.toArray().join(' ')}</matrix>`
  149. }
  150. // Process the given piece of geometry into the geometry library
  151. // Returns the mesh id
  152. function processGeometry(g) {
  153. let info = geometryInfo.get(g)
  154. if (!info) {
  155. // convert the geometry to bufferGeometry if it isn't already
  156. const bufferGeometry = g
  157. if (bufferGeometry.isBufferGeometry !== true) {
  158. throw new Error('THREE.ColladaExporter: Geometry is not of type THREE.BufferGeometry.')
  159. }
  160. const meshid = `Mesh${libraryGeometries.length + 1}`
  161. const indexCount = bufferGeometry.index ? bufferGeometry.index.count * bufferGeometry.index.itemSize : bufferGeometry.attributes.position.count
  162. const groups = bufferGeometry.groups != null && bufferGeometry.groups.length !== 0 ? bufferGeometry.groups : [{ start: 0, count: indexCount, materialIndex: 0 }]
  163. const gname = g.name ? ` name="${g.name}"` : ''
  164. let gnode = `<geometry id="${meshid}"${gname}><mesh>`
  165. // define the geometry node and the vertices for the geometry
  166. const posName = `${meshid}-position`
  167. const vertName = `${meshid}-vertices`
  168. gnode += getAttribute(bufferGeometry.attributes.position, posName, ['X', 'Y', 'Z'], 'float')
  169. gnode += `<vertices id="${vertName}"><input semantic="POSITION" source="#${posName}" /></vertices>`
  170. // NOTE: We're not optimizing the attribute arrays here, so they're all the same length and
  171. // can therefore share the same triangle indices. However, MeshLab seems to have trouble opening
  172. // models with attributes that share an offset.
  173. // MeshLab Bug#424: https://sourceforge.net/p/meshlab/bugs/424/
  174. // serialize normals
  175. let triangleInputs = `<input semantic="VERTEX" source="#${vertName}" offset="0" />`
  176. if ('normal' in bufferGeometry.attributes) {
  177. const normName = `${meshid}-normal`
  178. gnode += getAttribute(bufferGeometry.attributes.normal, normName, ['X', 'Y', 'Z'], 'float')
  179. triangleInputs += `<input semantic="NORMAL" source="#${normName}" offset="0" />`
  180. }
  181. // serialize uvs
  182. if ('uv' in bufferGeometry.attributes) {
  183. const uvName = `${meshid}-texcoord`
  184. gnode += getAttribute(bufferGeometry.attributes.uv, uvName, ['S', 'T'], 'float')
  185. triangleInputs += `<input semantic="TEXCOORD" source="#${uvName}" offset="0" set="0" />`
  186. }
  187. // serialize lightmap uvs
  188. if ('uv2' in bufferGeometry.attributes) {
  189. const uvName = `${meshid}-texcoord2`
  190. gnode += getAttribute(bufferGeometry.attributes.uv2, uvName, ['S', 'T'], 'float')
  191. triangleInputs += `<input semantic="TEXCOORD" source="#${uvName}" offset="0" set="1" />`
  192. }
  193. // serialize colors
  194. if ('color' in bufferGeometry.attributes) {
  195. // colors are always written as floats
  196. const colName = `${meshid}-color`
  197. gnode += getAttribute(bufferGeometry.attributes.color, colName, ['R', 'G', 'B'], 'float', true)
  198. triangleInputs += `<input semantic="COLOR" source="#${colName}" offset="0" />`
  199. }
  200. let indexArray = null
  201. if (bufferGeometry.index) {
  202. indexArray = attrBufferToArray(bufferGeometry.index)
  203. } else {
  204. indexArray = new Array(indexCount)
  205. for (let i = 0, l = indexArray.length; i < l; i++) indexArray[i] = i
  206. }
  207. for (let i = 0, l = groups.length; i < l; i++) {
  208. const group = groups[i]
  209. const subarr = subArray(indexArray, group.start, group.count)
  210. const polycount = subarr.length / 3
  211. gnode += `<triangles material="MESH_MATERIAL_${group.materialIndex}" count="${polycount}">`
  212. gnode += triangleInputs
  213. gnode += `<p>${subarr.join(' ')}</p>`
  214. gnode += '</triangles>'
  215. }
  216. gnode += '</mesh></geometry>'
  217. libraryGeometries.push(gnode)
  218. info = { meshid: meshid, bufferGeometry: bufferGeometry }
  219. geometryInfo.set(g, info)
  220. }
  221. return info
  222. }
  223. // Process the given texture into the image library
  224. // Returns the image library
  225. function processTexture(tex) {
  226. let texid = imageMap.get(tex)
  227. if (texid == null) {
  228. texid = `image-${libraryImages.length + 1}`
  229. const ext = 'png'
  230. const name = tex.name || texid
  231. let imageNode = `<image id="${texid}" name="${name}">`
  232. if (version === '1.5.0') {
  233. imageNode += `<init_from><ref>${options.textureDirectory}${name}.${ext}</ref></init_from>`
  234. } else {
  235. // version image node 1.4.1
  236. imageNode += `<init_from>${options.textureDirectory}${name}.${ext}</init_from>`
  237. }
  238. imageNode += '</image>'
  239. libraryImages.push(imageNode)
  240. imageMap.set(tex, texid)
  241. textures.push({
  242. directory: options.textureDirectory,
  243. name,
  244. ext,
  245. data: imageToData(tex.image, ext),
  246. original: tex
  247. })
  248. }
  249. return texid
  250. }
  251. // Process the given material into the material and effect libraries
  252. // Returns the material id
  253. function processMaterial(m) {
  254. let matid = materialMap.get(m)
  255. if (matid == null) {
  256. matid = `Mat${libraryEffects.length + 1}`
  257. let type = 'phong'
  258. if (m.isMeshLambertMaterial === true) {
  259. type = 'lambert'
  260. } else if (m.isMeshBasicMaterial === true) {
  261. type = 'constant'
  262. if (m.map !== null) {
  263. // The Collada spec does not support diffuse texture maps with the
  264. // constant shader type.
  265. // mrdoob/three.js#15469
  266. console.warn('ColladaExporter: Texture maps not supported with MeshBasicMaterial.')
  267. }
  268. }
  269. const emissive = m.emissive ? m.emissive : new Color(0, 0, 0)
  270. const diffuse = m.color ? m.color : new Color(0, 0, 0)
  271. const specular = m.specular ? m.specular : new Color(1, 1, 1)
  272. const shininess = m.shininess || 0
  273. const reflectivity = m.reflectivity || 0
  274. emissive.convertLinearToSRGB()
  275. specular.convertLinearToSRGB()
  276. diffuse.convertLinearToSRGB()
  277. // Do not export and alpha map for the reasons mentioned in issue (#13792)
  278. // in three.js alpha maps are black and white, but collada expects the alpha
  279. // channel to specify the transparency
  280. let transparencyNode = ''
  281. if (m.transparent === true) {
  282. transparencyNode += '<transparent>' + (m.map ? '<texture texture="diffuse-sampler"></texture>' : '<float>1</float>') + '</transparent>'
  283. if (m.opacity < 1) {
  284. transparencyNode += `<transparency><float>${m.opacity}</float></transparency>`
  285. }
  286. }
  287. const techniqueNode =
  288. `<technique sid="common"><${type}>` +
  289. '<emission>' +
  290. (m.emissiveMap ? '<texture texture="emissive-sampler" texcoord="TEXCOORD" />' : `<color sid="emission">${emissive.r} ${emissive.g} ${emissive.b} 1</color>`) +
  291. '</emission>' +
  292. (type !== 'constant'
  293. ? '<diffuse>' + (m.map ? '<texture texture="diffuse-sampler" texcoord="TEXCOORD" />' : `<color sid="diffuse">${diffuse.r} ${diffuse.g} ${diffuse.b} 1</color>`) + '</diffuse>'
  294. : '') +
  295. (type !== 'constant' ? '<bump>' + (m.normalMap ? '<texture texture="bump-sampler" texcoord="TEXCOORD" />' : '') + '</bump>' : '') +
  296. (type === 'phong'
  297. ? `<specular><color sid="specular">${specular.r} ${specular.g} ${specular.b} 1</color></specular>` +
  298. '<shininess>' +
  299. (m.specularMap ? '<texture texture="specular-sampler" texcoord="TEXCOORD" />' : `<float sid="shininess">${shininess}</float>`) +
  300. '</shininess>'
  301. : '') +
  302. `<reflective><color>${diffuse.r} ${diffuse.g} ${diffuse.b} 1</color></reflective>` +
  303. `<reflectivity><float>${reflectivity}</float></reflectivity>` +
  304. transparencyNode +
  305. `</${type}></technique>`
  306. const effectnode =
  307. `<effect id="${matid}-effect">` +
  308. '<profile_COMMON>' +
  309. (m.map
  310. ? '<newparam sid="diffuse-surface"><surface type="2D">' +
  311. `<init_from>${processTexture(m.map)}</init_from>` +
  312. '</surface></newparam>' +
  313. '<newparam sid="diffuse-sampler"><sampler2D><source>diffuse-surface</source></sampler2D></newparam>'
  314. : '') +
  315. (m.specularMap
  316. ? '<newparam sid="specular-surface"><surface type="2D">' +
  317. `<init_from>${processTexture(m.specularMap)}</init_from>` +
  318. '</surface></newparam>' +
  319. '<newparam sid="specular-sampler"><sampler2D><source>specular-surface</source></sampler2D></newparam>'
  320. : '') +
  321. (m.emissiveMap
  322. ? '<newparam sid="emissive-surface"><surface type="2D">' +
  323. `<init_from>${processTexture(m.emissiveMap)}</init_from>` +
  324. '</surface></newparam>' +
  325. '<newparam sid="emissive-sampler"><sampler2D><source>emissive-surface</source></sampler2D></newparam>'
  326. : '') +
  327. (m.normalMap
  328. ? '<newparam sid="bump-surface"><surface type="2D">' +
  329. `<init_from>${processTexture(m.normalMap)}</init_from>` +
  330. '</surface></newparam>' +
  331. '<newparam sid="bump-sampler"><sampler2D><source>bump-surface</source></sampler2D></newparam>'
  332. : '') +
  333. techniqueNode +
  334. (m.side === DoubleSide ? '<extra><technique profile="THREEJS"><double_sided sid="double_sided" type="int">1</double_sided></technique></extra>' : '') +
  335. '</profile_COMMON>' +
  336. '</effect>'
  337. const materialName = m.name ? ` name="${m.name}"` : ''
  338. const materialNode = `<material id="${matid}"${materialName}><instance_effect url="#${matid}-effect" /></material>`
  339. libraryMaterials.push(materialNode)
  340. libraryEffects.push(effectnode)
  341. materialMap.set(m, matid)
  342. }
  343. return matid
  344. }
  345. // Recursively process the object into a scene
  346. function processObject(o) {
  347. let node = `<node name="${o.name}">`
  348. node += getTransform(o)
  349. if (o.isMesh === true && o.geometry !== null) {
  350. // function returns the id associated with the mesh and a "BufferGeometry" version
  351. // of the geometry in case it's not a geometry.
  352. const geomInfo = processGeometry(o.geometry)
  353. const meshid = geomInfo.meshid
  354. const geometry = geomInfo.bufferGeometry
  355. // ids of the materials to bind to the geometry
  356. let matids = null
  357. let matidsArray
  358. // get a list of materials to bind to the sub groups of the geometry.
  359. // If the amount of subgroups is greater than the materials, than reuse
  360. // the materials.
  361. const mat = o.material || new MeshBasicMaterial()
  362. const materials = Array.isArray(mat) ? mat : [mat]
  363. if (geometry.groups.length > materials.length) {
  364. matidsArray = new Array(geometry.groups.length)
  365. } else {
  366. matidsArray = new Array(materials.length)
  367. }
  368. matids = matidsArray.fill().map((v, i) => processMaterial(materials[i % materials.length]))
  369. node +=
  370. `<instance_geometry url="#${meshid}">` +
  371. (matids.length > 0
  372. ? '<bind_material><technique_common>' +
  373. matids
  374. .map(
  375. (id, i) =>
  376. `<instance_material symbol="MESH_MATERIAL_${i}" target="#${id}" >` +
  377. '<bind_vertex_input semantic="TEXCOORD" input_semantic="TEXCOORD" input_set="0" />' +
  378. '</instance_material>'
  379. )
  380. .join('') +
  381. '</technique_common></bind_material>'
  382. : '') +
  383. '</instance_geometry>'
  384. }
  385. o.children.forEach(c => (node += processObject(c)))
  386. node += '</node>'
  387. return node
  388. }
  389. const geometryInfo = new WeakMap()
  390. const materialMap = new WeakMap()
  391. const imageMap = new WeakMap()
  392. const textures = []
  393. const libraryImages = []
  394. const libraryGeometries = []
  395. const libraryEffects = []
  396. const libraryMaterials = []
  397. const libraryVisualScenes = processObject(object)
  398. const specLink = version === '1.4.1' ? 'http://www.collada.org/2005/11/COLLADASchema' : 'https://www.khronos.org/collada/'
  399. let dae =
  400. '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' +
  401. `<COLLADA xmlns="${specLink}" version="${version}">` +
  402. '<asset>' +
  403. ('<contributor>' +
  404. '<authoring_tool>three.js Collada Exporter</authoring_tool>' +
  405. (options.author !== null ? `<author>${options.author}</author>` : '') +
  406. '</contributor>' +
  407. `<created>${new Date().toISOString()}</created>` +
  408. `<modified>${new Date().toISOString()}</modified>` +
  409. (options.unitName !== null ? `<unit name="${options.unitName}" meter="${options.unitMeter}" />` : '') +
  410. `<up_axis>${options.upAxis}</up_axis>`) +
  411. '</asset>'
  412. dae += `<library_images>${libraryImages.join('')}</library_images>`
  413. dae += `<library_effects>${libraryEffects.join('')}</library_effects>`
  414. dae += `<library_materials>${libraryMaterials.join('')}</library_materials>`
  415. dae += `<library_geometries>${libraryGeometries.join('')}</library_geometries>`
  416. dae += `<library_visual_scenes><visual_scene id="Scene" name="scene">${libraryVisualScenes}</visual_scene></library_visual_scenes>`
  417. dae += '<scene><instance_visual_scene url="#Scene"/></scene>'
  418. dae += '</COLLADA>'
  419. const res = {
  420. data: format(dae),
  421. textures
  422. }
  423. if (typeof onDone === 'function') {
  424. requestAnimationFrame(() => onDone(res))
  425. }
  426. return res
  427. }
  428. }
  429. export { ColladaExporter }