2 Aug 2017
Forge Viewer Skybox and Camera animation

The question of how to create a skybox in the Forge Viewer popped up a while ago on Stackoverflow so I decided to give it a try. ..
Adding a Skybox in Three.js is rather straightforward, I created the small util class below to handle that:
class ViewerSkybox { | |
constructor (viewer, options) { | |
const faceMaterials = options.imageList.map((url) => { | |
return new THREE.MeshBasicMaterial({ | |
map: THREE.ImageUtils.loadTexture(url), | |
side: THREE.BackSide | |
}) | |
}) | |
const skyMaterial = new THREE.MeshFaceMaterial( | |
faceMaterials) | |
const geometry = new THREE.CubeGeometry( | |
options.size.x, | |
options.size.y, | |
options.size.z, | |
1, 1, 1, | |
null, true) | |
const skybox = new THREE.Mesh( | |
geometry, skyMaterial) | |
viewer.impl.scene.add(skybox) | |
} | |
} |
However when adding a custom mesh to the scene the viewer is not automatically updating the scene bounds, so far/near clipping planes will still be too close to the model in order to show the skybox properly. An easy way to workaround is to load a second model to the scene. I then created a "container" model which only has two tiny cubes positioned at the desired extends (-500, -500, -500) and (500, 500, 500) and load along the first model.
Once the bigger model is loaded the navigation tool will zoom in/out too fast because it is now calibrated to the container model, this can be fixed with viewer.navigation.setBounds.
To make the demo a bit more exciting I wanted to add some controlled camera motion around the world Y axis so it's spinning while looking at the model. In Three.js you would only transform the camera position, however in the Forge Viewer you also need to apply the same transform to the up vector.
Finally I used CAMERA_CHANGE_EVENT to constrain the position so user cannot zoom too close or too far from the model. It's not perfect as a probably more powerful way to achieve that would be to implement a custom navigation tool but that a fair piece of additional work, so I'll leave that for later...
Here is the complete code and a live demo for you to play with!
///////////////////////////////////////////////////////////////////// | |
// Viewing.Extension.Skybox | |
// by Philippe Leefsma, July 2017 | |
// | |
///////////////////////////////////////////////////////////////////// | |
import MultiModelExtensionBase from 'Viewer.MultiModelExtensionBase' | |
import xpos from './img/bridge/skybox-xpos.png' | |
import xneg from './img/bridge/skybox-xneg.png' | |
import ypos from './img/bridge/skybox-ypos.png' | |
import yneg from './img/bridge/skybox-yneg.png' | |
import zpos from './img/bridge/skybox-zpos.png' | |
import zneg from './img/bridge/skybox-zneg.png' | |
import EventTool from 'Viewer.EventTool' | |
import Skybox from 'Viewer.Skybox' | |
import Stopwatch from 'Stopwatch' | |
class SkyboxExtension extends MultiModelExtensionBase { | |
///////////////////////////////////////////////////////// | |
// Class constructor | |
// | |
///////////////////////////////////////////////////////// | |
constructor(viewer, options) { | |
super (viewer, options) | |
this.onCameraChanged = | |
this.onCameraChanged.bind(this) | |
this.runAnimation = | |
this.runAnimation.bind(this) | |
this.eventTool = new EventTool(viewer) | |
const imageList = [ | |
xpos, xneg, | |
ypos, yneg, | |
zpos, zneg | |
] | |
const size = new THREE.Vector3() | |
size.fromArray(options.size || [10000, 10000, 10000]) | |
this.skybox = new Skybox(viewer, { | |
imageList, | |
size | |
}) | |
this.stopwatch = new Stopwatch() | |
} | |
///////////////////////////////////////////////////////// | |
// Extension Id | |
// | |
///////////////////////////////////////////////////////// | |
static get ExtensionId() { | |
return 'Viewing.Extension.Skybox' | |
} | |
///////////////////////////////////////////////////////// | |
// Load callback | |
// | |
///////////////////////////////////////////////////////// | |
load() { | |
console.log('Viewing.Extension.Skybox loaded') | |
this.eventTool.on('mousewheel', (e) => { | |
window.clearTimeout(this.timeoutId) | |
this.timeoutId = window.setTimeout(() => { | |
this.stopwatch.getElapsedMs() | |
this.userInteraction = false | |
this.runAnimation() | |
}, 3500) | |
this.userInteraction = true | |
return false | |
}) | |
this.eventTool.on('buttondown', (e) => { | |
window.clearTimeout(this.timeoutId) | |
this.userInteraction = true | |
return false | |
}) | |
this.eventTool.on('buttonup', (e) => { | |
this.timeoutId = window.setTimeout(() => { | |
this.stopwatch.getElapsedMs() | |
this.runAnimation() | |
}, 3500) | |
this.userInteraction = false | |
return false | |
}) | |
this.viewer.addEventListener( | |
Autodesk.Viewing.CAMERA_CHANGE_EVENT, | |
this.onCameraChanged) | |
return true | |
} | |
///////////////////////////////////////////////////////// | |
// Setup navigation | |
// | |
///////////////////////////////////////////////////////// | |
configureNavigation () { | |
const nav = this.viewer.navigation | |
nav.setLockSettings({ | |
pan: true | |
}) | |
this.bounds = new THREE.Box3( | |
new THREE.Vector3(-100, -100, -100), | |
new THREE.Vector3(100, 100, 100)) | |
nav.fitBounds(true, this.bounds) | |
this.viewer.setViewCube('front') | |
nav.toPerspective() | |
setTimeout(() => { | |
this.viewer.autocam.setHomeViewFrom( | |
nav.getCamera()) | |
this.options.loader.show(false) | |
}, 2000) | |
} | |
///////////////////////////////////////////////////////// | |
// Model completed load callback | |
// | |
///////////////////////////////////////////////////////// | |
onModelCompletedLoad (event) { | |
if (event.model.dbModelId) { | |
this.loadContainer(this.options.containerURN).then( | |
() => { | |
this.configureNavigation() | |
}) | |
this.stopwatch.getElapsedMs() | |
this.eventTool.activate() | |
this.runAnimation() | |
} | |
} | |
///////////////////////////////////////////////////////// | |
// Load container model | |
// | |
///////////////////////////////////////////////////////// | |
loadContainer (urn) { | |
return new Promise(async(resolve) => { | |
const doc = await this.options.loadDocument(urn) | |
const path = this.options.getViewablePath(doc) | |
this.viewer.loadModel(path, {}, (model) => { | |
resolve (model) | |
}) | |
}) | |
} | |
///////////////////////////////////////////////////////// | |
// Unload callback | |
// | |
///////////////////////////////////////////////////////// | |
unload() { | |
console.log('Viewing.Extension.Skybox unloaded') | |
window.cancelAnimationFrame(this.animId) | |
this.viewer.removeEventListener( | |
Autodesk.Viewing.CAMERA_CHANGE_EVENT, | |
this.onCameraChanged) | |
this.userInteraction = true | |
this.eventTool.off() | |
} | |
///////////////////////////////////////////////////////// | |
// Clamp vector length, not avail in three.js version | |
// used by the viewer | |
// | |
///////////////////////////////////////////////////////// | |
clampLength(vector, min, max ) { | |
const length = vector.length() | |
vector.divideScalar(length || 1) | |
vector.multiplyScalar( | |
Math.max(min, Math.min(max, length))) | |
} | |
///////////////////////////////////////////////////////// | |
// Camera changed event | |
// | |
///////////////////////////////////////////////////////// | |
onCameraChanged () { | |
const nav = this.viewer.navigation | |
const pos = nav.getPosition() | |
if (pos.length() > 700.0 || pos.length() < 100.0) { | |
this.clampLength(pos, 100.0, 700.0) | |
nav.fitBounds(true, this.bounds) | |
nav.setView(pos, new THREE.Vector3(0,0,0)) | |
} | |
} | |
///////////////////////////////////////////////////////// | |
// Rotate camera around axis | |
// | |
///////////////////////////////////////////////////////// | |
rotateCamera (axis, speed, dt) { | |
const nav = this.viewer.navigation | |
const up = nav.getCameraUpVector() | |
const pos = nav.getPosition() | |
const matrix = new THREE.Matrix4().makeRotationAxis( | |
axis, speed * dt); | |
pos.applyMatrix4(matrix) | |
up.applyMatrix4(matrix) | |
nav.setView(pos, new THREE.Vector3(0,0,0)) | |
nav.setCameraUpVector(up) | |
} | |
///////////////////////////////////////////////////////// | |
// starts animation | |
// | |
///////////////////////////////////////////////////////// | |
runAnimation () { | |
if (!this.userInteraction) { | |
const dt = this.stopwatch.getElapsedMs() * 0.001 | |
const axis = new THREE.Vector3(0,1,0) | |
this.rotateCamera(axis, 10.0 * Math.PI/180, dt) | |
this.animId = window.requestAnimationFrame( | |
this.runAnimation) | |
} | |
} | |
} | |
Autodesk.Viewing.theExtensionManager.registerExtension( | |
SkyboxExtension.ExtensionId, | |
SkyboxExtension) |