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 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
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 usecalculateNewTargetFromRotation
method ofCameraManagerHelper
. -
Inside
update
method you should updatenear
andfar
planes of the camera, which can be easily done usingupdateCameraNearAndFar
method ofCameraManagerHelper
. -
The functions
activate
anddeactivate
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 ofget enabled
should reflect this state.