GLTFExporter.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /**
  2. * @author fernandojsg / http://fernandojsg.com
  3. */
  4. //------------------------------------------------------------------------------
  5. // GLTF Exporter
  6. //------------------------------------------------------------------------------
  7. THREE.GLTFExporter = function ( renderer ) {
  8. this.renderer = renderer;
  9. };
  10. THREE.GLTFExporter.prototype = {
  11. constructor: THREE.GLTFExporter,
  12. /**
  13. * Parse scenes and generate GLTF output
  14. * @param {THREE.Scene or [THREE.Scenes]} input THREE.Scene or Array of THREE.Scenes
  15. * @param {[type]} onDone Callback on completed
  16. * @param {[type]} options options
  17. * trs: Exports position, rotation and scale instead of matrix
  18. */
  19. parse: function ( input, onDone, options ) {
  20. options = options || {};
  21. var glUtils = new THREE.WebGLUtils( this.renderer.context, this.renderer.extensions );
  22. var gl = this.renderer.context;
  23. var outputJSON = {
  24. asset: {
  25. version: "2.0",
  26. generator: "THREE.JS GLTFExporter" // @QUESTION Does it support spaces?
  27. }
  28. };
  29. var byteOffset = 0;
  30. var dataViews = [];
  31. /**
  32. * Compare two arrays
  33. */
  34. function sameArray ( array1, array2 ) {
  35. return ( array1.length === array2.length ) && array1.every( function( element, index ) {
  36. return element === array2[ index ];
  37. });
  38. }
  39. /**
  40. * Get the min and he max vectors from the given attribute
  41. * @param {THREE.WebGLAttribute} attribute Attribute to find the min/max
  42. * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components)
  43. */
  44. function getMinMax ( attribute ) {
  45. var output = {
  46. min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ),
  47. max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY )
  48. };
  49. for ( var i = 0; i < attribute.count; i++ ) {
  50. for ( var a = 0; a < attribute.itemSize; a++ ) {
  51. var value = attribute.array[ i * attribute.itemSize + a ];
  52. output.min[ a ] = Math.min( output.min[ a ], value );
  53. output.max[ a ] = Math.max( output.max[ a ], value );
  54. }
  55. }
  56. return output;
  57. }
  58. /**
  59. * Add extension to the extensions array
  60. * @param {String} extensionName Extension name
  61. */
  62. function addExtension ( extensionName ) {
  63. if ( !outputJSON.extensionsUsed ) {
  64. outputJSON.extensionsUsed = [];
  65. }
  66. if ( outputJSON.extensionsUsed.indexOf( extensionName ) !== -1 ) {
  67. outputJSON.extensionsUsed.push( extensionName );
  68. }
  69. }
  70. /**
  71. * Process a buffer to append to the default one.
  72. * @param {THREE.BufferAttribute} attribute Attribute to store
  73. * @param {Integer} componentType Component type (Unsigned short, unsigned int or float)
  74. * @return {Integer} Index of the buffer created (Currently always 0)
  75. */
  76. function processBuffer ( attribute, componentType ) {
  77. if ( !outputJSON.buffers ) {
  78. outputJSON.buffers = [
  79. {
  80. byteLength: 0,
  81. uri: ''
  82. }
  83. ];
  84. }
  85. // Create a new dataview and dump the attribute's array into it
  86. var dataView = new DataView( new ArrayBuffer( attribute.array.byteLength ) );
  87. var offset = 0;
  88. var offsetInc = componentType === gl.UNSIGNED_SHORT ? 2 : 4;
  89. for ( var i = 0; i < attribute.count; i++ ) {
  90. for (var a = 0; a < attribute.itemSize; a++ ) {
  91. var value = attribute.array[ i * attribute.itemSize + a ];
  92. if ( componentType === gl.FLOAT ) {
  93. dataView.setFloat32( offset, value, true );
  94. } else if ( componentType === gl.UNSIGNED_INT ) {
  95. dataView.setUint8( offset, value, true );
  96. } else if ( componentType === gl.UNSIGNED_SHORT ) {
  97. dataView.setUint16( offset, value, true );
  98. }
  99. offset += offsetInc;
  100. }
  101. }
  102. // We just use one buffer
  103. dataViews.push( dataView );
  104. return 0;
  105. }
  106. /**
  107. * Process and generate a BufferView
  108. * @param {[type]} data [description]
  109. * @return {[type]} [description]
  110. */
  111. function processBufferView ( data, componentType ) {
  112. var isVertexAttributes = componentType === gl.FLOAT;
  113. if ( !outputJSON.bufferViews ) {
  114. outputJSON.bufferViews = [];
  115. }
  116. var gltfBufferView = {
  117. buffer: processBuffer( data, componentType ),
  118. byteOffset: byteOffset,
  119. byteLength: data.array.byteLength,
  120. byteStride: data.itemSize * ( componentType === gl.UNSIGNED_SHORT ? 2 : 4 ),
  121. target: isVertexAttributes ? gl.ARRAY_BUFFER : gl.ELEMENT_ARRAY_BUFFER
  122. };
  123. byteOffset += data.array.byteLength;
  124. outputJSON.bufferViews.push( gltfBufferView );
  125. // @TODO Ideally we'll have just two bufferviews: 0 is for vertex attributes, 1 for indices
  126. var output = {
  127. id: outputJSON.bufferViews.length - 1,
  128. byteLength: 0
  129. };
  130. return output;
  131. }
  132. /**
  133. * Process attribute to generate an accessor
  134. * @param {THREE.WebGLAttribute} attribute Attribute to process
  135. * @return {Integer} Index of the processed accessor on the "accessors" array
  136. */
  137. function processAccessor ( attribute ) {
  138. if ( !outputJSON.accessors ) {
  139. outputJSON.accessors = [];
  140. }
  141. var types = [
  142. 'SCALAR',
  143. 'VEC2',
  144. 'VEC3',
  145. 'VEC4'
  146. ];
  147. // Detect the component type of the attribute array (float, uint or ushort)
  148. var componentType = attribute instanceof THREE.Float32BufferAttribute ? gl.FLOAT :
  149. ( attribute instanceof THREE.Uint32BufferAttribute ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT );
  150. var minMax = getMinMax( attribute );
  151. var bufferView = processBufferView( attribute, componentType );
  152. var gltfAccessor = {
  153. bufferView: bufferView.id,
  154. byteOffset: bufferView.byteOffset,
  155. componentType: componentType,
  156. count: attribute.count,
  157. max: minMax.max,
  158. min: minMax.min,
  159. type: types[ attribute.itemSize - 1 ]
  160. };
  161. outputJSON.accessors.push( gltfAccessor );
  162. return outputJSON.accessors.length - 1;
  163. }
  164. /**
  165. * Process image
  166. * @param {Texture} map Texture to process
  167. * @return {Integer} Index of the processed texture in the "images" array
  168. */
  169. function processImage ( map ) {
  170. if ( !outputJSON.images ) {
  171. outputJSON.images = [];
  172. }
  173. var gltfImage = {};
  174. if ( options.embedImages ) {
  175. // @TODO { bufferView, mimeType }
  176. } else {
  177. // @TODO base64 based on options
  178. gltfImage.uri = map.image.src;
  179. }
  180. outputJSON.images.push( gltfImage );
  181. return outputJSON.images.length - 1;
  182. }
  183. /**
  184. * Process sampler
  185. * @param {Texture} map Texture to process
  186. * @return {Integer} Index of the processed texture in the "samplers" array
  187. */
  188. function processSampler ( map ) {
  189. if ( !outputJSON.samplers ) {
  190. outputJSON.samplers = [];
  191. }
  192. var gltfSampler = {
  193. magFilter: glUtils.convert( map.magFilter ),
  194. minFilter: glUtils.convert( map.minFilter ),
  195. wrapS: glUtils.convert( map.wrapS ),
  196. wrapT: glUtils.convert( map.wrapT )
  197. };
  198. outputJSON.samplers.push( gltfSampler );
  199. return outputJSON.samplers.length - 1;
  200. }
  201. /**
  202. * Process texture
  203. * @param {Texture} map Map to process
  204. * @return {Integer} Index of the processed texture in the "textures" array
  205. */
  206. function processTexture ( map ) {
  207. if (!outputJSON.textures) {
  208. outputJSON.textures = [];
  209. }
  210. var gltfTexture = {
  211. sampler: processSampler( map ),
  212. source: processImage( map )
  213. };
  214. outputJSON.textures.push( gltfTexture );
  215. return outputJSON.textures.length - 1;
  216. }
  217. /**
  218. * Process material
  219. * @param {THREE.Material} material Material to process
  220. * @return {Integer} Index of the processed material in the "materials" array
  221. */
  222. function processMaterial ( material ) {
  223. if ( !outputJSON.materials ) {
  224. outputJSON.materials = [];
  225. }
  226. if ( !material instanceof THREE.MeshStandardMaterial ) {
  227. throw 'Currently just support THREE.StandardMaterial is supported';
  228. }
  229. // @QUESTION Should we avoid including any attribute that has the default value?
  230. var gltfMaterial = {
  231. pbrMetallicRoughness: {
  232. baseColorFactor: material.color.toArray().concat( [ material.opacity ] ),
  233. metallicFactor: material.metalness,
  234. roughnessFactor: material.roughness
  235. }
  236. };
  237. if ( material.map ) {
  238. gltfMaterial.pbrMetallicRoughness.baseColorTexture = {
  239. index: processTexture( material.map ),
  240. texCoord: 0 // @FIXME
  241. }
  242. }
  243. if ( material.side === THREE.DoubleSide ) {
  244. gltfMaterial.doubleSided = true;
  245. }
  246. if ( material.name ) {
  247. gltfMaterial.name = material.name;
  248. }
  249. outputJSON.materials.push( gltfMaterial );
  250. return outputJSON.materials.length - 1;
  251. }
  252. /**
  253. * Process mesh
  254. * @param {THREE.Mesh} mesh Mesh to process
  255. * @return {Integer} Index of the processed mesh in the "meshes" array
  256. */
  257. function processMesh( mesh ) {
  258. if ( !outputJSON.meshes ) {
  259. outputJSON.meshes = [];
  260. }
  261. var geometry = mesh.geometry;
  262. // @FIXME Select the correct mode based on the mesh
  263. var mode = gl.TRIANGLES;
  264. var gltfMesh = {
  265. primitives: [
  266. {
  267. mode: mode,
  268. attributes: {},
  269. material: processMaterial( mesh.material ),
  270. indices: processAccessor( geometry.index )
  271. }
  272. ]
  273. };
  274. // We've just one primitive per mesh
  275. var gltfAttributes = gltfMesh.primitives[ 0 ].attributes;
  276. var attributes = geometry.attributes;
  277. // Conversion between attributes names in threejs and gltf spec
  278. var nameConversion = {
  279. uv: 'TEXCOORD_0',
  280. uv2: 'TEXCOORD_1',
  281. color: 'COLOR_0'
  282. };
  283. // For every attribute create an accessor
  284. for ( attributeName in geometry.attributes ) {
  285. var attribute = geometry.attributes[ attributeName ];
  286. attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase()
  287. gltfAttributes[ attributeName ] = processAccessor( attribute );
  288. }
  289. // @todo Not really necessary, isn't it?
  290. if ( geometry.type ) {
  291. gltfMesh.name = geometry.type;
  292. }
  293. outputJSON.meshes.push( gltfMesh );
  294. return outputJSON.meshes.length - 1;
  295. }
  296. /**
  297. * Process camera
  298. * @param {THREE.Camera} camera Camera to process
  299. * @return {Integer} Index of the processed mesh in the "camera" array
  300. */
  301. function processCamera( camera ) {
  302. if ( !outputJSON.cameras ) {
  303. outputJSON.cameras = [];
  304. }
  305. var isOrtho = camera instanceof THREE.OrthographicCamera;
  306. var gltfCamera = {
  307. type: isOrtho ? 'orthographic' : 'perspective'
  308. };
  309. if ( isOrtho ) {
  310. gltfCamera.orthographic = {
  311. xmag: camera.right * 2,
  312. ymag: camera.top * 2,
  313. zfar: camera.far,
  314. znear: camera.near
  315. }
  316. } else {
  317. gltfCamera.perspective = {
  318. aspectRatio: camera.aspect,
  319. yfov: THREE.Math.degToRad( camera.fov ) / camera.aspect,
  320. zfar: camera.far,
  321. znear: camera.near
  322. };
  323. }
  324. if ( camera.name ) {
  325. gltfCamera.name = camera.type;
  326. }
  327. outputJSON.cameras.push( gltfCamera );
  328. return outputJSON.cameras.length - 1;
  329. }
  330. /**
  331. * Process Object3D node
  332. * @param {THREE.Object3D} node Object3D to processNode
  333. * @return {Integer} Index of the node in the nodes list
  334. */
  335. function processNode ( object ) {
  336. if ( !outputJSON.nodes ) {
  337. outputJSON.nodes = [];
  338. }
  339. var gltfNode = {};
  340. if ( options.trs ) {
  341. var rotation = object.quaternion.toArray();
  342. var position = object.position.toArray();
  343. var scale = object.scale.toArray();
  344. if ( !sameArray( rotation, [ 0, 0, 0, 1 ] ) ) {
  345. gltfNode.rotation = rotation;
  346. }
  347. if ( !sameArray( position, [ 0, 0, 0 ] ) ) {
  348. gltfNode.position = position;
  349. }
  350. if ( !sameArray( scale, [ 1, 1, 1 ] ) ) {
  351. gltfNode.scale = scale;
  352. }
  353. } else {
  354. object.updateMatrix();
  355. if (! sameArray( object.matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] ) ) {
  356. gltfNode.matrix = object.matrix.elements;
  357. }
  358. };
  359. if ( object.name ) {
  360. gltfNode.name = object.name;
  361. }
  362. if ( object instanceof THREE.Mesh ) {
  363. gltfNode.mesh = processMesh( object );
  364. } else if ( object instanceof THREE.Camera ) {
  365. gltfNode.camera = processCamera( object );
  366. }
  367. if ( object.children.length > 0 ) {
  368. gltfNode.children = [];
  369. for ( var i = 0, l = object.children.length; i < l; i ++ ) {
  370. var child = object.children[ i ];
  371. if ( child instanceof THREE.Mesh ) {
  372. gltfNode.children.push( processNode( child ) );
  373. }
  374. }
  375. }
  376. outputJSON.nodes.push( gltfNode );
  377. return outputJSON.nodes.length - 1;
  378. }
  379. /**
  380. * Process Scene
  381. * @param {THREE.Scene} node Scene to process
  382. */
  383. function processScene( scene ) {
  384. if ( !outputJSON.scenes ) {
  385. outputJSON.scenes = [];
  386. outputJSON.scene = 0;
  387. }
  388. var gltfScene = {
  389. nodes: []
  390. };
  391. if ( scene.name ) {
  392. gltfScene.name = scene.name;
  393. }
  394. outputJSON.scenes.push( gltfScene );
  395. for ( var i = 0, l = scene.children.length; i < l; i ++ ) {
  396. var child = scene.children[ i ];
  397. // @TODO Right now we just process meshes and lights
  398. if ( child instanceof THREE.Mesh || child instanceof THREE.Camera ) {
  399. gltfScene.nodes.push( processNode( child ) );
  400. }
  401. }
  402. }
  403. // Process the scene/s
  404. if ( input instanceof Array ) {
  405. for ( i = 0; i < input.length; i++ ) {
  406. processScene( input[ i ] );
  407. }
  408. } else {
  409. processScene( input );
  410. }
  411. // Generate buffer
  412. // Create a new blob with all the dataviews from the buffers
  413. var blob = new Blob( dataViews, { type: 'application/octet-binary' } );
  414. // Update the bytlength of the only main buffer and update the uri with the base64 representation of it
  415. outputJSON.buffers[ 0 ].byteLength = blob.size;
  416. objectURL = URL.createObjectURL( blob );
  417. var reader = new window.FileReader();
  418. reader.readAsDataURL( blob );
  419. reader.onloadend = function() {
  420. base64data = reader.result;
  421. outputJSON.buffers[ 0 ].uri = base64data;
  422. onDone( outputJSON );
  423. }
  424. }
  425. };