Looking at Custom Camera Inputs
Tags: Babylonjs, JavaScript, 3d, Input Devices, Typescript

One of the more common questions I see whenever I look at the Babylon.js forums is “How can I change X behavior of my camera?” Most of the time, this question can be answered with a simple settings adjustment for the camera, like increasing the angular sensibility or setting the upper and lower beta limits. Sometimes, the answer is a bit more complicated than that.

Photo by bruce mars on Unsplash

When building out a set of features for your userbase, it’s important to cover as many scenarios as is reasonably possible and provide for the ability of the user to tweak and modify as needed. Sometimes, the standard camera behavior is almost what you need. That’s where creating a Custom Input object comes into play.

Photo by Bryan Natanael on Unsplash

A Custom Input object is effectively just a class that someone makes to consume input events and move the camera. Creating one can seem like a daunting task but can be easily be accomplished with a little planning. First, you have to figure out which camera you’re going to use. Specific cameras use specific input objects and depending on what you want to replace, you’ll need it’s class name:

// Examples of removing various camera inputs

// Removing FreeCamera Keyboard

camera.inputs.removeByType("FreeCameraKeyboardMoveInput"); // Removing ArcRotateCamera Keyboard

camera.inputs.removeByType("ArcRotateCameraKeyboardMoveInput"); // Removing FreeCamera Mouse

camera.inputs.removeByType("FreeCameraMouseInput"); // Removing ArcRotateCamera Pointers (combined Mouse/Touch)

camera.inputs.removeByType("ArcRotateCameraPointersInput");

It should also be noted that while you’re replacing part of a camera’s input, you are still creating a class from an interface. If you’re not familiar with how a camera is supposed to move, programmatically, it would behoove you first to take a look at the source code and understand the inner workings. Every single input class will inherit from ICameraInput<T> where T is whichever camera you’re using (eg. class ExampleCustomInput implements ICameraInput<FreeCamera> ). If you’re writing your custom input in Javascript, you don’t need to do anything explicit but in either case, you WILL need at least 4 of the below components.

Photo by Glenn Carstens-Peters on Unsplash

Components of a Custom Input Object

In order to define a Custom Input object, you will need to define any needed variables (keys, sensibility, etc.) and the following functions:

getClassName()

This function defines the full name of the input object. This is the string value that you’ll use if you ever want to remove an input using the removeByType function (eg. camera.inputs.removeByType("ArcRotateCameraKeyboardInput") ). For the most part, your getClassName function will look like this:

// Javascript

ExamplePointerInput.prototype.getClassName() = function () {

return "ExamplePointerInput";

}; // Typescript

public getClassName(): string {

return "ExamplePointerInput";

}

getSimpleName()

This function just sets up a simple name for your input object. When you set this function up, it should look almost identical to getClassName:

// Javascript

ExamplePointersInput.prototype.getSimpleName() = function () {

return “pointer”;

}; // Typescript

public getSimpleName(): string {

return "pointer";

};

You could just directly access your input object by using camera.inputs.attached.pointer .

attachControl(element, noPreventDefault)

This is the biggest function of all that you need to implement. This function will be called anytime that the attachControls function is called for your camera. This is going to be your workhorse function. This will setup how you work with your variables and provide the necessary behaviors for your camera.

detachControl(element)

What gets attached, must be detached. The detachControl function will be called when you call your camera’s detachControls function. When you write this function, you want to write it in such a way that you will disable your custom inputs but be able to re-enable them with attachControl .

checkInputs()

This isn’t necessarily required, depending on how you set up your attachControl function but the purpose of this is to run for each frame. This might be a useful place to put code that you want to verify parameters or enforce restrictions before actually moving the camera.

Breakdown of Example: Chessboard Demo

So a while back I created a demo that shows a chessboard with some objects that can be interacted with. Inside of this demo, I had to get creative with the keyboard inputs because the standard ArcRotateCamera’s keyboard controls didn’t do everything that I needed them to. First, let’s look at the variables that I created for my Custom Input class (Note: this code will be in Javascript)

var ArcRotateCameraKeyboardPanInput = function () {

this._keys = [];



this.keysLeft = [37];

this.keysRight = [39];

this.keysUp = [38];

this.keysDown = [40];

};

These variables are effectively your public variables. Here, I just have the bare minimum needed for 2D overhead movement. The _keys array is just one way to handle storing what keys are active and isn’t necessarily a requirement. I’ve also included arrays for all of the arrow keys (key codes). My reasoning for this is to emulate how we do things in Babylon.js and provide for the option to assign multiple key codes to perform the same functions (eg. WASD). It should also be noted that these values can be referenced outside of this objects code, if you want to provide the ability to add or change keybinds during runtime.

I also added variables using the prototype property to demonstrate another way to include needed variables:

ArcRotateCameraKeyboardPanInput.prototype.activeMove = true;

