24 Oct 2017

Smooth Camera Transitions in the Forge Viewer

Default blog image

Last week I had a question about how to animate the Forge Viewer camera along a path to produce a fly-through effect, so I decided to give it a try. I already had sample code for animating the camera: Forge Viewer Skybox and Camera animation, but the path animation was hardcoded and could not be customized, so this is a different approach here.

It's also the opportunity to play with a cool and popular library (almost 5000 github stars) named Tween.js: it is aimed to create smooth parametrized transition, so called tween, between a starting and a target state. 

The idea is to create an interface to let the user save viewer states and use the viewport parameters to transition between current camera state to the next. Transitions can be basic or more complex and all kind of customisation is doable using Tween.js. Luckily, the viewer API is using only three vectors to define the camera state: position, target, and upVector, so there is no need to tween a rotation matrix or a quaternion. Tweening a vector is super easy so I created the function below that takes a state obtained from viewer.getState() and tween from current states to that target state:

/////////////////////////////////////////////////////////
// Smooth camera transition from current state to
// target state using Tween.js
//
/////////////////////////////////////////////////////////
tweenCameraTo (state) {

    // tween parameters, specific to my app but easy
    // to adapt ...
    const {

      targetTweenDuration,
      posTweenDuration,
      upTweenDuration,

      targetTweenEasing,
      posTweenEasing,
      upTweenEasing

    } = this.react.getState()

    const targetEnd = new THREE.Vector3(
      state.viewport.target[0],
      state.viewport.target[1],
      state.viewport.target[2])

    const posEnd = new THREE.Vector3(
      state.viewport.eye[0],
      state.viewport.eye[1],
      state.viewport.eye[2])

    const upEnd = new THREE.Vector3(
      state.viewport.up[0],
      state.viewport.up[1],
      state.viewport.up[2])

    const nav = this.navigation

    const target = new THREE.Vector3().copy(
      nav.getTarget())

    const pos = new THREE.Vector3().copy(
      nav.getPosition())

    const up = new THREE.Vector3().copy(
      nav.getCameraUpVector())


    const targetTween = this.createTween({
      easing: targetTweenEasing.id,
      onUpdate: (v) => {
        nav.setTarget(v)
      },
      duration: targetTweenDuration,
      object: target,
      to: targetEnd
    })

    const posTween = this.createTween({
      easing: posTweenEasing.id,
      onUpdate: (v) => {
        nav.setPosition(v)
      },
      duration: posTweenDuration,
      object: pos,
      to: posEnd
    })

    const upTween = this.createTween({
      easing: upTweenEasing.id,
      onUpdate: (v) => {
        nav.setCameraUpVector(v)
      },
      duration: upTweenDuration,
      object: up,
      to: upEnd
    })

    Promise.all([
      targetTween,
      posTween,
      upTween]).then(() => {

      this.animate = false
    })

    this.runAnimation(true)
  }

I also wrapped the tween creation in a function that returns a Promise, so it's very convenient to handle multiple tweens running in parallel which can have a different execution time and easily trigger code once they have all completed, in that scenario running the transition to the next state:

/////////////////////////////////////////////////////////
// Promisified Tween
//
/////////////////////////////////////////////////////////
createTween (params) {

    return new Promise ((resolve) => {

      new TWEEN.Tween(params.object)
        .to(params.to, params.duration)
        .onComplete(() => resolve())
        .onUpdate(params.onUpdate)
        .easing(params.easing)
        .start()
    })
  }

Saving/restoring viewer states is a no-brainer; however, creating a nice UI around it for the user and a server-side API to access your database for persistence is a non-trivial piece of work. Thanks to the flexibility of my extensions build on top of React, I was able to easily combine that new tween sample with my existing Viewing.Extension.ConfigManager and literally inject that extension UI into the new one, leveraging what I had done previously at no cost.

Here is the source of that new demo: Viewing.Extension.CameraTween the live version where you can play with and a demo of the sequence I created below:

Go ahead and give this a try for yourself.

Related Article