Camera Switch Animation
This article will provide an example of how to use code to animate switching a camera from one to another.
Pre-Reading
The Example in this article will utilize Three.js, the Player API, Animations, and the Camera API to find information about the camera and move/rotate it.
Change the Camera attribute value to move the camera from one camera to another.
This camera change is achieved by having a Configuration Rule that executes a custom script every time the Camera attribute value has changed. The code can be seen here
// Initialize all variables for animation callback at top level of javascript
let startCamera;
let endCamera;
let swapCamera;
let startTrans;
let startQuaternion;
let startTargetTrans;
let startTargetDist;
let endTrans;
let endQuaternion;
let endCameraBaseNode;
let endTargetDist;
let endTargetTrans;
let maxCameraTime = 2000;
let start = undefined;
// Initialize Configurator to get Cameras
const config = api.configurator.getConfiguration();
// Pull end camera value from string configuration attribute that holds names of all cameras
endCamName = config["Camera"];
// Get both current and end cameras into storage
endCamera = api.scene.get({from: api.instanceId, name: endCamName});
startCamera = api.scene.get({from: api.instanceId, id: api.configurator.player.cameraController.activeCamera});
// **IMPORTANT** Place a null with the same translation as each of your cameras' start translations. Name the nulls <Camera name>_Base_Null. Make sure your camera and the base null are on the same level in the scene graph. These cameras and nulls should *NOT* be parented to one another
endCameraBaseNode = api.scene.get({from: api.instanceId, name: endCamName + "_Base_Null"});
animateCameraTo(endCameraBaseNode);
function animateCameraTo(endCameraBaseNode){
// Initialize the camera to perform a swap between two locations
createAndSetSwapCamera(startCamera);
// Initialize information about start camera position/orientation
startTrans = api.camera.getPosition();
startQuaternion = api.camera.getQuaternion();
startTargetTrans = getTargetTranslation(startCamera);
startTargetDist = getDistanceBetween(startTrans, startTargetTrans);
// Initialize information about end camera position/orientation
endTrans = endCameraBaseNode.plugs.Transform[0].translation.valueOf();
endTargetTrans = getTargetTranslation(endCamera);
endQuaternion = getCamQuaternion(endTrans, endTargetTrans);
endTargetDist = getDistanceBetween(endTrans, endTargetTrans);
// Call animation callback function
window.requestAnimationFrame(camStep);
}
function camStep(timestamp){
// Get Start time for animation
if(start === undefined) start = timestamp;
// Calculate how far along camera movement should be based on desired time
const elapsed = timestamp - start;
const elapsedPercent = elapsed / maxCameraTime;
// Call easing function for smoother, more natural, movement
const animPercent = easeIn(elapsedPercent);
// Init variables for calculation of next camera position
let camTrans = new api.THREE.Vector3();
let targetTrans = new api.THREE.Vector3();
let camQuat = new api.THREE.Quaternion();
let directionVector = new api.THREE.Vector3(0,0,1);
// Linear interpolation between how far the camera is from its original target to distance from final target
targetDist = startTargetDist + ((endTargetDist - startTargetDist) * animPercent);
// Linear interpolation of the camera target from start camera's target to end camera's target
targetTrans.lerpVectors(startTargetTrans, endTargetTrans, animPercent);
// Linear interpolation of camera quaternion between start quaternion and end quaternion **NOTE WE DO NOT USE ROTATION**
api.THREE.Quaternion.slerp(startQuaternion, endQuaternion, camQuat, animPercent);
// Calculate camera position based on distance from target point between start target and end target
directionVector.applyQuaternion(camQuat);
camTrans.copy(targetTrans).addScaledVector(directionVector, targetDist);
if(elapsed < maxCameraTime){
// Set camera position & quaternion
api.camera.setPosition(camTrans);
api.camera.setQuaternion(camQuat);
window.requestAnimationFrame(camStep);
} else {
// Set active camera to final cam, delete temporary swap camera, and reset start variable for future animations
api.setActiveCamera(api.scene.get({from: api.instanceId, name: endCamera.name}).id);
api.scene.deleteNode(api.scene.get({from: api.instanceId, name: 'Swap_Cam'}));
start = undefined;
return;
}
}
function createAndSetSwapCamera(startCam){
let camCopy = JSON.parse(JSON.stringify(api.scene.get({from: api.instanceId, id: startCam.id})));
delete camCopy.id;
camCopy.name = 'Swap_Cam';
// Disable camera constraints because this could prevent camera movements while using cloned camera
camCopy.plugs.Camera[0].constraintLatitudeMode = 0;
camCopy.plugs.Camera[0].constraintLongitudeMode = 0;
newCamId = api.scene.addNode(camCopy, api.instanceId);
api.setActiveCamera(newCamId);
}
function getTargetTranslation(cam){
const targetId = api.scene.get({from: api.instanceId, name: cam.name, plug: "Camera", property: "targetNode"});
return targetId ? api.scene.get({from: api.instanceId, id: targetId, plug: "Transform", property: "translation"}) : {x: 0, y: 0, z: 0};
}
function getDistanceBetween(point1, point2){
return Math.sqrt(Math.pow(point2.x - point1.x,2) + Math.pow(point2.y - point1.y,2) + Math.pow(point2.z - point1.z,2));
}
function getCamQuaternion(camTrans, camTargetTrans){
// Calculate camera Quaternion based on distance and direction from camera target, Note: We do this because we cannot directly request the quaternion from a non-active camera
const camVector = {x: camTargetTrans.x - camTrans.x, y: camTargetTrans.y - camTrans.y, z: camTargetTrans.z - camTrans.z};
let euler = new api.THREE.Euler();
euler.setFromVector3(camVector, 'ZYX');
let camQuat = new api.THREE.Quaternion();
camQuat = camQuat.setFromEuler(euler);
return camQuat;
}
// Cubic easing function
function easeIn(x){
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
}
The structure of the scene in the example above is shown here. Note the two cameras and the base nulls for each camera with the same translations as the camera. The Target Node for each camera is the polyMesh object that shares the shape name with the camera.
Setup Requirements
The above code example can be copy-pasted into a new scene and work with minimal setup. The code uses the following assumptions:
- Each camera has a set Target Node and is pointed at that Target Node If there is no Target Node designated, then the origin {0,0,0} is used.
- For best results with this code, cameras should always be pointed directly at their desired target nodes.
- Each camera has Keep Player Changes = False
- Each camera has a null with the same translation in the scene as the camera. Those nulls are named <camera name>_Base_Null
- A configuration attribute named “Camera” exists on the scene. The options for the configuration attribute match the names of the Camera nodes.
- The script should be placed on a configuration rule with a condition that checks if attribute “Camera” has changed
Camera Limitations
- When changing the location of the camera in the Threekit player, the level of Zoom is not preserved. If you start with a camera, zoom out to a max zoom distance. After re-positioning the camera via setPosition(), you are able to zoom out the camera the full zoom distance yet again. As a result, this code assumes we do not wish to store camera position changes. So, users are placed at the same default position after each camera change, and the orbit/zoom limits are always the same.
- With Keep Player Changes = False, cameras will revert to their normal location when users switch back to the camera. However, until the camera is used again, the camera’s node on the scene graph will hold the updated location from the user’s camera movement. To get around this, the example requires the initial position nulls be created.
- If your start and end cameras do not have the same field of view, there will be a noticeable point at the end of the animation where the swap camera is switched to the final camera.