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.PerspectiveCameraobject toCognite3DViewer - 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:
-
'cameraChange'event must be fired every time camera position or rotation changes to trigger redraw in Reveal. -
For the
setCameraStatemethod you should properly handle how change in rotation and target affects the camera and corresponds with the behaviour of camera controls (OrbitControlsin this case). For calculating camera target from rotation you can usecalculateNewTargetFromRotationmethod ofCameraManagerHelper. -
Inside
updatemethod you should updatenearandfarplanes of the camera, which can be easily done usingupdateCameraNearAndFarmethod ofCameraManagerHelper. -
The functions
activateanddeactivateare 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 ofget enabledshould reflect this state.