Customizing Camera Inputs

How to Customize Camera Inputs

Every Babylon.js camera will automatically handle inputs for you, once you call the camera's attachControl method. You can revoke the control by using the detachControl method. Most Babylon.js experts use a two-step process to activate and attach a camera:

//First, set the scene's activeCamera... to be YOUR camera.
scene.activeCamera = myCamera;
// Then attach the activeCamera to the canvas.
//Parameters: canvas, noPreventDefault
scene.activeCamera.attachControl(canvas, true);

A simpler version might look like this:

myCamera.attachControl(canvas);

By default noPreventDefault is set to false, meaning that preventDefault() is automatically called on all canvas mouse clicks and touch events.

Babylon.js v2.4 introduced a different way to manage camera inputs to provide an approach oriented toward composability of inputs. You can now use an input manager and each input can be seen as a plugin that is specific to this camera family and to a given input type (mouse, keyboard, gamepad, device orientation, etc.).

Using input managers, you can add, remove, enable, or disable any input available for the camera. You can also easily implement custom input mechanisms or override the existing ones.

The input manager is available through the camera's inputs property. For example:

const camera = new BABYLON.FreeCamera("sceneCamera", new BABYLON.Vector3(0, 1, -15), scene);
const inputManager = camera.inputs;

Configure your inputs

Most inputs provide settings to customize the sensibility and adapt it to your own scene.

Each input provides a short name available on the manager. The goal is to provide a friendly syntax when playing with your inputs.

const camera = new BABYLON.FreeCamera("sceneCamera", new BABYLON.Vector3(0, 1, -15), scene);
camera.inputs.add(new BABYLON.FreeCameraGamepadInput());
camera.inputs.attached.gamepad.gamepadAngularSensibility = 250;

Adding an existing input

The input managers of ArcRotateCamera and FreeCamera expose short-hand functions for adding built-in inputs.

const camera = new BABYLON.FreeCamera("sceneCamera", new BABYLON.Vector3(0, 1, -15), scene);
camera.inputs.addGamepad();

If you wish, you can also add an instance of your own input (we will cover how to implement your own input at the end of this article).

const camera = new BABYLON.FreeCamera("sceneCamera", new BABYLON.Vector3(0, 1, -15), scene);
camera.inputs.add(new BABYLON.FreeCameraGamepadInput());

Enable or disable inputs

When you call attachControl on the camera, you are activating all inputs attached to the input manager. In the same way, you could turn off all inputs by calling detachControl on the camera.

If you want to disable an input temporarily, you can call detachControl directly on the input... like this:

const camera = new BABYLON.FreeCamera("sceneCamera", new BABYLON.Vector3(0, 1, -15), scene);
camera.inputs.attached.mouse.detachControl();
camera.inputs.addGamepad();

You can then call attachInput when you want to turn it on again.

camera.inputs.attachInput(camera.inputs.attached.mouse);

Removing inputs

Sometimes you want a very specific input mechanism. The best approach in such cases is probably to clear all inputs and add only those you may want in your scene.

const camera = new BABYLON.FreeCamera("sceneCamera", new BABYLON.Vector3(0, 1, -15), scene);
camera.inputs.clear();
camera.inputs.addMouse();

You can also remove a single input from your input manager. You can remove them by instance, or by Type name

const camera = new BABYLON.FreeCamera("sceneCamera", new BABYLON.Vector3(0, 1, -15), scene);
// remove by instance
camera.inputs.remove(camera.inputs.attached.mouse);
// remove by type
camera.inputs.removeByType("FreeCameraKeyboardMoveInput");

Implementing Your Own Input

Your input method is created as a function object. You must then write code for several methods, with required names, that are called by the input function object. The method names and purpose are:

// This function must return the class name of the camera, it could be used for serializing your scene
getClassName();
// This function must return the simple name that will be injected in the input manager as short hand
// for example "mouse" will turn into camera.inputs.attached.mouse
getSimpleName();
//T his function must activate your input event. Even if your input does not need a DOM element
// element and noPreventDefault must be present and used as parameter names.
// Return void.
attachControl(noPreventDefault);
// Detach control must deactivate your input and release all pointers, closures or event listeners
// element must be present as a parameter name.
// Return void.
detachControl();
// This optional function will get called for each rendered frame, if you want to synchronize your
// input to rendering, no need to use requestAnimationFrame. It's a good place for applying
// calculations if you have to.
// Return void.
checkInputs();

HammerJS Input

If the builtin touch controls for the ArcRotateCamera are not enough for you, you can implement your own camera input using the well-known touch gestures library HammerJS. https://hammerjs.github.io/

