Skip to content

Commit

Permalink
Merge pull request #1849 from wonderunit/xr-gltf-export
Browse files Browse the repository at this point in the history
GLTF Export
  • Loading branch information
setpixel authored Nov 6, 2019
2 parents ef3b234 + d381348 commit 0916d33
Show file tree
Hide file tree
Showing 8 changed files with 2,465 additions and 4 deletions.
6 changes: 6 additions & 0 deletions src/js/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
}
]
},
Expand Down
3 changes: 2 additions & 1 deletion src/js/services/model-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => ({
Expand Down
6 changes: 5 additions & 1 deletion src/js/shot-generator/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const Icon = require('./Icon')
const Toolbar = require('./Toolbar')
const FatalErrorBoundary = require('./FatalErrorBoundary')

const {useExportToGltf, loadCameraModel} = require('./use-export-to-gltf')

const ModelLoader = require('../services/model-loader')

const h = require('../utils/h')
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -387,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)
Expand Down
173 changes: 173 additions & 0 deletions src/js/shot-generator/use-export-to-gltf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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')
require('../vendor/three/examples/js/utils/SkeletonUtils')
const {gltfLoader} = require('./Components')
const {
getSceneObjects
} = require('../shared/reducers/shot-generator')

const notifications = require('../window/notifications')


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', () => {
notifications.notify({
message: 'Preparing to export GLTF…',
timing: 5
})
let scene = new THREE.Scene()
for (let child of sceneRef.current.children) {
// HACK test to avoid IconSprites, which fail to .clone
if (!child.icon) {
if (child.userData.id && sceneObjects[child.userData.id]) {
let sceneObject = sceneObjects[child.userData.id]
if (sceneObject.type === 'volume') {

} else if (sceneObject.type === 'character') {

let skinnedMesh = child.getObjectByProperty('type', 'SkinnedMesh')

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.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()

clone.userData = {}

clone.material = new THREE.MeshStandardMaterial()
clone.name = sceneObject.name || sceneObject.displayName

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)
}
}
}

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, Buffer.from(glb))

notifications.notify({
message: `Exported to:\n${filename}`,
timing: 5
})

shell.showItemInFolder(filepath)
}
}, options)
})
}

return function cleanup() {
ipcRenderer.removeAllListeners('shot-generator:export-gltf')
}
}, [board, meta, sceneObjects])
}

module.exports = {useExportToGltf, loadCameraModel}
Loading

0 comments on commit 0916d33

Please sign in to comment.