Skip to main content
Version: Next

Custom CameraManager

By default Cognite3DViewer uses DefaultCameraManager class to manage user interaction with the camera. In certain cases it can be useful to customize behaviour of the camera:

  • Need to provide custom THREE.PerspectiveCamera object to Cognite3DViewer
  • Need to extend camera controls to any specific case that is not covered by current implementation

Overview

To create a custom camera manager class, CameraManager interface from @cognite/reveal must be implemented and provided to Cognite3DViewer on construction using the cameraManager-option. You can also set camera manager in runtime by calling setCameraManager method of Cognite3DViewer class. Interface is defined here in the API reference.

Main implementation specific functions are setCameraState, fitCameraToBoundingBox and update. Setting state shouldn't be possible when rotation and target are passed at the same time because they could conflict. When implementing these functions you can use a helper class CameraManagerHelper that contains some useful methods.

Example implementation

Here is an example implementation of a custom camera manager that utilizes standard ThreeJS OrbitControls for mouse movement:

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

import {
CameraManager,
CameraManagerHelper,
CameraState,
CameraEventDelegate,
CameraChangeDelegate,
CameraStopDelegate,
CameraManagerEventType,
DebouncedCameraStopEventTrigger
} from '@cognite/reveal';

export class CustomCameraManager implements CameraManager {
private _domElement: HTMLElement;
private _camera: THREE.PerspectiveCamera;
private _controls: OrbitControls;
private readonly _cameraChangedListener: Array<CameraChangeDelegate> = [];
private readonly _stopEventTrigger: DebouncedCameraStopEventTrigger;

constructor(domElement: HTMLElement, camera: THREE.PerspectiveCamera) {
this._domElement = domElement;
this._camera = camera;
this._controls = new OrbitControls(this._camera, domElement);
this._controls.enableDamping = true;
this._controls.dampingFactor = 0.3;

this._stopEventTrigger = new DebouncedCameraStopEventTrigger(this);

this._controls.addEventListener('change', () => {
this._cameraChangedListener.forEach(cb => cb(this._camera.position, this._controls.target));
});
}

get enabled(): boolean {
return this._controls.enabled;
}

getCamera(): THREE.PerspectiveCamera {
return this._camera;
}

setCameraState(state: CameraState): void {
if (state.rotation && state.target) throw new Error("Can't set both rotation and target");
const position = state.position ?? this._camera.position;
const rotation = state.rotation ?? this._camera.quaternion;
const target = state.target ?? (state.rotation ?
CameraManagerHelper.calculateNewTargetFromRotation(
this._camera, state.rotation, this._controls.target) :
this._controls.target);

this._camera.position.copy(position);
this._controls.target.copy(target);
this._camera.quaternion.copy(rotation);
}

getCameraState(): Required<CameraState> {
return {
position: this._camera.position.clone(),
target: this._controls.target.clone(),
rotation: this._camera.quaternion.clone(),
}
}

activate(cameraManager?: CameraManager): void {
this._controls.enabled = true;

if (cameraManager) {
this.setCameraState({ target: cameraManager.getCameraState().target });
}
}

deactivate(): void {
this._controls.enabled = false;
}

on(eventType: CameraManagerEventType, callback: CameraEventDelegate): void {
switch(eventType) {
case 'cameraChange':
this._cameraChangedListener.push(callback);
break;
case 'cameraStop':
this._stopEventTrigger.subscribe(callback as CameraStopDelegate);
break;
default:
throw Error(`Unrecognized camera event type: ${event}`);
}
}

off(eventType: CameraManagerEventType, callback: CameraEventDelegate): void {
switch(eventType) {
case 'cameraChange':
const index = this._cameraChangedListener.indexOf(callback);
if (index !== -1) {
this._cameraChangedListener.splice(index, 1);
}
break;
case 'cameraStop':
this._stopEventTrigger.unsubscribe(callback as CameraStopDelegate);
break;
default:
throw Error(`Unrecognized camera event type: ${event}`);
}
}

fitCameraToBoundingBox(boundingBox: THREE.Box3, duration?: number, radiusFactor?: number): void {
const { position, target } = CameraManagerHelper.calculateCameraStateToFitBoundingBox(this._camera, boundingBox, radiusFactor);

this.setCameraState({ position, target });
}

update(deltaTime: number, boundingBox: THREE.Box3): void {
this._controls.update();
CameraManagerHelper.updateCameraNearAndFar(this._camera, boundingBox);
}

dispose(): void {
this._stopEventTrigger.dispose();
this._controls.dispose();
this._cameraChangedListener.splice(0);
}
}

There are four things that you should really pay extra attention to:

  1. 'cameraChange' event must be fired every time camera position or rotation changes to trigger redraw in Reveal.

  2. For the setCameraState method you should properly handle how change in rotation and target affects the camera and corresponds with the behaviour of camera controls (OrbitControls in this case). For calculating camera target from rotation you can use calculateNewTargetFromRotation method of CameraManagerHelper .

  3. Inside update method you should update near and far planes of the camera, which can be easily done using updateCameraNearAndFar method of CameraManagerHelper.

  4. The functions activate and deactivate are called automatically when a new CameraManager is assigned to the Cognite3DViewer. These functions are intended to help manage enabling and disabling of input when the manager changes state. Return value of get enabled should reflect this state.