ArcRotateCameraKeyboardPanInput.prototype.activeRotate = false;

For both getClassName and getSimpleName , I kept it simple and just did what I mentioned in the components section:

ArcRotateCameraKeyboardPanInput.prototype.getClassName = function () {

return "ArcRotateCameraKeyboardPanInput";

}; ArcRotateCameraKeyboardPanInput.prototype.getSimpleName = function () {

return "KeyboardPan";

};

The attachControl function has all of our setup code. We have two functions _onKeyDown and _onKeyUp that we’re going to use for each of the type of keyboard events that we’ll encounter:

ArcRotateCameraKeyboardPanInput.prototype.attachControl = function (noPreventDefault) {

var _this = this;

var engine = this.camera.getEngine();

var 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 ||

_this.keysUp.indexOf(evt.keyCode) !== -1 ||

_this.keysDown.indexOf(evt.keyCode) !== -1) {

var 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 ||

_this.keysUp.indexOf(evt.keyCode) !== -1 ||

_this.keysDown.indexOf(evt.keyCode) !== -1) {

var 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 }

]);

}

};

As you can see from the code, we set up _onKeyDown and _onKeyUp functions and add them as listeners for keydown and keyup events. With this, most of the work for getting our keyboard input to be read is done. Now, we just need to do something with the read keys.

While it’s generally optional, checkInputs can be useful for handling things conditionally. For the demo, we need the keyboard to do double duty in that we need it to pan when zoomed out, and rotate when zoomed in:

ArcRotateCameraKeyboardPanInput.prototype.checkInputs = function () {

if (this._onKeyDown) {

if (this.activeMove) {

var speed = 2 * camera._computeLocalCameraSpeed();

let transformMatrix = BABYLON.Matrix.Zero();

let localDirection = BABYLON.Vector3.Zero();

let transformedDirection = BABYLON.Vector3.Zero(); for (var index = 0; index < this._keys.length; index++) {

var keyCode = this._keys[index];

if (this.keysLeft.indexOf(keyCode) !== -1) {

localDirection.copyFromFloats(-speed, 0, 0);

}

else if (this.keysRight.indexOf(keyCode) !== -1) {

localDirection.copyFromFloats(speed, 0, 0);

}

else if (this.keysUp.indexOf(keyCode) !== -1) {

localDirection.copyFromFloats(0, speed, 0);

}

else if (this.keysDown.indexOf(keyCode) !== -1) {

localDirection.copyFromFloats(0, -speed, 0);

}

camera.getViewMatrix().invertToRef(transformMatrix);

BABYLON.Vector3.TransformNormalToRef(localDirection, transformMatrix, transformedDirection);

camera.position.addInPlace(transformedDirection);

camera.target.addInPlace(transformedDirection);

}

}

else if (this.activeRotate) {

for (var index = 0; index < this._keys.length; index++) {

var keyCode = this._keys[index];

if (this.keysLeft.indexOf(keyCode) !== -1) {

camera.inertialAlphaOffset -= 3 / 1000;

}

else if (this.keysRight.indexOf(keyCode) !== -1) {

camera.inertialAlphaOffset -= -3 / 1000;

}

else if (this.keysUp.indexOf(keyCode) !== -1) {

camera.inertialBetaOffset -= 3 / 1000;

}

else if (this.keysDown.indexOf(keyCode) !== -1) {

camera.inertialBetaOffset -= -3 / 1000;

}

}

}

}

};

I realize that the above code may like look a bit much but it can be broken down into two parts activeMove and activeRotate .

With activeMove , we want to just pan the camera. We first take the local, untranslated vector that we wish to move and translate it using our camera’s view matrix as a reference. Simply put, we take relative direction to the camera’s viewpoint and figure out which way to move it based on its absolute position.

activeRotate is a much simpler part because it leverages the basic properties of the ArcRotateCamera. Basically, we add/subtract a small value from our camera’s inertialAlphaOffset or inertialBetaOffset . The ArcRotateCamera then will smoothly rotate the camera until its alpha and beta values are adjusted by this offset. It should be noted that the 3/1000 value that I chose here was an arbitrary one that worked well for my demo. I generally don’t recommend the use of hard-coded or magic numbers. You could use this kind of equation to make the movement more adjustable or user-controlled (eg. speed instead of 3 and angularSensibility instead of 1000) but that’s up to you.

Finally, there’s detachControl :

ArcRotateCameraKeyboardPanInput.prototype.detachControl = function () {

if (this._onKeyDown) {

var engine = this.camera.getEngine();

var element = engine.getInputElement();

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;

}

};

This just removes our event listeners, actively pressed keys, and saved functions. When writing both your attach and detach function, you’ll want to make sure that what you add as observables, callbacks, and event listeners should always be disabled and/or cleared with your detach function. Your detach function should never permanently disable something that the attach function can’t re-enable. This is something that you should always keep in mind when designing your Custom Input objects.