From 80601cbeeece26efdef9bafacdff272d78196c27 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 2 Oct 2019 12:12:08 -0500 Subject: [PATCH 01/16] =?UTF-8?q?SG:=20Add=20"Export=20GLTF=E2=80=A6"=20me?= =?UTF-8?q?nu=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vendor GLTFExporter from three r107 - add useExportToGltf hook --- src/js/menu.js | 6 + src/js/shot-generator/Editor.js | 4 + src/js/shot-generator/use-export-to-gltf.js | 77 + .../examples/js/exporters/GLTFExporter.js | 2257 +++++++++++++++++ src/js/windows/shot-generator/main.js | 3 + 5 files changed, 2347 insertions(+) create mode 100644 src/js/shot-generator/use-export-to-gltf.js create mode 100644 src/js/vendor/three/examples/js/exporters/GLTFExporter.js diff --git a/src/js/menu.js b/src/js/menu.js index daa26904b5..e02e619306 100644 --- a/src/js/menu.js +++ b/src/js/menu.js @@ -856,6 +856,12 @@ const shotGeneratorMenu = [ click (item, focusedWindow, event) { ipcRenderer.send('openDialogue') } + }, + { + label: 'Export GLTF…', + click (item, focusedWindow, event) { + ipcRenderer.send('shot-generator:export-gltf') + } } ] }, diff --git a/src/js/shot-generator/Editor.js b/src/js/shot-generator/Editor.js index 10c2e78eba..3481692f7f 100644 --- a/src/js/shot-generator/Editor.js +++ b/src/js/shot-generator/Editor.js @@ -35,6 +35,8 @@ const Icon = require('./Icon') const Toolbar = require('./Toolbar') const FatalErrorBoundary = require('./FatalErrorBoundary') +const useExportToGltf = require('./use-export-to-gltf') + const ModelLoader = require('../services/model-loader') const h = require('../utils/h') @@ -329,6 +331,8 @@ const Editor = connect( } }, []) + useExportToGltf(scene) + // render Toolbar with updated camera when scene is ready, or when activeCamera changes useEffect(() => { setCamera(scene.current.children.find(o => o.userData.id === activeCamera)) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js new file mode 100644 index 0000000000..d52ed5a6ae --- /dev/null +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -0,0 +1,77 @@ +const { useEffect } = React +const { useSelector } = require('react-redux') +const { ipcRenderer, shell } = require('electron') +const path = require('path') +const fs = require('fs-extra') +const moment = require('moment') + +const THREE = require('three') +window.THREE = window.THREE || THREE +require('../vendor/three/examples/js/exporters/GLTFExporter.js') + +const notifications = require('../window/notifications') + +const useExportToGltf = (sceneRef) => { + const meta = useSelector(state => state.meta) + const board = useSelector(state => state.board) + + useEffect(() => { + if (board && meta && meta.storyboarderFilePath) { + ipcRenderer.on('shot-generator:export-gltf', () => { + notifications.notify({ + message: 'Preparing to export GLTF…', + timing: 5 + }) + + console.log('Preparing GLTF…') + let scene = new THREE.Scene() + for (let child of sceneRef.current.children) { + console.log('\tScene contains:', child) + // HACK test to avoid IconSprites, which fail to .clone + if (!child.icon) { + // for now, just add the Groups + if (child.type === 'Group') { + console.log('\tAdding to GLTF:', child) + scene.add(child.clone()) + } + } + } + + let exporter = new THREE.GLTFExporter() + let options = { + binary: true, + embedImages: true + } + exporter.parse(scene, function (glb) { + if (meta.storyboarderFilePath) { + + let timestamp = moment().format('YYYY-MM-DD hh.mm.ss') + let filename = `${board.url.replace('.png', '')}-${timestamp}.glb` + let filepath = path.join( + path.dirname(meta.storyboarderFilePath), + 'exports', + filename + ) + + fs.ensureDirSync(path.dirname(filepath)) + fs.writeFileSync(filepath, glb) + + notifications.notify({ + message: `Exported to:\n${filename}`, + timing: 5 + }) + + shell.showItemInFolder(filepath) + } + }, options) + }) + } + + return function cleanup() { + console.log('cleanup shot-generator:export-gltf') + ipcRenderer.removeAllListeners('shot-generator:export-gltf') + } + }, [board, meta]) +} + +module.exports = useExportToGltf diff --git a/src/js/vendor/three/examples/js/exporters/GLTFExporter.js b/src/js/vendor/three/examples/js/exporters/GLTFExporter.js new file mode 100644 index 0000000000..49af98dcb6 --- /dev/null +++ b/src/js/vendor/three/examples/js/exporters/GLTFExporter.js @@ -0,0 +1,2257 @@ +/** + * @author fernandojsg / http://fernandojsg.com + * @author Don McCurdy / https://www.donmccurdy.com + * @author Takahiro / https://github.com/takahirox + */ + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ +var WEBGL_CONSTANTS = { + POINTS: 0x0000, + LINES: 0x0001, + LINE_LOOP: 0x0002, + LINE_STRIP: 0x0003, + TRIANGLES: 0x0004, + TRIANGLE_STRIP: 0x0005, + TRIANGLE_FAN: 0x0006, + + UNSIGNED_BYTE: 0x1401, + UNSIGNED_SHORT: 0x1403, + FLOAT: 0x1406, + UNSIGNED_INT: 0x1405, + ARRAY_BUFFER: 0x8892, + ELEMENT_ARRAY_BUFFER: 0x8893, + + NEAREST: 0x2600, + LINEAR: 0x2601, + NEAREST_MIPMAP_NEAREST: 0x2700, + LINEAR_MIPMAP_NEAREST: 0x2701, + NEAREST_MIPMAP_LINEAR: 0x2702, + LINEAR_MIPMAP_LINEAR: 0x2703, + + CLAMP_TO_EDGE: 33071, + MIRRORED_REPEAT: 33648, + REPEAT: 10497 +}; + +var THREE_TO_WEBGL = {}; + +THREE_TO_WEBGL[ THREE.NearestFilter ] = WEBGL_CONSTANTS.NEAREST; +THREE_TO_WEBGL[ THREE.NearestMipmapNearestFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST; +THREE_TO_WEBGL[ THREE.NearestMipmapLinearFilter ] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR; +THREE_TO_WEBGL[ THREE.LinearFilter ] = WEBGL_CONSTANTS.LINEAR; +THREE_TO_WEBGL[ THREE.LinearMipmapNearestFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST; +THREE_TO_WEBGL[ THREE.LinearMipmapLinearFilter ] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR; + +THREE_TO_WEBGL[ THREE.ClampToEdgeWrapping ] = WEBGL_CONSTANTS.CLAMP_TO_EDGE; +THREE_TO_WEBGL[ THREE.RepeatWrapping ] = WEBGL_CONSTANTS.REPEAT; +THREE_TO_WEBGL[ THREE.MirroredRepeatWrapping ] = WEBGL_CONSTANTS.MIRRORED_REPEAT; + +var PATH_PROPERTIES = { + scale: 'scale', + position: 'translation', + quaternion: 'rotation', + morphTargetInfluences: 'weights' +}; + +//------------------------------------------------------------------------------ +// GLTF Exporter +//------------------------------------------------------------------------------ +THREE.GLTFExporter = function () {}; + +THREE.GLTFExporter.prototype = { + + constructor: THREE.GLTFExporter, + + /** + * Parse scenes and generate GLTF output + * @param {THREE.Scene or [THREE.Scenes]} input THREE.Scene or Array of THREE.Scenes + * @param {Function} onDone Callback on completed + * @param {Object} options options + */ + parse: function ( input, onDone, options ) { + + var DEFAULT_OPTIONS = { + binary: false, + trs: false, + onlyVisible: true, + truncateDrawRange: true, + embedImages: true, + animations: [], + forceIndices: false, + forcePowerOfTwoTextures: false, + includeCustomExtensions: false + }; + + options = Object.assign( {}, DEFAULT_OPTIONS, options ); + + if ( options.animations.length > 0 ) { + + // Only TRS properties, and not matrices, may be targeted by animation. + options.trs = true; + + } + + var outputJSON = { + + asset: { + + version: "2.0", + generator: "THREE.GLTFExporter" + + } + + }; + + var byteOffset = 0; + var buffers = []; + var pending = []; + var nodeMap = new Map(); + var skins = []; + var extensionsUsed = {}; + var cachedData = { + + meshes: new Map(), + attributes: new Map(), + attributesNormalized: new Map(), + materials: new Map(), + textures: new Map(), + images: new Map() + + }; + + var cachedCanvas; + + var uids = new Map(); + var uid = 0; + + /** + * Assign and return a temporal unique id for an object + * especially which doesn't have .uuid + * @param {Object} object + * @return {Integer} + */ + function getUID( object ) { + + if ( ! uids.has( object ) ) uids.set( object, uid ++ ); + + return uids.get( object ); + + } + + /** + * Compare two arrays + * @param {Array} array1 Array 1 to compare + * @param {Array} array2 Array 2 to compare + * @return {Boolean} Returns true if both arrays are equal + */ + function equalArray( array1, array2 ) { + + return ( array1.length === array2.length ) && array1.every( function ( element, index ) { + + return element === array2[ index ]; + + } ); + + } + + /** + * Converts a string to an ArrayBuffer. + * @param {string} text + * @return {ArrayBuffer} + */ + function stringToArrayBuffer( text ) { + + if ( window.TextEncoder !== undefined ) { + + return new TextEncoder().encode( text ).buffer; + + } + + var array = new Uint8Array( new ArrayBuffer( text.length ) ); + + for ( var i = 0, il = text.length; i < il; i ++ ) { + + var value = text.charCodeAt( i ); + + // Replacing multi-byte character with space(0x20). + array[ i ] = value > 0xFF ? 0x20 : value; + + } + + return array.buffer; + + } + + /** + * Get the min and max vectors from the given attribute + * @param {THREE.BufferAttribute} attribute Attribute to find the min/max in range from start to start + count + * @param {Integer} start + * @param {Integer} count + * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components) + */ + function getMinMax( attribute, start, count ) { + + var output = { + + min: new Array( attribute.itemSize ).fill( Number.POSITIVE_INFINITY ), + max: new Array( attribute.itemSize ).fill( Number.NEGATIVE_INFINITY ) + + }; + + for ( var i = start; i < start + count; i ++ ) { + + for ( var a = 0; a < attribute.itemSize; a ++ ) { + + var value = attribute.array[ i * attribute.itemSize + a ]; + output.min[ a ] = Math.min( output.min[ a ], value ); + output.max[ a ] = Math.max( output.max[ a ], value ); + + } + + } + + return output; + + } + + /** + * Checks if image size is POT. + * + * @param {Image} image The image to be checked. + * @returns {Boolean} Returns true if image size is POT. + * + */ + function isPowerOfTwo( image ) { + + return THREE.Math.isPowerOfTwo( image.width ) && THREE.Math.isPowerOfTwo( image.height ); + + } + + /** + * Checks if normal attribute values are normalized. + * + * @param {THREE.BufferAttribute} normal + * @returns {Boolean} + * + */ + function isNormalizedNormalAttribute( normal ) { + + if ( cachedData.attributesNormalized.has( normal ) ) { + + return false; + + } + + var v = new THREE.Vector3(); + + for ( var i = 0, il = normal.count; i < il; i ++ ) { + + // 0.0005 is from glTF-validator + if ( Math.abs( v.fromArray( normal.array, i * 3 ).length() - 1.0 ) > 0.0005 ) return false; + + } + + return true; + + } + + /** + * Creates normalized normal buffer attribute. + * + * @param {THREE.BufferAttribute} normal + * @returns {THREE.BufferAttribute} + * + */ + function createNormalizedNormalAttribute( normal ) { + + if ( cachedData.attributesNormalized.has( normal ) ) { + + return cachedData.attributesNormalized.get( normal ); + + } + + var attribute = normal.clone(); + + var v = new THREE.Vector3(); + + for ( var i = 0, il = attribute.count; i < il; i ++ ) { + + v.fromArray( attribute.array, i * 3 ); + + if ( v.x === 0 && v.y === 0 && v.z === 0 ) { + + // if values can't be normalized set (1, 0, 0) + v.setX( 1.0 ); + + } else { + + v.normalize(); + + } + + v.toArray( attribute.array, i * 3 ); + + } + + cachedData.attributesNormalized.set( normal, attribute ); + + return attribute; + + } + + /** + * Get the required size + padding for a buffer, rounded to the next 4-byte boundary. + * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment + * + * @param {Integer} bufferSize The size the original buffer. + * @returns {Integer} new buffer size with required padding. + * + */ + function getPaddedBufferSize( bufferSize ) { + + return Math.ceil( bufferSize / 4 ) * 4; + + } + + /** + * Returns a buffer aligned to 4-byte boundary. + * + * @param {ArrayBuffer} arrayBuffer Buffer to pad + * @param {Integer} paddingByte (Optional) + * @returns {ArrayBuffer} The same buffer if it's already aligned to 4-byte boundary or a new buffer + */ + function getPaddedArrayBuffer( arrayBuffer, paddingByte ) { + + paddingByte = paddingByte || 0; + + var paddedLength = getPaddedBufferSize( arrayBuffer.byteLength ); + + if ( paddedLength !== arrayBuffer.byteLength ) { + + var array = new Uint8Array( paddedLength ); + array.set( new Uint8Array( arrayBuffer ) ); + + if ( paddingByte !== 0 ) { + + for ( var i = arrayBuffer.byteLength; i < paddedLength; i ++ ) { + + array[ i ] = paddingByte; + + } + + } + + return array.buffer; + + } + + return arrayBuffer; + + } + + /** + * Serializes a userData. + * + * @param {THREE.Object3D|THREE.Material} object + * @param {Object} gltfProperty + */ + function serializeUserData( object, gltfProperty ) { + + if ( Object.keys( object.userData ).length === 0 ) { + + return; + + } + + try { + + var json = JSON.parse( JSON.stringify( object.userData ) ); + + if ( options.includeCustomExtensions && json.gltfExtensions ) { + + if ( gltfProperty.extensions === undefined ) { + + gltfProperty.extensions = {}; + + } + + for ( var extensionName in json.gltfExtensions ) { + + gltfProperty.extensions[ extensionName ] = json.gltfExtensions[ extensionName ]; + extensionsUsed[ extensionName ] = true; + + } + + delete json.gltfExtensions; + + } + + if ( Object.keys( json ).length > 0 ) { + + gltfProperty.extras = json; + + } + + } catch ( error ) { + + console.warn( 'THREE.GLTFExporter: userData of \'' + object.name + '\' ' + + 'won\'t be serialized because of JSON.stringify error - ' + error.message ); + + } + + } + + /** + * Applies a texture transform, if present, to the map definition. Requires + * the KHR_texture_transform extension. + */ + function applyTextureTransform( mapDef, texture ) { + + var didTransform = false; + var transformDef = {}; + + if ( texture.offset.x !== 0 || texture.offset.y !== 0 ) { + + transformDef.offset = texture.offset.toArray(); + didTransform = true; + + } + + if ( texture.rotation !== 0 ) { + + transformDef.rotation = texture.rotation; + didTransform = true; + + } + + if ( texture.repeat.x !== 1 || texture.repeat.y !== 1 ) { + + transformDef.scale = texture.repeat.toArray(); + didTransform = true; + + } + + if ( didTransform ) { + + mapDef.extensions = mapDef.extensions || {}; + mapDef.extensions[ 'KHR_texture_transform' ] = transformDef; + extensionsUsed[ 'KHR_texture_transform' ] = true; + + } + + } + + /** + * Process a buffer to append to the default one. + * @param {ArrayBuffer} buffer + * @return {Integer} + */ + function processBuffer( buffer ) { + + if ( ! outputJSON.buffers ) { + + outputJSON.buffers = [ { byteLength: 0 } ]; + + } + + // All buffers are merged before export. + buffers.push( buffer ); + + return 0; + + } + + /** + * Process and generate a BufferView + * @param {THREE.BufferAttribute} attribute + * @param {number} componentType + * @param {number} start + * @param {number} count + * @param {number} target (Optional) Target usage of the BufferView + * @return {Object} + */ + function processBufferView( attribute, componentType, start, count, target ) { + + if ( ! outputJSON.bufferViews ) { + + outputJSON.bufferViews = []; + + } + + // Create a new dataview and dump the attribute's array into it + + var componentSize; + + if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) { + + componentSize = 1; + + } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) { + + componentSize = 2; + + } else { + + componentSize = 4; + + } + + var byteLength = getPaddedBufferSize( count * attribute.itemSize * componentSize ); + var dataView = new DataView( new ArrayBuffer( byteLength ) ); + var offset = 0; + + for ( var i = start; i < start + count; i ++ ) { + + for ( var a = 0; a < attribute.itemSize; a ++ ) { + + // @TODO Fails on InterleavedBufferAttribute, and could probably be + // optimized for normal BufferAttribute. + var value = attribute.array[ i * attribute.itemSize + a ]; + + if ( componentType === WEBGL_CONSTANTS.FLOAT ) { + + dataView.setFloat32( offset, value, true ); + + } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_INT ) { + + dataView.setUint32( offset, value, true ); + + } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT ) { + + dataView.setUint16( offset, value, true ); + + } else if ( componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE ) { + + dataView.setUint8( offset, value ); + + } + + offset += componentSize; + + } + + } + + var gltfBufferView = { + + buffer: processBuffer( dataView.buffer ), + byteOffset: byteOffset, + byteLength: byteLength + + }; + + if ( target !== undefined ) gltfBufferView.target = target; + + if ( target === WEBGL_CONSTANTS.ARRAY_BUFFER ) { + + // Only define byteStride for vertex attributes. + gltfBufferView.byteStride = attribute.itemSize * componentSize; + + } + + byteOffset += byteLength; + + outputJSON.bufferViews.push( gltfBufferView ); + + // @TODO Merge bufferViews where possible. + var output = { + + id: outputJSON.bufferViews.length - 1, + byteLength: 0 + + }; + + return output; + + } + + /** + * Process and generate a BufferView from an image Blob. + * @param {Blob} blob + * @return {Promise} + */ + function processBufferViewImage( blob ) { + + if ( ! outputJSON.bufferViews ) { + + outputJSON.bufferViews = []; + + } + + return new Promise( function ( resolve ) { + + var reader = new window.FileReader(); + reader.readAsArrayBuffer( blob ); + reader.onloadend = function () { + + var buffer = getPaddedArrayBuffer( reader.result ); + + var bufferView = { + buffer: processBuffer( buffer ), + byteOffset: byteOffset, + byteLength: buffer.byteLength + }; + + byteOffset += buffer.byteLength; + + outputJSON.bufferViews.push( bufferView ); + + resolve( outputJSON.bufferViews.length - 1 ); + + }; + + } ); + + } + + /** + * Process attribute to generate an accessor + * @param {THREE.BufferAttribute} attribute Attribute to process + * @param {THREE.BufferGeometry} geometry (Optional) Geometry used for truncated draw range + * @param {Integer} start (Optional) + * @param {Integer} count (Optional) + * @return {Integer} Index of the processed accessor on the "accessors" array + */ + function processAccessor( attribute, geometry, start, count ) { + + var types = { + + 1: 'SCALAR', + 2: 'VEC2', + 3: 'VEC3', + 4: 'VEC4', + 16: 'MAT4' + + }; + + var componentType; + + // Detect the component type of the attribute array (float, uint or ushort) + if ( attribute.array.constructor === Float32Array ) { + + componentType = WEBGL_CONSTANTS.FLOAT; + + } else if ( attribute.array.constructor === Uint32Array ) { + + componentType = WEBGL_CONSTANTS.UNSIGNED_INT; + + } else if ( attribute.array.constructor === Uint16Array ) { + + componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT; + + } else if ( attribute.array.constructor === Uint8Array ) { + + componentType = WEBGL_CONSTANTS.UNSIGNED_BYTE; + + } else { + + throw new Error( 'THREE.GLTFExporter: Unsupported bufferAttribute component type.' ); + + } + + if ( start === undefined ) start = 0; + if ( count === undefined ) count = attribute.count; + + // @TODO Indexed buffer geometry with drawRange not supported yet + if ( options.truncateDrawRange && geometry !== undefined && geometry.index === null ) { + + var end = start + count; + var end2 = geometry.drawRange.count === Infinity + ? attribute.count + : geometry.drawRange.start + geometry.drawRange.count; + + start = Math.max( start, geometry.drawRange.start ); + count = Math.min( end, end2 ) - start; + + if ( count < 0 ) count = 0; + + } + + // Skip creating an accessor if the attribute doesn't have data to export + if ( count === 0 ) { + + return null; + + } + + var minMax = getMinMax( attribute, start, count ); + + var bufferViewTarget; + + // If geometry isn't provided, don't infer the target usage of the bufferView. For + // animation samplers, target must not be set. + if ( geometry !== undefined ) { + + bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER; + + } + + var bufferView = processBufferView( attribute, componentType, start, count, bufferViewTarget ); + + var gltfAccessor = { + + bufferView: bufferView.id, + byteOffset: bufferView.byteOffset, + componentType: componentType, + count: count, + max: minMax.max, + min: minMax.min, + type: types[ attribute.itemSize ] + + }; + + if ( ! outputJSON.accessors ) { + + outputJSON.accessors = []; + + } + + outputJSON.accessors.push( gltfAccessor ); + + return outputJSON.accessors.length - 1; + + } + + /** + * Process image + * @param {Image} image to process + * @param {Integer} format of the image (e.g. THREE.RGBFormat, THREE.RGBAFormat etc) + * @param {Boolean} flipY before writing out the image + * @return {Integer} Index of the processed texture in the "images" array + */ + function processImage( image, format, flipY ) { + + if ( ! cachedData.images.has( image ) ) { + + cachedData.images.set( image, {} ); + + } + + var cachedImages = cachedData.images.get( image ); + var mimeType = format === THREE.RGBAFormat ? 'image/png' : 'image/jpeg'; + var key = mimeType + ":flipY/" + flipY.toString(); + + if ( cachedImages[ key ] !== undefined ) { + + return cachedImages[ key ]; + + } + + if ( ! outputJSON.images ) { + + outputJSON.images = []; + + } + + var gltfImage = { mimeType: mimeType }; + + if ( options.embedImages ) { + + var canvas = cachedCanvas = cachedCanvas || document.createElement( 'canvas' ); + + canvas.width = image.width; + canvas.height = image.height; + + if ( options.forcePowerOfTwoTextures && ! isPowerOfTwo( image ) ) { + + console.warn( 'GLTFExporter: Resized non-power-of-two image.', image ); + + canvas.width = THREE.Math.floorPowerOfTwo( canvas.width ); + canvas.height = THREE.Math.floorPowerOfTwo( canvas.height ); + + } + + var ctx = canvas.getContext( '2d' ); + + if ( flipY === true ) { + + ctx.translate( 0, canvas.height ); + ctx.scale( 1, - 1 ); + + } + + ctx.drawImage( image, 0, 0, canvas.width, canvas.height ); + + if ( options.binary === true ) { + + pending.push( new Promise( function ( resolve ) { + + canvas.toBlob( function ( blob ) { + + processBufferViewImage( blob ).then( function ( bufferViewIndex ) { + + gltfImage.bufferView = bufferViewIndex; + + resolve(); + + } ); + + }, mimeType ); + + } ) ); + + } else { + + gltfImage.uri = canvas.toDataURL( mimeType ); + + } + + } else { + + gltfImage.uri = image.src; + + } + + outputJSON.images.push( gltfImage ); + + var index = outputJSON.images.length - 1; + cachedImages[ key ] = index; + + return index; + + } + + /** + * Process sampler + * @param {Texture} map Texture to process + * @return {Integer} Index of the processed texture in the "samplers" array + */ + function processSampler( map ) { + + if ( ! outputJSON.samplers ) { + + outputJSON.samplers = []; + + } + + var gltfSampler = { + + magFilter: THREE_TO_WEBGL[ map.magFilter ], + minFilter: THREE_TO_WEBGL[ map.minFilter ], + wrapS: THREE_TO_WEBGL[ map.wrapS ], + wrapT: THREE_TO_WEBGL[ map.wrapT ] + + }; + + outputJSON.samplers.push( gltfSampler ); + + return outputJSON.samplers.length - 1; + + } + + /** + * Process texture + * @param {Texture} map Map to process + * @return {Integer} Index of the processed texture in the "textures" array + */ + function processTexture( map ) { + + if ( cachedData.textures.has( map ) ) { + + return cachedData.textures.get( map ); + + } + + if ( ! outputJSON.textures ) { + + outputJSON.textures = []; + + } + + var gltfTexture = { + + sampler: processSampler( map ), + source: processImage( map.image, map.format, map.flipY ) + + }; + + outputJSON.textures.push( gltfTexture ); + + var index = outputJSON.textures.length - 1; + cachedData.textures.set( map, index ); + + return index; + + } + + /** + * Process material + * @param {THREE.Material} material Material to process + * @return {Integer} Index of the processed material in the "materials" array + */ + function processMaterial( material ) { + + if ( cachedData.materials.has( material ) ) { + + return cachedData.materials.get( material ); + + } + + if ( ! outputJSON.materials ) { + + outputJSON.materials = []; + + } + + if ( material.isShaderMaterial ) { + + console.warn( 'GLTFExporter: THREE.ShaderMaterial not supported.' ); + return null; + + } + + // @QUESTION Should we avoid including any attribute that has the default value? + var gltfMaterial = { + + pbrMetallicRoughness: {} + + }; + + if ( material.isMeshBasicMaterial ) { + + gltfMaterial.extensions = { KHR_materials_unlit: {} }; + + extensionsUsed[ 'KHR_materials_unlit' ] = true; + + } else if ( ! material.isMeshStandardMaterial ) { + + console.warn( 'GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results.' ); + + } + + // pbrMetallicRoughness.baseColorFactor + var color = material.color.toArray().concat( [ material.opacity ] ); + + if ( ! equalArray( color, [ 1, 1, 1, 1 ] ) ) { + + gltfMaterial.pbrMetallicRoughness.baseColorFactor = color; + + } + + if ( material.isMeshStandardMaterial ) { + + gltfMaterial.pbrMetallicRoughness.metallicFactor = material.metalness; + gltfMaterial.pbrMetallicRoughness.roughnessFactor = material.roughness; + + } else if ( material.isMeshBasicMaterial ) { + + gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.0; + gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.9; + + } else { + + gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.5; + gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.5; + + } + + // pbrMetallicRoughness.metallicRoughnessTexture + if ( material.metalnessMap || material.roughnessMap ) { + + if ( material.metalnessMap === material.roughnessMap ) { + + var metalRoughMapDef = { index: processTexture( material.metalnessMap ) }; + applyTextureTransform( metalRoughMapDef, material.metalnessMap ); + gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture = metalRoughMapDef; + + } else { + + console.warn( 'THREE.GLTFExporter: Ignoring metalnessMap and roughnessMap because they are not the same Texture.' ); + + } + + } + + // pbrMetallicRoughness.baseColorTexture + if ( material.map ) { + + var baseColorMapDef = { index: processTexture( material.map ) }; + applyTextureTransform( baseColorMapDef, material.map ); + gltfMaterial.pbrMetallicRoughness.baseColorTexture = baseColorMapDef; + + } + + if ( material.isMeshBasicMaterial || + material.isLineBasicMaterial || + material.isPointsMaterial ) { + + } else { + + // emissiveFactor + var emissive = material.emissive.clone().multiplyScalar( material.emissiveIntensity ).toArray(); + + if ( ! equalArray( emissive, [ 0, 0, 0 ] ) ) { + + gltfMaterial.emissiveFactor = emissive; + + } + + // emissiveTexture + if ( material.emissiveMap ) { + + var emissiveMapDef = { index: processTexture( material.emissiveMap ) }; + applyTextureTransform( emissiveMapDef, material.emissiveMap ); + gltfMaterial.emissiveTexture = emissiveMapDef; + + } + + } + + // normalTexture + if ( material.normalMap ) { + + var normalMapDef = { index: processTexture( material.normalMap ) }; + + if ( material.normalScale.x !== - 1 ) { + + if ( material.normalScale.x !== material.normalScale.y ) { + + console.warn( 'THREE.GLTFExporter: Normal scale components are different, ignoring Y and exporting X.' ); + + } + + normalMapDef.scale = material.normalScale.x; + + } + + applyTextureTransform( normalMapDef, material.normalMap ); + + gltfMaterial.normalTexture = normalMapDef; + + } + + // occlusionTexture + if ( material.aoMap ) { + + var occlusionMapDef = { + index: processTexture( material.aoMap ), + texCoord: 1 + }; + + if ( material.aoMapIntensity !== 1.0 ) { + + occlusionMapDef.strength = material.aoMapIntensity; + + } + + applyTextureTransform( occlusionMapDef, material.aoMap ); + + gltfMaterial.occlusionTexture = occlusionMapDef; + + } + + // alphaMode + if ( material.transparent || material.alphaTest > 0.0 ) { + + gltfMaterial.alphaMode = material.opacity < 1.0 ? 'BLEND' : 'MASK'; + + // Write alphaCutoff if it's non-zero and different from the default (0.5). + if ( material.alphaTest > 0.0 && material.alphaTest !== 0.5 ) { + + gltfMaterial.alphaCutoff = material.alphaTest; + + } + + } + + // doubleSided + if ( material.side === THREE.DoubleSide ) { + + gltfMaterial.doubleSided = true; + + } + + if ( material.name !== '' ) { + + gltfMaterial.name = material.name; + + } + + serializeUserData( material, gltfMaterial ); + + outputJSON.materials.push( gltfMaterial ); + + var index = outputJSON.materials.length - 1; + cachedData.materials.set( material, index ); + + return index; + + } + + /** + * Process mesh + * @param {THREE.Mesh} mesh Mesh to process + * @return {Integer} Index of the processed mesh in the "meshes" array + */ + function processMesh( mesh ) { + + var cacheKey = mesh.geometry.uuid + ':' + mesh.material.uuid; + if ( cachedData.meshes.has( cacheKey ) ) { + + return cachedData.meshes.get( cacheKey ); + + } + + var geometry = mesh.geometry; + + var mode; + + // Use the correct mode + if ( mesh.isLineSegments ) { + + mode = WEBGL_CONSTANTS.LINES; + + } else if ( mesh.isLineLoop ) { + + mode = WEBGL_CONSTANTS.LINE_LOOP; + + } else if ( mesh.isLine ) { + + mode = WEBGL_CONSTANTS.LINE_STRIP; + + } else if ( mesh.isPoints ) { + + mode = WEBGL_CONSTANTS.POINTS; + + } else { + + if ( ! geometry.isBufferGeometry ) { + + console.warn( 'GLTFExporter: Exporting THREE.Geometry will increase file size. Use THREE.BufferGeometry instead.' ); + + var geometryTemp = new THREE.BufferGeometry(); + geometryTemp.fromGeometry( geometry ); + geometry = geometryTemp; + + } + + if ( mesh.drawMode === THREE.TriangleFanDrawMode ) { + + console.warn( 'GLTFExporter: TriangleFanDrawMode and wireframe incompatible.' ); + mode = WEBGL_CONSTANTS.TRIANGLE_FAN; + + } else if ( mesh.drawMode === THREE.TriangleStripDrawMode ) { + + mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINE_STRIP : WEBGL_CONSTANTS.TRIANGLE_STRIP; + + } else { + + mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES; + + } + + } + + var gltfMesh = {}; + + var attributes = {}; + var primitives = []; + var targets = []; + + // Conversion between attributes names in threejs and gltf spec + var nameConversion = { + + uv: 'TEXCOORD_0', + uv2: 'TEXCOORD_1', + color: 'COLOR_0', + skinWeight: 'WEIGHTS_0', + skinIndex: 'JOINTS_0' + + }; + + var originalNormal = geometry.getAttribute( 'normal' ); + + if ( originalNormal !== undefined && ! isNormalizedNormalAttribute( originalNormal ) ) { + + console.warn( 'THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.' ); + + geometry.addAttribute( 'normal', createNormalizedNormalAttribute( originalNormal ) ); + + } + + // @QUESTION Detect if .vertexColors = THREE.VertexColors? + // For every attribute create an accessor + var modifiedAttribute = null; + for ( var attributeName in geometry.attributes ) { + + // Ignore morph target attributes, which are exported later. + if ( attributeName.substr( 0, 5 ) === 'morph' ) continue; + + var attribute = geometry.attributes[ attributeName ]; + attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase(); + + // Prefix all geometry attributes except the ones specifically + // listed in the spec; non-spec attributes are considered custom. + var validVertexAttributes = + /^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/; + if ( ! validVertexAttributes.test( attributeName ) ) { + + attributeName = '_' + attributeName; + + } + + if ( cachedData.attributes.has( getUID( attribute ) ) ) { + + attributes[ attributeName ] = cachedData.attributes.get( getUID( attribute ) ); + continue; + + } + + // JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT. + modifiedAttribute = null; + var array = attribute.array; + if ( attributeName === 'JOINTS_0' && + ! ( array instanceof Uint16Array ) && + ! ( array instanceof Uint8Array ) ) { + + console.warn( 'GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.' ); + modifiedAttribute = new THREE.BufferAttribute( new Uint16Array( array ), attribute.itemSize, attribute.normalized ); + + } + + var accessor = processAccessor( modifiedAttribute || attribute, geometry ); + if ( accessor !== null ) { + + attributes[ attributeName ] = accessor; + cachedData.attributes.set( getUID( attribute ), accessor ); + + } + + } + + if ( originalNormal !== undefined ) geometry.addAttribute( 'normal', originalNormal ); + + // Skip if no exportable attributes found + if ( Object.keys( attributes ).length === 0 ) { + + return null; + + } + + // Morph targets + if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) { + + var weights = []; + var targetNames = []; + var reverseDictionary = {}; + + if ( mesh.morphTargetDictionary !== undefined ) { + + for ( var key in mesh.morphTargetDictionary ) { + + reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key; + + } + + } + + for ( var i = 0; i < mesh.morphTargetInfluences.length; ++ i ) { + + var target = {}; + + var warned = false; + + for ( var attributeName in geometry.morphAttributes ) { + + // glTF 2.0 morph supports only POSITION/NORMAL/TANGENT. + // Three.js doesn't support TANGENT yet. + + if ( attributeName !== 'position' && attributeName !== 'normal' ) { + + if ( ! warned ) { + + console.warn( 'GLTFExporter: Only POSITION and NORMAL morph are supported.' ); + warned = true; + + } + + continue; + + } + + var attribute = geometry.morphAttributes[ attributeName ][ i ]; + var gltfAttributeName = attributeName.toUpperCase(); + + // Three.js morph attribute has absolute values while the one of glTF has relative values. + // + // glTF 2.0 Specification: + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets + + var baseAttribute = geometry.attributes[ attributeName ]; + + if ( cachedData.attributes.has( getUID( attribute ) ) ) { + + target[ gltfAttributeName ] = cachedData.attributes.get( getUID( attribute ) ); + continue; + + } + + // Clones attribute not to override + var relativeAttribute = attribute.clone(); + + for ( var j = 0, jl = attribute.count; j < jl; j ++ ) { + + relativeAttribute.setXYZ( + j, + attribute.getX( j ) - baseAttribute.getX( j ), + attribute.getY( j ) - baseAttribute.getY( j ), + attribute.getZ( j ) - baseAttribute.getZ( j ) + ); + + } + + target[ gltfAttributeName ] = processAccessor( relativeAttribute, geometry ); + cachedData.attributes.set( getUID( baseAttribute ), target[ gltfAttributeName ] ); + + } + + targets.push( target ); + + weights.push( mesh.morphTargetInfluences[ i ] ); + if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] ); + + } + + gltfMesh.weights = weights; + + if ( targetNames.length > 0 ) { + + gltfMesh.extras = {}; + gltfMesh.extras.targetNames = targetNames; + + } + + } + + var forceIndices = options.forceIndices; + var isMultiMaterial = Array.isArray( mesh.material ); + + if ( isMultiMaterial && geometry.groups.length === 0 ) return null; + + if ( ! forceIndices && geometry.index === null && isMultiMaterial ) { + + // temporal workaround. + console.warn( 'THREE.GLTFExporter: Creating index for non-indexed multi-material mesh.' ); + forceIndices = true; + + } + + var didForceIndices = false; + + if ( geometry.index === null && forceIndices ) { + + var indices = []; + + for ( var i = 0, il = geometry.attributes.position.count; i < il; i ++ ) { + + indices[ i ] = i; + + } + + geometry.setIndex( indices ); + + didForceIndices = true; + + } + + var materials = isMultiMaterial ? mesh.material : [ mesh.material ]; + var groups = isMultiMaterial ? geometry.groups : [ { materialIndex: 0, start: undefined, count: undefined } ]; + + for ( var i = 0, il = groups.length; i < il; i ++ ) { + + var primitive = { + mode: mode, + attributes: attributes, + }; + + serializeUserData( geometry, primitive ); + + if ( targets.length > 0 ) primitive.targets = targets; + + if ( geometry.index !== null ) { + + var cacheKey = getUID( geometry.index ); + + if ( groups[ i ].start !== undefined || groups[ i ].count !== undefined ) { + + cacheKey += ':' + groups[ i ].start + ':' + groups[ i ].count; + + } + + if ( cachedData.attributes.has( cacheKey ) ) { + + primitive.indices = cachedData.attributes.get( cacheKey ); + + } else { + + primitive.indices = processAccessor( geometry.index, geometry, groups[ i ].start, groups[ i ].count ); + cachedData.attributes.set( cacheKey, primitive.indices ); + + } + + if ( primitive.indices === null ) delete primitive.indices; + + } + + var material = processMaterial( materials[ groups[ i ].materialIndex ] ); + + if ( material !== null ) { + + primitive.material = material; + + } + + primitives.push( primitive ); + + } + + if ( didForceIndices ) { + + geometry.setIndex( null ); + + } + + gltfMesh.primitives = primitives; + + if ( ! outputJSON.meshes ) { + + outputJSON.meshes = []; + + } + + outputJSON.meshes.push( gltfMesh ); + + var index = outputJSON.meshes.length - 1; + cachedData.meshes.set( cacheKey, index ); + + return index; + + } + + /** + * Process camera + * @param {THREE.Camera} camera Camera to process + * @return {Integer} Index of the processed mesh in the "camera" array + */ + function processCamera( camera ) { + + if ( ! outputJSON.cameras ) { + + outputJSON.cameras = []; + + } + + var isOrtho = camera.isOrthographicCamera; + + var gltfCamera = { + + type: isOrtho ? 'orthographic' : 'perspective' + + }; + + if ( isOrtho ) { + + gltfCamera.orthographic = { + + xmag: camera.right * 2, + ymag: camera.top * 2, + zfar: camera.far <= 0 ? 0.001 : camera.far, + znear: camera.near < 0 ? 0 : camera.near + + }; + + } else { + + gltfCamera.perspective = { + + aspectRatio: camera.aspect, + yfov: THREE.Math.degToRad( camera.fov ), + zfar: camera.far <= 0 ? 0.001 : camera.far, + znear: camera.near < 0 ? 0 : camera.near + + }; + + } + + if ( camera.name !== '' ) { + + gltfCamera.name = camera.type; + + } + + outputJSON.cameras.push( gltfCamera ); + + return outputJSON.cameras.length - 1; + + } + + /** + * Creates glTF animation entry from AnimationClip object. + * + * Status: + * - Only properties listed in PATH_PROPERTIES may be animated. + * + * @param {THREE.AnimationClip} clip + * @param {THREE.Object3D} root + * @return {number} + */ + function processAnimation( clip, root ) { + + if ( ! outputJSON.animations ) { + + outputJSON.animations = []; + + } + + clip = THREE.GLTFExporter.Utils.mergeMorphTargetTracks( clip.clone(), root ); + + var tracks = clip.tracks; + var channels = []; + var samplers = []; + + for ( var i = 0; i < tracks.length; ++ i ) { + + var track = tracks[ i ]; + var trackBinding = THREE.PropertyBinding.parseTrackName( track.name ); + var trackNode = THREE.PropertyBinding.findNode( root, trackBinding.nodeName ); + var trackProperty = PATH_PROPERTIES[ trackBinding.propertyName ]; + + if ( trackBinding.objectName === 'bones' ) { + + if ( trackNode.isSkinnedMesh === true ) { + + trackNode = trackNode.skeleton.getBoneByName( trackBinding.objectIndex ); + + } else { + + trackNode = undefined; + + } + + } + + if ( ! trackNode || ! trackProperty ) { + + console.warn( 'THREE.GLTFExporter: Could not export animation track "%s".', track.name ); + return null; + + } + + var inputItemSize = 1; + var outputItemSize = track.values.length / track.times.length; + + if ( trackProperty === PATH_PROPERTIES.morphTargetInfluences ) { + + outputItemSize /= trackNode.morphTargetInfluences.length; + + } + + var interpolation; + + // @TODO export CubicInterpolant(InterpolateSmooth) as CUBICSPLINE + + // Detecting glTF cubic spline interpolant by checking factory method's special property + // GLTFCubicSplineInterpolant is a custom interpolant and track doesn't return + // valid value from .getInterpolation(). + if ( track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true ) { + + interpolation = 'CUBICSPLINE'; + + // itemSize of CUBICSPLINE keyframe is 9 + // (VEC3 * 3: inTangent, splineVertex, and outTangent) + // but needs to be stored as VEC3 so dividing by 3 here. + outputItemSize /= 3; + + } else if ( track.getInterpolation() === THREE.InterpolateDiscrete ) { + + interpolation = 'STEP'; + + } else { + + interpolation = 'LINEAR'; + + } + + samplers.push( { + + input: processAccessor( new THREE.BufferAttribute( track.times, inputItemSize ) ), + output: processAccessor( new THREE.BufferAttribute( track.values, outputItemSize ) ), + interpolation: interpolation + + } ); + + channels.push( { + + sampler: samplers.length - 1, + target: { + node: nodeMap.get( trackNode ), + path: trackProperty + } + + } ); + + } + + outputJSON.animations.push( { + + name: clip.name || 'clip_' + outputJSON.animations.length, + samplers: samplers, + channels: channels + + } ); + + return outputJSON.animations.length - 1; + + } + + function processSkin( object ) { + + var node = outputJSON.nodes[ nodeMap.get( object ) ]; + + var skeleton = object.skeleton; + var rootJoint = object.skeleton.bones[ 0 ]; + + if ( rootJoint === undefined ) return null; + + var joints = []; + var inverseBindMatrices = new Float32Array( skeleton.bones.length * 16 ); + + for ( var i = 0; i < skeleton.bones.length; ++ i ) { + + joints.push( nodeMap.get( skeleton.bones[ i ] ) ); + + skeleton.boneInverses[ i ].toArray( inverseBindMatrices, i * 16 ); + + } + + if ( outputJSON.skins === undefined ) { + + outputJSON.skins = []; + + } + + outputJSON.skins.push( { + + inverseBindMatrices: processAccessor( new THREE.BufferAttribute( inverseBindMatrices, 16 ) ), + joints: joints, + skeleton: nodeMap.get( rootJoint ) + + } ); + + var skinIndex = node.skin = outputJSON.skins.length - 1; + + return skinIndex; + + } + + function processLight( light ) { + + var lightDef = {}; + + if ( light.name ) lightDef.name = light.name; + + lightDef.color = light.color.toArray(); + + lightDef.intensity = light.intensity; + + if ( light.isDirectionalLight ) { + + lightDef.type = 'directional'; + + } else if ( light.isPointLight ) { + + lightDef.type = 'point'; + if ( light.distance > 0 ) lightDef.range = light.distance; + + } else if ( light.isSpotLight ) { + + lightDef.type = 'spot'; + if ( light.distance > 0 ) lightDef.range = light.distance; + lightDef.spot = {}; + lightDef.spot.innerConeAngle = ( light.penumbra - 1.0 ) * light.angle * - 1.0; + lightDef.spot.outerConeAngle = light.angle; + + } + + if ( light.decay !== undefined && light.decay !== 2 ) { + + console.warn( 'THREE.GLTFExporter: Light decay may be lost. glTF is physically-based, ' + + 'and expects light.decay=2.' ); + + } + + if ( light.target + && ( light.target.parent !== light + || light.target.position.x !== 0 + || light.target.position.y !== 0 + || light.target.position.z !== - 1 ) ) { + + console.warn( 'THREE.GLTFExporter: Light direction may be lost. For best results, ' + + 'make light.target a child of the light with position 0,0,-1.' ); + + } + + var lights = outputJSON.extensions[ 'KHR_lights_punctual' ].lights; + lights.push( lightDef ); + return lights.length - 1; + + } + + /** + * Process Object3D node + * @param {THREE.Object3D} node Object3D to processNode + * @return {Integer} Index of the node in the nodes list + */ + function processNode( object ) { + + if ( ! outputJSON.nodes ) { + + outputJSON.nodes = []; + + } + + var gltfNode = {}; + + if ( options.trs ) { + + var rotation = object.quaternion.toArray(); + var position = object.position.toArray(); + var scale = object.scale.toArray(); + + if ( ! equalArray( rotation, [ 0, 0, 0, 1 ] ) ) { + + gltfNode.rotation = rotation; + + } + + if ( ! equalArray( position, [ 0, 0, 0 ] ) ) { + + gltfNode.translation = position; + + } + + if ( ! equalArray( scale, [ 1, 1, 1 ] ) ) { + + gltfNode.scale = scale; + + } + + } else { + + if ( object.matrixAutoUpdate ) { + + object.updateMatrix(); + + } + + if ( ! equalArray( object.matrix.elements, [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] ) ) { + + gltfNode.matrix = object.matrix.elements; + + } + + } + + // We don't export empty strings name because it represents no-name in Three.js. + if ( object.name !== '' ) { + + gltfNode.name = String( object.name ); + + } + + serializeUserData( object, gltfNode ); + + if ( object.isMesh || object.isLine || object.isPoints ) { + + var mesh = processMesh( object ); + + if ( mesh !== null ) { + + gltfNode.mesh = mesh; + + } + + } else if ( object.isCamera ) { + + gltfNode.camera = processCamera( object ); + + } else if ( object.isDirectionalLight || object.isPointLight || object.isSpotLight ) { + + if ( ! extensionsUsed[ 'KHR_lights_punctual' ] ) { + + outputJSON.extensions = outputJSON.extensions || {}; + outputJSON.extensions[ 'KHR_lights_punctual' ] = { lights: [] }; + extensionsUsed[ 'KHR_lights_punctual' ] = true; + + } + + gltfNode.extensions = gltfNode.extensions || {}; + gltfNode.extensions[ 'KHR_lights_punctual' ] = { light: processLight( object ) }; + + } else if ( object.isLight ) { + + console.warn( 'THREE.GLTFExporter: Only directional, point, and spot lights are supported.', object ); + return null; + + } + + if ( object.isSkinnedMesh ) { + + skins.push( object ); + + } + + if ( object.children.length > 0 ) { + + var children = []; + + for ( var i = 0, l = object.children.length; i < l; i ++ ) { + + var child = object.children[ i ]; + + if ( child.visible || options.onlyVisible === false ) { + + var node = processNode( child ); + + if ( node !== null ) { + + children.push( node ); + + } + + } + + } + + if ( children.length > 0 ) { + + gltfNode.children = children; + + } + + + } + + outputJSON.nodes.push( gltfNode ); + + var nodeIndex = outputJSON.nodes.length - 1; + nodeMap.set( object, nodeIndex ); + + return nodeIndex; + + } + + /** + * Process Scene + * @param {THREE.Scene} node Scene to process + */ + function processScene( scene ) { + + if ( ! outputJSON.scenes ) { + + outputJSON.scenes = []; + outputJSON.scene = 0; + + } + + var gltfScene = { + + nodes: [] + + }; + + if ( scene.name !== '' ) { + + gltfScene.name = scene.name; + + } + + if ( scene.userData && Object.keys( scene.userData ).length > 0 ) { + + gltfScene.extras = serializeUserData( scene ); + + } + + outputJSON.scenes.push( gltfScene ); + + var nodes = []; + + for ( var i = 0, l = scene.children.length; i < l; i ++ ) { + + var child = scene.children[ i ]; + + if ( child.visible || options.onlyVisible === false ) { + + var node = processNode( child ); + + if ( node !== null ) { + + nodes.push( node ); + + } + + } + + } + + if ( nodes.length > 0 ) { + + gltfScene.nodes = nodes; + + } + + serializeUserData( scene, gltfScene ); + + } + + /** + * Creates a THREE.Scene to hold a list of objects and parse it + * @param {Array} objects List of objects to process + */ + function processObjects( objects ) { + + var scene = new THREE.Scene(); + scene.name = 'AuxScene'; + + for ( var i = 0; i < objects.length; i ++ ) { + + // We push directly to children instead of calling `add` to prevent + // modify the .parent and break its original scene and hierarchy + scene.children.push( objects[ i ] ); + + } + + processScene( scene ); + + } + + function processInput( input ) { + + input = input instanceof Array ? input : [ input ]; + + var objectsWithoutScene = []; + + for ( var i = 0; i < input.length; i ++ ) { + + if ( input[ i ] instanceof THREE.Scene ) { + + processScene( input[ i ] ); + + } else { + + objectsWithoutScene.push( input[ i ] ); + + } + + } + + if ( objectsWithoutScene.length > 0 ) { + + processObjects( objectsWithoutScene ); + + } + + for ( var i = 0; i < skins.length; ++ i ) { + + processSkin( skins[ i ] ); + + } + + for ( var i = 0; i < options.animations.length; ++ i ) { + + processAnimation( options.animations[ i ], input[ 0 ] ); + + } + + } + + processInput( input ); + + Promise.all( pending ).then( function () { + + // Merge buffers. + var blob = new Blob( buffers, { type: 'application/octet-stream' } ); + + // Declare extensions. + var extensionsUsedList = Object.keys( extensionsUsed ); + if ( extensionsUsedList.length > 0 ) outputJSON.extensionsUsed = extensionsUsedList; + + if ( outputJSON.buffers && outputJSON.buffers.length > 0 ) { + + // Update bytelength of the single buffer. + outputJSON.buffers[ 0 ].byteLength = blob.size; + + var reader = new window.FileReader(); + + if ( options.binary === true ) { + + // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification + + var GLB_HEADER_BYTES = 12; + var GLB_HEADER_MAGIC = 0x46546C67; + var GLB_VERSION = 2; + + var GLB_CHUNK_PREFIX_BYTES = 8; + var GLB_CHUNK_TYPE_JSON = 0x4E4F534A; + var GLB_CHUNK_TYPE_BIN = 0x004E4942; + + reader.readAsArrayBuffer( blob ); + reader.onloadend = function () { + + // Binary chunk. + var binaryChunk = getPaddedArrayBuffer( reader.result ); + var binaryChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) ); + binaryChunkPrefix.setUint32( 0, binaryChunk.byteLength, true ); + binaryChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_BIN, true ); + + // JSON chunk. + var jsonChunk = getPaddedArrayBuffer( stringToArrayBuffer( JSON.stringify( outputJSON ) ), 0x20 ); + var jsonChunkPrefix = new DataView( new ArrayBuffer( GLB_CHUNK_PREFIX_BYTES ) ); + jsonChunkPrefix.setUint32( 0, jsonChunk.byteLength, true ); + jsonChunkPrefix.setUint32( 4, GLB_CHUNK_TYPE_JSON, true ); + + // GLB header. + var header = new ArrayBuffer( GLB_HEADER_BYTES ); + var headerView = new DataView( header ); + headerView.setUint32( 0, GLB_HEADER_MAGIC, true ); + headerView.setUint32( 4, GLB_VERSION, true ); + var totalByteLength = GLB_HEADER_BYTES + + jsonChunkPrefix.byteLength + jsonChunk.byteLength + + binaryChunkPrefix.byteLength + binaryChunk.byteLength; + headerView.setUint32( 8, totalByteLength, true ); + + var glbBlob = new Blob( [ + header, + jsonChunkPrefix, + jsonChunk, + binaryChunkPrefix, + binaryChunk + ], { type: 'application/octet-stream' } ); + + var glbReader = new window.FileReader(); + glbReader.readAsArrayBuffer( glbBlob ); + glbReader.onloadend = function () { + + onDone( glbReader.result ); + + }; + + }; + + } else { + + reader.readAsDataURL( blob ); + reader.onloadend = function () { + + var base64data = reader.result; + outputJSON.buffers[ 0 ].uri = base64data; + onDone( outputJSON ); + + }; + + } + + } else { + + onDone( outputJSON ); + + } + + } ); + + } + +}; + +THREE.GLTFExporter.Utils = { + + insertKeyframe: function ( track, time ) { + + var tolerance = 0.001; // 1ms + var valueSize = track.getValueSize(); + + var times = new track.TimeBufferType( track.times.length + 1 ); + var values = new track.ValueBufferType( track.values.length + valueSize ); + var interpolant = track.createInterpolant( new track.ValueBufferType( valueSize ) ); + + var index; + + if ( track.times.length === 0 ) { + + times[ 0 ] = time; + + for ( var i = 0; i < valueSize; i ++ ) { + + values[ i ] = 0; + + } + + index = 0; + + } else if ( time < track.times[ 0 ] ) { + + if ( Math.abs( track.times[ 0 ] - time ) < tolerance ) return 0; + + times[ 0 ] = time; + times.set( track.times, 1 ); + + values.set( interpolant.evaluate( time ), 0 ); + values.set( track.values, valueSize ); + + index = 0; + + } else if ( time > track.times[ track.times.length - 1 ] ) { + + if ( Math.abs( track.times[ track.times.length - 1 ] - time ) < tolerance ) { + + return track.times.length - 1; + + } + + times[ times.length - 1 ] = time; + times.set( track.times, 0 ); + + values.set( track.values, 0 ); + values.set( interpolant.evaluate( time ), track.values.length ); + + index = times.length - 1; + + } else { + + for ( var i = 0; i < track.times.length; i ++ ) { + + if ( Math.abs( track.times[ i ] - time ) < tolerance ) return i; + + if ( track.times[ i ] < time && track.times[ i + 1 ] > time ) { + + times.set( track.times.slice( 0, i + 1 ), 0 ); + times[ i + 1 ] = time; + times.set( track.times.slice( i + 1 ), i + 2 ); + + values.set( track.values.slice( 0, ( i + 1 ) * valueSize ), 0 ); + values.set( interpolant.evaluate( time ), ( i + 1 ) * valueSize ); + values.set( track.values.slice( ( i + 1 ) * valueSize ), ( i + 2 ) * valueSize ); + + index = i + 1; + + break; + + } + + } + + } + + track.times = times; + track.values = values; + + return index; + + }, + + mergeMorphTargetTracks: function ( clip, root ) { + + var tracks = []; + var mergedTracks = {}; + var sourceTracks = clip.tracks; + + for ( var i = 0; i < sourceTracks.length; ++ i ) { + + var sourceTrack = sourceTracks[ i ]; + var sourceTrackBinding = THREE.PropertyBinding.parseTrackName( sourceTrack.name ); + var sourceTrackNode = THREE.PropertyBinding.findNode( root, sourceTrackBinding.nodeName ); + + if ( sourceTrackBinding.propertyName !== 'morphTargetInfluences' || sourceTrackBinding.propertyIndex === undefined ) { + + // Tracks that don't affect morph targets, or that affect all morph targets together, can be left as-is. + tracks.push( sourceTrack ); + continue; + + } + + if ( sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodDiscrete + && sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodLinear ) { + + if ( sourceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) { + + // This should never happen, because glTF morph target animations + // affect all targets already. + throw new Error( 'THREE.GLTFExporter: Cannot merge tracks with glTF CUBICSPLINE interpolation.' ); + + } + + console.warn( 'THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead.' ); + + sourceTrack = sourceTrack.clone(); + sourceTrack.setInterpolation( THREE.InterpolateLinear ); + + } + + var targetCount = sourceTrackNode.morphTargetInfluences.length; + var targetIndex = sourceTrackNode.morphTargetDictionary[ sourceTrackBinding.propertyIndex ]; + + if ( targetIndex === undefined ) { + + throw new Error( 'THREE.GLTFExporter: Morph target name not found: ' + sourceTrackBinding.propertyIndex ); + + } + + var mergedTrack; + + // If this is the first time we've seen this object, create a new + // track to store merged keyframe data for each morph target. + if ( mergedTracks[ sourceTrackNode.uuid ] === undefined ) { + + mergedTrack = sourceTrack.clone(); + + var values = new mergedTrack.ValueBufferType( targetCount * mergedTrack.times.length ); + + for ( var j = 0; j < mergedTrack.times.length; j ++ ) { + + values[ j * targetCount + targetIndex ] = mergedTrack.values[ j ]; + + } + + mergedTrack.name = '.morphTargetInfluences'; + mergedTrack.values = values; + + mergedTracks[ sourceTrackNode.uuid ] = mergedTrack; + tracks.push( mergedTrack ); + + continue; + + } + + var sourceInterpolant = sourceTrack.createInterpolant( new sourceTrack.ValueBufferType( 1 ) ); + + mergedTrack = mergedTracks[ sourceTrackNode.uuid ]; + + // For every existing keyframe of the merged track, write a (possibly + // interpolated) value from the source track. + for ( var j = 0; j < mergedTrack.times.length; j ++ ) { + + mergedTrack.values[ j * targetCount + targetIndex ] = sourceInterpolant.evaluate( mergedTrack.times[ j ] ); + + } + + // For every existing keyframe of the source track, write a (possibly + // new) keyframe to the merged track. Values from the previous loop may + // be written again, but keyframes are de-duplicated. + for ( var j = 0; j < sourceTrack.times.length; j ++ ) { + + var keyframeIndex = this.insertKeyframe( mergedTrack, sourceTrack.times[ j ] ); + mergedTrack.values[ keyframeIndex * targetCount + targetIndex ] = sourceTrack.values[ j ]; + + } + + } + + clip.tracks = tracks; + + return clip; + + } + +}; diff --git a/src/js/windows/shot-generator/main.js b/src/js/windows/shot-generator/main.js index ca7b3d816a..e0c3cd33f0 100644 --- a/src/js/windows/shot-generator/main.js +++ b/src/js/windows/shot-generator/main.js @@ -114,6 +114,9 @@ ipcMain.on('shot-generator:edit:redo', () => { win.webContents.send('shot-generator:edit:redo') }) +ipcMain.on('shot-generator:export-gltf', () => + win.webContents.send('shot-generator:export-gltf')) + module.exports = { show, getWindow: () => win From 211b8ab3c34eaeba5567a591d93e78aa1a11a290 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 2 Oct 2019 12:31:52 -0500 Subject: [PATCH 02/16] write to binary file --- src/js/shot-generator/use-export-to-gltf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index d52ed5a6ae..95a5351ab6 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -54,7 +54,7 @@ const useExportToGltf = (sceneRef) => { ) fs.ensureDirSync(path.dirname(filepath)) - fs.writeFileSync(filepath, glb) + fs.writeFileSync(filepath, Buffer.from(glb)) notifications.notify({ message: `Exported to:\n${filename}`, From 94baed29e9782564395687a99cd455441a5293f5 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 2 Oct 2019 12:32:30 -0500 Subject: [PATCH 03/16] SG: GTLF exporter knows about sceneObjects --- src/js/shot-generator/use-export-to-gltf.js | 26 ++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index 95a5351ab6..e0e37a309b 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -9,11 +9,16 @@ const THREE = require('three') window.THREE = window.THREE || THREE require('../vendor/three/examples/js/exporters/GLTFExporter.js') +const { + getSceneObjects +} = require('../shared/reducers/shot-generator') + const notifications = require('../window/notifications') const useExportToGltf = (sceneRef) => { const meta = useSelector(state => state.meta) const board = useSelector(state => state.board) + const sceneObjects = useSelector(getSceneObjects) useEffect(() => { if (board && meta && meta.storyboarderFilePath) { @@ -26,17 +31,26 @@ const useExportToGltf = (sceneRef) => { console.log('Preparing GLTF…') let scene = new THREE.Scene() for (let child of sceneRef.current.children) { - console.log('\tScene contains:', child) + // console.log('\tScene contains:', child) // HACK test to avoid IconSprites, which fail to .clone if (!child.icon) { - // for now, just add the Groups - if (child.type === 'Group') { - console.log('\tAdding to GLTF:', child) - scene.add(child.clone()) + if (child.userData.id && sceneObjects[child.userData.id]) { + let sceneObject = sceneObjects[child.userData.id] + // TODO skip volumetric? + if (sceneObject.type === 'character') { + // TODO prevent out-of-memory error when cloning Character + // scene.add(child.clone()) + } else { + console.log('\tCloning for GLTF:', sceneObject.name) + scene.add(child.clone()) + } + console.log('\t\tOK') } } } + console.log('\tExporting Scene:', scene) + let exporter = new THREE.GLTFExporter() let options = { binary: true, @@ -71,7 +85,7 @@ const useExportToGltf = (sceneRef) => { console.log('cleanup shot-generator:export-gltf') ipcRenderer.removeAllListeners('shot-generator:export-gltf') } - }, [board, meta]) + }, [board, meta, sceneObjects]) } module.exports = useExportToGltf From 0e86ec65778c5e0d5e6b786b9810249d256bc444 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 2 Oct 2019 16:39:24 -0500 Subject: [PATCH 04/16] prevent out-of-memory error when cloning Character --- src/js/shot-generator/use-export-to-gltf.js | 27 ++++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index e0e37a309b..c23ba259e1 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -9,6 +9,8 @@ const THREE = require('three') window.THREE = window.THREE || THREE require('../vendor/three/examples/js/exporters/GLTFExporter.js') +const cloneGltf = require('../xr/src/helpers/clone-gltf') + const { getSceneObjects } = require('../shared/reducers/shot-generator') @@ -38,10 +40,27 @@ const useExportToGltf = (sceneRef) => { let sceneObject = sceneObjects[child.userData.id] // TODO skip volumetric? if (sceneObject.type === 'character') { - // TODO prevent out-of-memory error when cloning Character - // scene.add(child.clone()) - } else { - console.log('\tCloning for GLTF:', sceneObject.name) + console.log('\Cloning', sceneObject.type) + + let memento = { + bonesHelper: child.bonesHelper, + ikRig: child.userData.ikRig + } + child.bonesHelper = null + child.userData.ikRig = null + + // workaround for skinned mesh clone w/ skeleton + // (cloneGltf knows how to add bones back to skinned mesh skeleton) + let cloned = cloneGltf({ scene: child }) + for (child of cloned.scene.children) { + scene.add(child) + } + + child.bonesHelper = memento.bonesHelper + child.userData.ikRig = memento.ikRig + + } else if (sceneObject) { + console.log('\Cloning', sceneObject.type) scene.add(child.clone()) } console.log('\t\tOK') From c2138fc8aea20b40dc6162ef26dd24dd81e235a6 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 2 Oct 2019 17:25:56 -0500 Subject: [PATCH 05/16] clone only skinned mesh from character --- src/js/shot-generator/use-export-to-gltf.js | 24 +++++++-------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index c23ba259e1..ec55803c65 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -42,28 +42,20 @@ const useExportToGltf = (sceneRef) => { if (sceneObject.type === 'character') { console.log('\Cloning', sceneObject.type) - let memento = { - bonesHelper: child.bonesHelper, - ikRig: child.userData.ikRig - } - child.bonesHelper = null - child.userData.ikRig = null - - // workaround for skinned mesh clone w/ skeleton - // (cloneGltf knows how to add bones back to skinned mesh skeleton) - let cloned = cloneGltf({ scene: child }) - for (child of cloned.scene.children) { - scene.add(child) - } + for (node of child.children) { + if (node.isSkinnedMesh) { - child.bonesHelper = memento.bonesHelper - child.userData.ikRig = memento.ikRig + let cloned = cloneGltf({ scene: node }) + let clone = cloned.scene + + scene.add(clone) + } + } } else if (sceneObject) { console.log('\Cloning', sceneObject.type) scene.add(child.clone()) } - console.log('\t\tOK') } } } From ad959f0431bfba94e2e30865c7a3506235239009 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Wed, 2 Oct 2019 18:30:05 -0500 Subject: [PATCH 06/16] try cloning bones --- src/js/shot-generator/use-export-to-gltf.js | 35 ++++++++++++++++----- src/js/xr/src/three/clone-skinned-mesh.js | 29 +++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 src/js/xr/src/three/clone-skinned-mesh.js diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index ec55803c65..4e03921adc 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -10,6 +10,7 @@ window.THREE = window.THREE || THREE require('../vendor/three/examples/js/exporters/GLTFExporter.js') const cloneGltf = require('../xr/src/helpers/clone-gltf') +const cloneSkinnedMesh = require('../xr/src/three/clone-skinned-mesh') const { getSceneObjects @@ -38,23 +39,43 @@ const useExportToGltf = (sceneRef) => { if (!child.icon) { if (child.userData.id && sceneObjects[child.userData.id]) { let sceneObject = sceneObjects[child.userData.id] - // TODO skip volumetric? - if (sceneObject.type === 'character') { - console.log('\Cloning', sceneObject.type) + if (sceneObject.type === 'volume') { + console.log('\tSkipping', sceneObject.type) + + } else if (sceneObject.type === 'character') { + console.log('\tCloning', sceneObject.type) for (node of child.children) { if (node.isSkinnedMesh) { - let cloned = cloneGltf({ scene: node }) - let clone = cloned.scene + let rootBone = child.children.find(n => n.isBone) + let [clone, bones] = cloneSkinnedMesh(node, rootBone) + clone.remove(clone.children[0]) // remove the Bone + + clone.userData = {} + + clone.material = new THREE.MeshStandardMaterial() + clone.name = sceneObject.name || sceneObject.displayName + + let root = bones[clone.skeleton.bones[0].name] + clone.add(root) scene.add(clone) + } else { + console.log('\t\tSkipping node', node) } } } else if (sceneObject) { - console.log('\Cloning', sceneObject.type) - scene.add(child.clone()) + console.log('\tCloning', sceneObject.type) + let clone = child.clone() + + clone.userData = {} + + clone.material = new THREE.MeshStandardMaterial() + clone.name = sceneObject.name || sceneObject.displayName + + scene.add(clone) } } } diff --git a/src/js/xr/src/three/clone-skinned-mesh.js b/src/js/xr/src/three/clone-skinned-mesh.js new file mode 100644 index 0000000000..b086d47b42 --- /dev/null +++ b/src/js/xr/src/three/clone-skinned-mesh.js @@ -0,0 +1,29 @@ +const THREE = require('three') + +const cloneSkinnedMesh = (source, rootBone) => { + let clonedBones = {} + rootBone.traverse(bone => { + clonedBones[bone.name] = bone.clone() + }) + + let newSkinnedMesh = source.clone() + + // find matching bones by name + const bones = [] + for (let i = 0; i < source.skeleton.bones.length; ++i) { + let boneName = source.skeleton.bones[i].name + bones.push(clonedBones[boneName]) + } + + newSkinnedMesh.bind( + new THREE.Skeleton( + bones, + source.skeleton.boneInverses + ), + newSkinnedMesh.matrixWorld + ) + + return [newSkinnedMesh, clonedBones] +} + +module.exports = cloneSkinnedMesh From 4103565b6e2d7d28030b1d5bde503cb02908e00a Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Thu, 3 Oct 2019 11:42:34 -0500 Subject: [PATCH 07/16] use SkeletonUtils.clone to clone SkinnedMesh and Bone --- src/js/shot-generator/use-export-to-gltf.js | 35 ++++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index 4e03921adc..1b76e50cd1 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -8,9 +8,7 @@ const moment = require('moment') const THREE = require('three') window.THREE = window.THREE || THREE require('../vendor/three/examples/js/exporters/GLTFExporter.js') - -const cloneGltf = require('../xr/src/helpers/clone-gltf') -const cloneSkinnedMesh = require('../xr/src/three/clone-skinned-mesh') +require('../vendor/three/examples/js/utils/SkeletonUtils') const { getSceneObjects @@ -48,19 +46,32 @@ const useExportToGltf = (sceneRef) => { for (node of child.children) { if (node.isSkinnedMesh) { - let rootBone = child.children.find(n => n.isBone) - let [clone, bones] = cloneSkinnedMesh(node, rootBone) - clone.remove(clone.children[0]) // remove the Bone + let oldBonesHelper = node.parent.bonesHelper + let oldikRig = node.parent.userData.ikRig + node.parent.bonesHelper = null + node.parent.userData.ikRig = null + + let clone = THREE.SkeletonUtils.clone( node.parent ) + + let bone = clone.children[0] + let mesh = clone.children[1] - clone.userData = {} + mesh.name = sceneObject.name || sceneObject.displayName + mesh.userData = {} - clone.material = new THREE.MeshStandardMaterial() - clone.name = sceneObject.name || sceneObject.displayName + scene.add( bone ) + scene.add( mesh ) - let root = bones[clone.skeleton.bones[0].name] - clone.add(root) + console.log('\n\n\n\n') + console.log('-------------') + console.log('mesh', mesh) + console.log('bone', bone) + console.log('original .skeleton.boneMatrices', node.skeleton.boneMatrices) + console.log('cloned .skeleton.boneMatrices', mesh.skeleton.boneMatrices) + console.log('\n\n\n\n') - scene.add(clone) + node.parent.bonesHelper = oldBonesHelper + node.parent.userData.ikRig = oldikRig } else { console.log('\t\tSkipping node', node) } From 8dca8dd9ce2e2cef2cb99f8daad0ab3fc3bdebc1 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Thu, 3 Oct 2019 12:01:48 -0500 Subject: [PATCH 08/16] don't clone Character --- src/js/shot-generator/use-export-to-gltf.js | 48 +++++++++++---------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index 1b76e50cd1..77c4e64fba 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -30,6 +30,7 @@ const useExportToGltf = (sceneRef) => { }) console.log('Preparing GLTF…') + let mementos = [] let scene = new THREE.Scene() for (let child of sceneRef.current.children) { // console.log('\tScene contains:', child) @@ -46,32 +47,22 @@ const useExportToGltf = (sceneRef) => { for (node of child.children) { if (node.isSkinnedMesh) { - let oldBonesHelper = node.parent.bonesHelper - let oldikRig = node.parent.userData.ikRig - node.parent.bonesHelper = null - node.parent.userData.ikRig = null - - let clone = THREE.SkeletonUtils.clone( node.parent ) + mementos.push({ + parent: node.parent, + bonesHelper: node.parent.bonesHelper, + ikRig: node.parent.userData.ikRig, - let bone = clone.children[0] - let mesh = clone.children[1] + mesh: node, + name: node.name, + userData: node.userData + }) - mesh.name = sceneObject.name || sceneObject.displayName - mesh.userData = {} - - scene.add( bone ) - scene.add( mesh ) - - console.log('\n\n\n\n') - console.log('-------------') - console.log('mesh', mesh) - console.log('bone', bone) - console.log('original .skeleton.boneMatrices', node.skeleton.boneMatrices) - console.log('cloned .skeleton.boneMatrices', mesh.skeleton.boneMatrices) - console.log('\n\n\n\n') + node.parent.bonesHelper = null + node.parent.userData.ikRig = null + node.name = sceneObject.name || sceneObject.displayName + node.userData = {} + scene.add( node.parent ) - node.parent.bonesHelper = oldBonesHelper - node.parent.userData.ikRig = oldikRig } else { console.log('\t\tSkipping node', node) } @@ -100,6 +91,17 @@ const useExportToGltf = (sceneRef) => { embedImages: true } exporter.parse(scene, function (glb) { + + for (let memento of mementos) { + memento.parent.bonesHelper = memento.bonesHelper + memento.parent.userData.ikRig = memento.ikRig + + memento.mesh.name = memento.name + memento.mesh.userData = memento.userData + + sceneRef.current.add(memento.parent) + } + if (meta.storyboarderFilePath) { let timestamp = moment().format('YYYY-MM-DD hh.mm.ss') From 45374e64669362d0930669ac7ae879aa41a7e289 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Thu, 3 Oct 2019 12:23:09 -0500 Subject: [PATCH 09/16] simplify --- src/js/shot-generator/use-export-to-gltf.js | 40 +++++++++------------ 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index 77c4e64fba..1af6af3250 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -44,29 +44,23 @@ const useExportToGltf = (sceneRef) => { } else if (sceneObject.type === 'character') { console.log('\tCloning', sceneObject.type) - for (node of child.children) { - if (node.isSkinnedMesh) { - - mementos.push({ - parent: node.parent, - bonesHelper: node.parent.bonesHelper, - ikRig: node.parent.userData.ikRig, - - mesh: node, - name: node.name, - userData: node.userData - }) - - node.parent.bonesHelper = null - node.parent.userData.ikRig = null - node.name = sceneObject.name || sceneObject.displayName - node.userData = {} - scene.add( node.parent ) - - } else { - console.log('\t\tSkipping node', node) - } - } + let skinnedMesh = child.getObjectByProperty('type', 'SkinnedMesh') + + mementos.push({ + parent: skinnedMesh.parent, + bonesHelper: skinnedMesh.parent.bonesHelper, + ikRig: skinnedMesh.parent.userData.ikRig, + + mesh: skinnedMesh, + name: skinnedMesh.name, + userData: skinnedMesh.userData + }) + + skinnedMesh.parent.bonesHelper = null + skinnedMesh.parent.userData.ikRig = null + skinnedMesh.name = sceneObject.name || sceneObject.displayName + skinnedMesh.userData = {} + scene.add( skinnedMesh.parent ) } else if (sceneObject) { console.log('\tCloning', sceneObject.type) From 7bb06da9d4943eb47422f9ba36b1dcdc5fe70420 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Thu, 3 Oct 2019 12:27:12 -0500 Subject: [PATCH 10/16] fix Character name export --- src/js/shot-generator/use-export-to-gltf.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index 1af6af3250..dad295723e 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -50,15 +50,15 @@ const useExportToGltf = (sceneRef) => { parent: skinnedMesh.parent, bonesHelper: skinnedMesh.parent.bonesHelper, ikRig: skinnedMesh.parent.userData.ikRig, + name: skinnedMesh.parent.name, mesh: skinnedMesh, - name: skinnedMesh.name, userData: skinnedMesh.userData }) skinnedMesh.parent.bonesHelper = null skinnedMesh.parent.userData.ikRig = null - skinnedMesh.name = sceneObject.name || sceneObject.displayName + skinnedMesh.parent.name = sceneObject.name || sceneObject.displayName skinnedMesh.userData = {} scene.add( skinnedMesh.parent ) @@ -89,8 +89,8 @@ const useExportToGltf = (sceneRef) => { for (let memento of mementos) { memento.parent.bonesHelper = memento.bonesHelper memento.parent.userData.ikRig = memento.ikRig + memento.parent.name = memento.name - memento.mesh.name = memento.name memento.mesh.userData = memento.userData sceneRef.current.add(memento.parent) From 820c302fe5a45de2cf8e121d8e2ad46588741c19 Mon Sep 17 00:00:00 2001 From: Eric Skogen Date: Thu, 3 Oct 2019 13:07:48 -0500 Subject: [PATCH 11/16] clear userdata before exporting --- src/js/shot-generator/use-export-to-gltf.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index dad295723e..9eb8d0e03c 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -49,17 +49,21 @@ const useExportToGltf = (sceneRef) => { mementos.push({ parent: skinnedMesh.parent, bonesHelper: skinnedMesh.parent.bonesHelper, - ikRig: skinnedMesh.parent.userData.ikRig, + userData: skinnedMesh.parent.userData, name: skinnedMesh.parent.name, mesh: skinnedMesh, - userData: skinnedMesh.userData + meshUserData: skinnedMesh.userData }) skinnedMesh.parent.bonesHelper = null - skinnedMesh.parent.userData.ikRig = null skinnedMesh.parent.name = sceneObject.name || sceneObject.displayName + skinnedMesh.parent.userData = {} + skinnedMesh.userData = {} + + console.log('\t\tAdded', skinnedMesh.parent) + scene.add( skinnedMesh.parent ) } else if (sceneObject) { @@ -88,10 +92,10 @@ const useExportToGltf = (sceneRef) => { for (let memento of mementos) { memento.parent.bonesHelper = memento.bonesHelper - memento.parent.userData.ikRig = memento.ikRig + memento.parent.userData = memento.userData memento.parent.name = memento.name - memento.mesh.userData = memento.userData + memento.mesh.userData = memento.meshUserData sceneRef.current.add(memento.parent) } From 058af1c08f4b29bfc0a45b41f6aacb0d83b5f8f5 Mon Sep 17 00:00:00 2001 From: Vlad Stepura Date: Mon, 21 Oct 2019 13:42:06 +0300 Subject: [PATCH 12/16] Fixed gltfExport morphtargets ACCESSOR_NON_UNIT --- src/js/shot-generator/use-export-to-gltf.js | 27 ++++++++++--------- .../examples/js/exporters/GLTFExporter.js | 16 ++--------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index 9eb8d0e03c..e8fb4bdb66 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -5,6 +5,7 @@ const path = require('path') const fs = require('fs-extra') const moment = require('moment') +const {setZDirecion, setReverseZ} = require( "./IK/utils/axisUtils"); const THREE = require('three') window.THREE = window.THREE || THREE require('../vendor/three/examples/js/exporters/GLTFExporter.js') @@ -45,13 +46,13 @@ const useExportToGltf = (sceneRef) => { console.log('\tCloning', sceneObject.type) let skinnedMesh = child.getObjectByProperty('type', 'SkinnedMesh') - + mementos.push({ parent: skinnedMesh.parent, bonesHelper: skinnedMesh.parent.bonesHelper, userData: skinnedMesh.parent.userData, name: skinnedMesh.parent.name, - + scale: skinnedMesh.parent.scale.clone(), mesh: skinnedMesh, meshUserData: skinnedMesh.userData }) @@ -63,40 +64,40 @@ const useExportToGltf = (sceneRef) => { skinnedMesh.userData = {} console.log('\t\tAdded', skinnedMesh.parent) - + skinnedMesh.geometry.normalizeNormals(); scene.add( skinnedMesh.parent ) - + } else if (sceneObject) { console.log('\tCloning', sceneObject.type) let clone = child.clone() - + clone.userData = {} - + clone.material = new THREE.MeshStandardMaterial() clone.name = sceneObject.name || sceneObject.displayName - + scene.add(clone) } } } } - + console.log('\tExporting Scene:', scene) - + let exporter = new THREE.GLTFExporter() let options = { binary: true, - embedImages: true + embedImages: true, + } exporter.parse(scene, function (glb) { - + for (let memento of mementos) { memento.parent.bonesHelper = memento.bonesHelper memento.parent.userData = memento.userData memento.parent.name = memento.name - memento.mesh.userData = memento.meshUserData - + sceneRef.current.add(memento.parent) } diff --git a/src/js/vendor/three/examples/js/exporters/GLTFExporter.js b/src/js/vendor/three/examples/js/exporters/GLTFExporter.js index 49af98dcb6..d99d643a04 100644 --- a/src/js/vendor/three/examples/js/exporters/GLTFExporter.js +++ b/src/js/vendor/three/examples/js/exporters/GLTFExporter.js @@ -615,7 +615,7 @@ THREE.GLTFExporter.prototype = { * @return {Integer} Index of the processed accessor on the "accessors" array */ function processAccessor( attribute, geometry, start, count ) { - + var types = { 1: 'SCALAR', @@ -677,7 +677,6 @@ THREE.GLTFExporter.prototype = { } var minMax = getMinMax( attribute, start, count ); - var bufferViewTarget; // If geometry isn't provided, don't infer the target usage of the bufferView. For @@ -689,7 +688,6 @@ THREE.GLTFExporter.prototype = { } var bufferView = processBufferView( attribute, componentType, start, count, bufferViewTarget ); - var gltfAccessor = { bufferView: bufferView.id, @@ -701,7 +699,6 @@ THREE.GLTFExporter.prototype = { type: types[ attribute.itemSize ] }; - if ( ! outputJSON.accessors ) { outputJSON.accessors = []; @@ -847,7 +844,6 @@ THREE.GLTFExporter.prototype = { * @return {Integer} Index of the processed texture in the "textures" array */ function processTexture( map ) { - if ( cachedData.textures.has( map ) ) { return cachedData.textures.get( map ); @@ -882,7 +878,6 @@ THREE.GLTFExporter.prototype = { * @return {Integer} Index of the processed material in the "materials" array */ function processMaterial( material ) { - if ( cachedData.materials.has( material ) ) { return cachedData.materials.get( material ); @@ -1086,7 +1081,6 @@ THREE.GLTFExporter.prototype = { * @return {Integer} Index of the processed mesh in the "meshes" array */ function processMesh( mesh ) { - var cacheKey = mesh.geometry.uuid + ':' + mesh.material.uuid; if ( cachedData.meshes.has( cacheKey ) ) { @@ -1181,7 +1175,6 @@ THREE.GLTFExporter.prototype = { var attribute = geometry.attributes[ attributeName ]; attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase(); - // Prefix all geometry attributes except the ones specifically // listed in the spec; non-spec attributes are considered custom. var validVertexAttributes = @@ -1279,7 +1272,7 @@ THREE.GLTFExporter.prototype = { // glTF 2.0 Specification: // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets - var baseAttribute = geometry.attributes[ attributeName ]; + var baseAttribute = geometry.attributes[ attributeName ].clone(); if ( cachedData.attributes.has( getUID( attribute ) ) ) { @@ -1301,7 +1294,6 @@ THREE.GLTFExporter.prototype = { ); } - target[ gltfAttributeName ] = processAccessor( relativeAttribute, geometry ); cachedData.attributes.set( getUID( baseAttribute ), target[ gltfAttributeName ] ); @@ -1498,7 +1490,6 @@ THREE.GLTFExporter.prototype = { * @return {number} */ function processAnimation( clip, root ) { - if ( ! outputJSON.animations ) { outputJSON.animations = []; @@ -1607,7 +1598,6 @@ THREE.GLTFExporter.prototype = { } function processSkin( object ) { - var node = outputJSON.nodes[ nodeMap.get( object ) ]; var skeleton = object.skeleton; @@ -1631,7 +1621,6 @@ THREE.GLTFExporter.prototype = { outputJSON.skins = []; } - outputJSON.skins.push( { inverseBindMatrices: processAccessor( new THREE.BufferAttribute( inverseBindMatrices, 16 ) ), @@ -1705,7 +1694,6 @@ THREE.GLTFExporter.prototype = { * @return {Integer} Index of the node in the nodes list */ function processNode( object ) { - if ( ! outputJSON.nodes ) { outputJSON.nodes = []; From 41742f116a61edb121911eeda2b4893aac9cbb2a Mon Sep 17 00:00:00 2001 From: Vlad Stepura Date: Tue, 5 Nov 2019 09:34:45 +0200 Subject: [PATCH 13/16] Implemented characters marker on export --- src/js/shot-generator/use-export-to-gltf.js | 38 ++++----------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index e8fb4bdb66..5b6bf16ca5 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -5,7 +5,6 @@ const path = require('path') const fs = require('fs-extra') const moment = require('moment') -const {setZDirecion, setReverseZ} = require( "./IK/utils/axisUtils"); const THREE = require('three') window.THREE = window.THREE || THREE require('../vendor/three/examples/js/exporters/GLTFExporter.js') @@ -31,7 +30,6 @@ const useExportToGltf = (sceneRef) => { }) console.log('Preparing GLTF…') - let mementos = [] let scene = new THREE.Scene() for (let child of sceneRef.current.children) { // console.log('\tScene contains:', child) @@ -46,26 +44,12 @@ const useExportToGltf = (sceneRef) => { console.log('\tCloning', sceneObject.type) let skinnedMesh = child.getObjectByProperty('type', 'SkinnedMesh') - - mementos.push({ - parent: skinnedMesh.parent, - bonesHelper: skinnedMesh.parent.bonesHelper, - userData: skinnedMesh.parent.userData, - name: skinnedMesh.parent.name, - scale: skinnedMesh.parent.scale.clone(), - mesh: skinnedMesh, - meshUserData: skinnedMesh.userData - }) - - skinnedMesh.parent.bonesHelper = null - skinnedMesh.parent.name = sceneObject.name || sceneObject.displayName - skinnedMesh.parent.userData = {} - - skinnedMesh.userData = {} - - console.log('\t\tAdded', skinnedMesh.parent) - skinnedMesh.geometry.normalizeNormals(); - scene.add( skinnedMesh.parent ) + + let simpleMesh = new THREE.Mesh(skinnedMesh.geometry, new THREE.MeshStandardMaterial()) + simpleMesh.scale.copy(skinnedMesh.worldScale()) + simpleMesh.quaternion.copy(skinnedMesh.worldQuaternion()) + simpleMesh.position.copy(skinnedMesh.worldPosition()) + scene.add( simpleMesh) } else if (sceneObject) { console.log('\tCloning', sceneObject.type) @@ -88,18 +72,8 @@ const useExportToGltf = (sceneRef) => { let options = { binary: true, embedImages: true, - } exporter.parse(scene, function (glb) { - - for (let memento of mementos) { - memento.parent.bonesHelper = memento.bonesHelper - memento.parent.userData = memento.userData - memento.parent.name = memento.name - memento.mesh.userData = memento.meshUserData - - sceneRef.current.add(memento.parent) - } if (meta.storyboarderFilePath) { From 907369cbd34a80b0e8902aebc3519c2f72978d4f Mon Sep 17 00:00:00 2001 From: Vlad Stepura Date: Tue, 5 Nov 2019 09:53:10 +0200 Subject: [PATCH 14/16] Made gpu picker pick multimeshes objects --- .../src/three/GPUPickers/Controllers/XRGPUPickerController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/xr/src/three/GPUPickers/Controllers/XRGPUPickerController.js b/src/js/xr/src/three/GPUPickers/Controllers/XRGPUPickerController.js index d65022239a..674be2c8e5 100644 --- a/src/js/xr/src/three/GPUPickers/Controllers/XRGPUPickerController.js +++ b/src/js/xr/src/three/GPUPickers/Controllers/XRGPUPickerController.js @@ -120,8 +120,8 @@ class XRGPUPickerController extends GPUPickerController meshes.push(this.gpuPickerFactory.createGUI(sceneMesh, this.idPool, excludingList)); break; case 'object': - meshes.push(this.gpuPickerFactory.createObject(sceneMesh, excludingList)); - break; + // meshes.push(this.gpuPickerFactory.createObject(sceneMesh, excludingList)); + // break; case 'virtual-camera': case 'light': default: From 04eeac6ed75d77e24abd34a2a864f38e5460b7d9 Mon Sep 17 00:00:00 2001 From: Vlad Stepura Date: Tue, 5 Nov 2019 16:24:38 +0200 Subject: [PATCH 15/16] Added export of walls and ground --- src/js/shot-generator/use-export-to-gltf.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index 5b6bf16ca5..eadccb7c80 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -20,7 +20,6 @@ const useExportToGltf = (sceneRef) => { const meta = useSelector(state => state.meta) const board = useSelector(state => state.board) const sceneObjects = useSelector(getSceneObjects) - useEffect(() => { if (board && meta && meta.storyboarderFilePath) { ipcRenderer.on('shot-generator:export-gltf', () => { @@ -28,8 +27,6 @@ const useExportToGltf = (sceneRef) => { message: 'Preparing to export GLTF…', timing: 5 }) - - console.log('Preparing GLTF…') let scene = new THREE.Scene() for (let child of sceneRef.current.children) { // console.log('\tScene contains:', child) @@ -38,10 +35,8 @@ const useExportToGltf = (sceneRef) => { if (child.userData.id && sceneObjects[child.userData.id]) { let sceneObject = sceneObjects[child.userData.id] if (sceneObject.type === 'volume') { - console.log('\tSkipping', sceneObject.type) } else if (sceneObject.type === 'character') { - console.log('\tCloning', sceneObject.type) let skinnedMesh = child.getObjectByProperty('type', 'SkinnedMesh') @@ -52,7 +47,6 @@ const useExportToGltf = (sceneRef) => { scene.add( simpleMesh) } else if (sceneObject) { - console.log('\tCloning', sceneObject.type) let clone = child.clone() clone.userData = {} @@ -62,7 +56,12 @@ const useExportToGltf = (sceneRef) => { scene.add(clone) } - } + } else if (child.userData.type === 'ground' || (child.geometry && child.geometry instanceof THREE.ExtrudeGeometry)) { + let clone = child.clone() + + clone.userData = {} + scene.add(clone) + } } } @@ -100,7 +99,6 @@ const useExportToGltf = (sceneRef) => { } return function cleanup() { - console.log('cleanup shot-generator:export-gltf') ipcRenderer.removeAllListeners('shot-generator:export-gltf') } }, [board, meta, sceneObjects]) From d3813483a374ced09db3a009d183926cead717c6 Mon Sep 17 00:00:00 2001 From: Vlad Stepura Date: Wed, 6 Nov 2019 10:09:56 +0200 Subject: [PATCH 16/16] Added virtual-camera model as camera on export --- src/js/services/model-loader.js | 3 +- src/js/shot-generator/Editor.js | 4 +- src/js/shot-generator/use-export-to-gltf.js | 78 +++++++++++++++++++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/src/js/services/model-loader.js b/src/js/services/model-loader.js index c2432eee2f..7f8722fa28 100644 --- a/src/js/services/model-loader.js +++ b/src/js/services/model-loader.js @@ -235,7 +235,8 @@ const pathToShotGeneratorData = // calculate filepath const builtInFolder = type => ({ 'object': path.join(pathToShotGeneratorData, 'objects'), - 'character': path.join(pathToShotGeneratorData, 'dummies', 'gltf') + 'character': path.join(pathToShotGeneratorData, 'dummies', 'gltf'), + 'xr': path.join(pathToShotGeneratorData, 'xr') }[type]) const projectFolder = type => ({ diff --git a/src/js/shot-generator/Editor.js b/src/js/shot-generator/Editor.js index 3481692f7f..d42d50a70d 100644 --- a/src/js/shot-generator/Editor.js +++ b/src/js/shot-generator/Editor.js @@ -35,7 +35,7 @@ const Icon = require('./Icon') const Toolbar = require('./Toolbar') const FatalErrorBoundary = require('./FatalErrorBoundary') -const useExportToGltf = require('./use-export-to-gltf') +const {useExportToGltf, loadCameraModel} = require('./use-export-to-gltf') const ModelLoader = require('../services/model-loader') @@ -391,7 +391,7 @@ const Editor = connect( // TODO cancellation (e.g.: redux-saga) const loadSceneObjects = async (dispatch, state) => { let storyboarderFilePath = state.meta.storyboarderFilePath - + loadCameraModel(storyboarderFilePath) const loadables = Object.values(sceneObjects) // has a value for model .filter(o => o.model != null) diff --git a/src/js/shot-generator/use-export-to-gltf.js b/src/js/shot-generator/use-export-to-gltf.js index eadccb7c80..bb69af9602 100644 --- a/src/js/shot-generator/use-export-to-gltf.js +++ b/src/js/shot-generator/use-export-to-gltf.js @@ -9,17 +9,80 @@ const THREE = require('three') window.THREE = window.THREE || THREE require('../vendor/three/examples/js/exporters/GLTFExporter.js') require('../vendor/three/examples/js/utils/SkeletonUtils') - +const {gltfLoader} = require('./Components') const { getSceneObjects } = require('../shared/reducers/shot-generator') const notifications = require('../window/notifications') -const useExportToGltf = (sceneRef) => { + +const ModelLoader = require('../services/model-loader') + +const materialFactory = () => new THREE.MeshBasicMaterial({ + color: 0x8c78f1, + flatShading: false +}) + +const meshFactory = originalMesh => { + let mesh = originalMesh.clone() + mesh.geometry.computeBoundingBox() + + // create a skeleton if one is not provided + if (mesh instanceof THREE.SkinnedMesh && !mesh.skeleton) { + mesh.skeleton = new THREE.Skeleton() + } + + let material = materialFactory() + + if (mesh.material.map) { + material.map = mesh.material.map + material.map.needsUpdate = true + } + mesh.material = material + + return mesh +} + + + +let virtualCameraObject = null +const loadModels = (models) => { + return Promise.all( + models.map((modelPath) => { + return new Promise((resolve, reject) => { + gltfLoader.load( + modelPath, + modelData => resolve(modelData.scene), + null, + reject + ) + }) + }) + ) +} + +const loadCameraModel = (storyboarderFilePath) => { + let expectedCameraFilepath = ModelLoader.getFilepathForModel({ + model: 'virtual-camera', + type: 'xr' + }, { storyboarderFilePath }) + loadModels([expectedCameraFilepath ]).then(([virtualCamera]) => { + virtualCameraObject = new THREE.Object3D() + virtualCamera.traverse( function ( child ) { + if ( child instanceof THREE.Mesh ) { + let mesh = meshFactory(child) + virtualCameraObject.add(mesh) + } + }) + }) +} + +const useExportToGltf = (sceneRef, storyboarderFilePath) => { const meta = useSelector(state => state.meta) const board = useSelector(state => state.board) const sceneObjects = useSelector(getSceneObjects) + useEffect(() => { if (board && meta && meta.storyboarderFilePath) { ipcRenderer.on('shot-generator:export-gltf', () => { @@ -29,7 +92,6 @@ const useExportToGltf = (sceneRef) => { }) let scene = new THREE.Scene() for (let child of sceneRef.current.children) { - // console.log('\tScene contains:', child) // HACK test to avoid IconSprites, which fail to .clone if (!child.icon) { if (child.userData.id && sceneObjects[child.userData.id]) { @@ -46,6 +108,12 @@ const useExportToGltf = (sceneRef) => { simpleMesh.position.copy(skinnedMesh.worldPosition()) scene.add( simpleMesh) + } else if (sceneObject.type === "camera") { + let camera = virtualCameraObject.clone() + camera.position.copy(child.worldPosition()) + camera.quaternion.copy(child.worldQuaternion()) + camera.scale.copy(child.worldScale()) + scene.add(camera) } else if (sceneObject) { let clone = child.clone() @@ -65,8 +133,6 @@ const useExportToGltf = (sceneRef) => { } } - console.log('\tExporting Scene:', scene) - let exporter = new THREE.GLTFExporter() let options = { binary: true, @@ -104,4 +170,4 @@ const useExportToGltf = (sceneRef) => { }, [board, meta, sceneObjects]) } -module.exports = useExportToGltf +module.exports = {useExportToGltf, loadCameraModel}