This article will provide an example of how to use code to animate switching from one camera to another camera.
Pre-Reading
This example in this article will utilize Threejs, 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.
The camera change above is achieved by a Configuration Rule executing a custom script every time the Camera attribute value changes. 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 = getQuaternion(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 getQuaternion(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;
}
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
- This code assumes we do not wish to store camera position changes. Users are placed at the same default position each camera change and the orbit/zoom limits are always the same.
- Example: 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.
- Initial position null values must be created to reset camera movement data on the scene graph in order to preserve a Keep Player Changes = False state such that cameras revert to their starting values when users switch between camera options.
- If the start and end cameras do not represent 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.