We have an example of how to use HammerJS to simulate something similar to Google Earth controls. We use BabylonJS's ArcRotateCamera for this purpose. The camera is locked on the Y axis and horizontal pan moves the camera along the X axis and the vertical pan moves it along the Z axis. The alpha angle of the camera is taken into account when panning. By pinching you can change the radius of the camera so you can zoom in and out. You can change the alpha angle of the camera by a two finger rotate gesture so basically you can rotate around the cameras target.

Example app: https://demos.babylonjs.xyz/hammerjs-example/#/

GitHub repo: https://github.com/RolandCsibrei/babylonjs-hammerjs-arc-rotate-camera

The example contains utils\ArcRotateCameraHammerJsInput.ts which implements ICameraInput<ArcRotateCamera>. There are multiple parameters for the input and are pretty self explanatory. Please refer to the source code of the input and set the sensitivity and treshold values which fits your needs. First you need to install HammerJS, please refer to https://hammerjs.github.io/getting-started/ and import it

import "hammerjs";

To use the new input you add it to your camera.inputs after you have create the camera. To avoid one input fighting the other remove the ArcRotateCameraPointersInput from camera.inputs. After you've created your Input you can set it's parameters. The default ones (please refer to https://github.com/RolandCsibrei/babylonjs-hammerjs-arc-rotate-camera/blob/680cf12155924a818faac5ff9d7f0a0271bb632b/src/utils/ArcRotateCameraHammerJsInput.ts#L21) are good for a general touch screen monitor so you may have to set them according to your needs.

// remove mouse input
camera.inputs.removeByType("ArcRotateCameraPointersInput");
// add hammer js input
const hammerJsInput = new ArcRotateCameraHammerJsInput();
// now you can set the parameters you like
// let's double the zoomSensitivity (default is 1)
hammerJsInput.zoomSensitivity = 2;
// add the input to the camera
camera.inputs.add(hammerJsInput);

Feel free to use this Input class as a starter for your own HammerJS based input.

With Javascript

This changes the normal key mappings for moving the camera left, right, forward, and back, and rotating at its current position.

First, remove the default keyboard input.

camera.inputs.removeByType("FreeCameraKeyboardMoveInput");

Now create the new input method FreeCameraKeyboardRotateInput:

const FreeCameraKeyboardRotateInput = function () {
this._keys = [];
this.keysLeft = [37];
this.keysRight = [39];
this.sensibility = 0.01;
};

Add get name methods:

FreeCameraKeyboardRotateInput.prototype.getClassName = function () {
return "FreeCameraKeyboardRotateInput";
};
FreeCameraKeyboardRotateInput.prototype.getSimpleName = function () {
return "keyboardRotate";
};

and attach and detach methods:

FreeCameraKeyboardRotateInput.prototype.attachControl = function (noPreventDefault) {
const _this = this;
const engine = this.camera.getEngine();
const element = engine.getInputElement();
if (!this._onKeyDown) {
element.tabIndex = 1;
this._onKeyDown = function (evt) {
if (_this.keysLeft.indexOf(evt.keyCode) !== -1 || _this.keysRight.indexOf(evt.keyCode) !== -1) {
const index = _this._keys.indexOf(evt.keyCode);
if (index === -1) {
_this._keys.push(evt.keyCode);
}
if (!noPreventDefault) {
evt.preventDefault();
}
}
};
this._onKeyUp = function (evt) {
if (_this.keysLeft.indexOf(evt.keyCode) !== -1 || _this.keysRight.indexOf(evt.keyCode) !== -1) {
const index = _this._keys.indexOf(evt.keyCode);
if (index >= 0) {
_this._keys.splice(index, 1);
}
if (!noPreventDefault) {
evt.preventDefault();
}
}
};
element.addEventListener("keydown", this._onKeyDown, false);
element.addEventListener("keyup", this._onKeyUp, false);
BABYLON.Tools.RegisterTopRootEvents(canvas, [{ name: "blur", handler: this._onLostFocus }]);
}
};
FreeCameraKeyboardRotateInput.prototype.detachControl = function () {
const engine = this.camera.getEngine();
const element = engine.getInputElement();
if (this._onKeyDown) {
element.removeEventListener("keydown", this._onKeyDown);
element.removeEventListener("keyup", this._onKeyUp);
BABYLON.Tools.UnregisterTopRootEvents(canvas, [{ name: "blur", handler: this._onLostFocus }]);
this._keys = [];
this._onKeyDown = null;
this._onKeyUp = null;
}
};

Optionally, add checking inputs:

FreeCameraKeyboardRotateInput.prototype.checkInputs = function () {
if (this._onKeyDown) {
const camera = this.camera;
// Keyboard
for (let index = 0; index < this._keys.length; index++) {
const keyCode = this._keys[index];
if (this.keysLeft.indexOf(keyCode) !== -1) {
camera.cameraRotation.y += this.sensibility;
} else if (this.keysRight.indexOf(keyCode) !== -1) {
camera.cameraRotation.y -= this.sensibility;
}
}
}
};

Finally, add this new input method to the camera inputs:

camera.inputs.add(new FreeCameraKeyboardRotateInput());
Rotate Free Camera Example

With Typescript

Using TypeScript, you could implement the interface ICameraInput:

interface ICameraInput<TCamera extends BABYLON.Camera> {
// the input manager will fill the parent camera
camera: TCamera;
//this function must return the class name of the camera, it could be used for serializing your scene
getClassName(): string;
//this function must return the simple name that will be injected in the input manager as short hand
//for example "mouse" will turn into camera.inputs.attached.mouse
getSimpleName(): string;
//this function must activate your input, event if your input does not need a DOM element
attachControl: (noPreventDefault?: boolean) => void;
//detach control must deactivate your input and release all pointers, closures or event listeners
detachControl: () => void;
//this optional function will get called for each rendered frame, if you want to synchronize your input to rendering,
//no need to use requestAnimationFrame. It's a good place for applying calculations if you have to
checkInputs?: () => void;
}

How to Make a Walk and Look Around Camera

The following example customizes the keyboard and mouse inputs to a universal camera. With this change, using the arrow keys you can walk forwards and backward in the scene and rotate to look left and right. Using the mouse you can look around and above and below.

In the example there are two viewports, the upper one gives a first-person view as you move and look around. The lower one gives a representation of the camera and the collision volume surrounding it.

Remember to click on the scene before using the arrow keys.

Walk and Look Camera Example

Using BaseCameraPointersInput to Create Custom Inputs

In addition to making custom camera inputs as illustrated in the Implementing Your Own Input section, you can also extend the functionality of some of the implemented base classes make creating some custom classes easier. One such class is the BaseCameraPointersInput class:

For either Javascript (ES6+) or Typescript, you should be able to extend the functionality of the BaseCameraPointersInput class. From there, you just need to override a few functions.

// You need to extend the BaseCameraPointersInput to get the required functionality
class YourCustomInputClass extends BABYLON.BaseCameraPointersInput {
// This is the constructor. Unless you have something specific that you need
// to do when you create your object, you don't need to implement this. You
// must call the super() function though, if you do.
// constructor() { super(); }
// This is exactly the same the function in the previous section and will still need to be
// implemented.
getClassName() {};
// This function is the exact same thing as the previous section. However, it has already
// been implemented with a value of "pointers" and is technically optional.
// getSimpleName() {};
// This function is already implemented. If you are planning to use this class, it is
// recommened to not override it.
// attachControl(noPreventDefault) {};
// Same thing with detachControl
// detachControl() {};
// This optional function will get called for each rendered frame, if you want to synchronize your
// input to rendering, no need to use requestAnimationFrame. It's a good place for applying
// calculations if you have to.
// Return void.
checkInputs() {};
// This function will fire during a POINTERMOVE event where there is either an active mouse
// button down or only one active touch. "point" will contain the coordinates, pointerId,
// and pointer type. The offsets are just the changes in position from the previous point.
// This will NOT fire if multiple touches are active. This method is required.
onTouch(point, offsetX, offsetY) {};
// This function will only fire during a POINTERMOVE event where more than one touch is active.
// This function will only support the first two active touches and all others will be ignored.
// Points A and B are said touches. Both previous and current pinch distances and positions are
// available to support basic gesture logic, as needed. As a warning, the previous movement may
// be null at the beginning of a multi-touch movement.
onMultiTouch(
pointA,
pointB,
previousPinchSquaredDistance,
pinchSquaredDistance,
previousMultiTouchPanPosition,
multiTouchPanPosition
) {};
// This function will only fire during a POINTERDOUBLETAP event. The "type" parameter
// is just the pointer type (mouse, touch, etc.). This is optional.
onDoubleTap(type) {};
// This function will fire when a contextmenu event occurs (right-click menu).
// "evt" is the triggering event. This is optional.
onContextMenu(evt) {};
// This function will fire when a POINTERDOWN event occurs.
// "evt" is the triggering event. This is optional.
onButtonDown(evt) {};
// This function will fire when a POINTERUP event occurs (right-click menu).
// "evt" is the triggering event. This is optional.
onButtonUp(evt) {};
// This function will fire when the window loses focus (eg. blur event)
// This is optional.
onLostFocus() {};
}

This may seem like a lot but the big takeaways are that onTouch is where you handle single pointer source events and onMultiTouch for events with at least two touch sources.

If you find yourself asking, "What benefit is there to using this versus creating my own from scratch", here are a few benefits. The BaseCameraPointersInput class will automatically handle various input and event based things like preventDefault, pointer capture, and pointer lock. On top of that, event handling is taken care of for you. While there's less flexibility in going this route, it might be easier to work with.

FreeCameraPointersInput (JS) Example FreeCameraPointersInput (TS) Example

Further reading