QuintusHegie

Missing CameraInputManager support for FollowCamera (camera.inputs)

Recommended Posts

Hi guys and gals,

I was reading the docs on Customizing Camera Inputs

https://doc.babylonjs.com/how_to/customizing_camera_inputs

and I want to customize the camera input for the FollowCamera.

But it has no camera.inputs set at the moment when I look at the definition:

https://github.com/BabylonJS/Babylon.js/blob/master/src/Cameras/babylon.followCamera.ts

While for example the FreeCamera has:

https://github.com/BabylonJS/Babylon.js/blob/master/src/Cameras/babylon.freeCamera.ts#L164

My question is:

  • Can you set the camera.inputs to a new CameraInputsManager instance?

Perhaps a DummyCameraInputsManager that's just an empty shell, doesn't need to do anything with input for now... just being there.

So then I can add my own inputs to this camera using:

camera.inputs.add(new MyFancyNewFollowCameraKeyboardInput());

I can then write camera input controllers (in JavaScript) that would adjust the FollowCamera's radius, rotationOffset and heightOffset.

If you like my camera input controllers for the FollowCamera I can share the code, so they might even become default. 🙂

Some background info:

I use a FollowCamera in my BabylonJS Model Train Simulator game.

It follows the train quite nicely, even in curves. 🙂

But when the train gets longer, I want the user to be able to adjust the radius to get the train in view again (zoom in/out).

Also when the player operates the train at a station, I want the user to be able to adjust the rotationOffset to get a clear view on loading/unloading the train at that station.

So it's kinda like an ArcRotate input but slightly different.

Let me know how I can help achieve this or if there's a similar solution that achieves more or less the same.

Thanks,

Quintus

Share this post


Link to post
Share on other sites
17 hours ago, Deltakosh said:

That's a good idea. No problem to create an input manager for the FollowCamera. Do you want to do a PR?

Sure, if a Pull Request is the way to go to add code looks fine to me. Need some help though, because this will be my first Pull Request ever. ^_^ Q

Share this post


Link to post
Share on other sites

Once the CameraInputsManager is there on the FollowCamera, this is my idea for the controls:

What to control with input device

camera.radius = desired distance to followed target
camera.rotationOffset = desired rotation offset from axis of followed target (xz-plane / y-axis) in degrees
camera.heightOffset = desired height offset from axis of followed target (xz-plane / y-axis)

The FollowCamera's Up always remains Up with the world (avoids roller coaster looping sickness 😉 ). It's a Camera that moves through the world but tries to follow a target.
A camera that fully aligns with the orientation vector of the followed object (e.g. a Plane viewed from it's tail) is a different type of camera.

How to control that

The controls should intuitively probably be such that they are relative to the view from the camera on the target.
The sensitivity and/or deadzone of the controls should be configurable and perhaps also some option to invert axis for heightOffset.
Here's my idea so far:

Keyboard

  • up (-) / down (+) to control radius
  • left (?) / right (?) to control rotationOffset
  • SHIFT + [up (+) / down (-)] to control heightOffset

Mouse

  • wheel forward (-) / wheel backward (+) scroll to control radius
  • drag left (?) / right (?) to control rotationOffset
  • drag up (?) / down (?) to control heightOffset

Touch

  • same as mouse but wheel is changed to 2 finger zoom in (2 fingers drag away from center to opposite side) / zoom out (2 fingers drag to center from opposite sides)

Gamepad

  • right stick Y up (-) / down (+)  to control radius
  • right stick X to control rotationOffset
  • left stick Y to control heightOffset

VirtualJoystick

  • same as gamepad

DeviceOrientation

  • lean forward (-) / backward (+) to control radius
  • rotate left/right around Up-axis to control rotationOffset

Pointer

  • no clue; perhaps point to a world location and then the new desired radius and rotationOffset is computed measured from current location locked target and pointed location, given the same desired heightOffset from the locked target's zx-plane?

That's about what I figured thus far would fit in the experience of the game I'm making.
Your comments are welcome so I can make the controls more generic and in conformance with the other already existing input controls.

Q

Share this post


Link to post
Share on other sites
On 9/24/2018 at 6:14 PM, Deltakosh said:

I have no use of this camera so I'm definitely not the right guy to provide guidance :)

AS far as I can tell, we should start small by adding a first keyboard input controller to get a sense of what we want to achieve?

The overall plan seems solid though

I'll bet you'll start to love this camera when you play my game when the new camera controls are ready 😉

Anyway, ok, something like this?


FollowCameraKeyboardMoveInput:

I wrote an untested example code for the Keyboard Input:

/**
 * Keyboard input to control the 'radius' (up/down), 'rotationOffset' (left/right) and 'heightOffset' parameters of FollowCamera.
 * @see http://www.html5gamedevs.com/topic/40164-missing-camerainputmanager-support-for-followcamera-camerainputs/
 */
var FollowCameraKeyboardMoveInput = function ()
{
	/**
	 * Defines the camera the input is attached to.
	 */
	this.camera = null;

	/**
	 * Gets or Set the list of keyboard keys used to control the forward move of the camera.
	 */
	this.keysUp = [38]; // Arrow Up key

	/**
	 * Gets or Set the list of keyboard keys used to control the backward move of the camera.
	 */
	this.keysDown = [40]; // Arrow Down key

	/**
	 * Gets or Set the list of keyboard keys used to control the left strafe move of the camera.
	 */
	this.keysLeft = [37]; // Arrow Left key

	/**
	 * Gets or Set the list of keyboard keys used to control the right strafe move of the camera.
	 */
	this.keysRight = [39]; // Arrow Right key

	this._keys = [];
	this._shiftKey = false; // TODO let developer choose whether to use shiftKey, ctrlKey, altKey or metaKey
	this._onCanvasBlurObserver = null;
	this._onKeyboardObserver = null;
	this._engine = null;
	this._scene = null;
};

/**
 * Attach the input controls to a specific dom element to get the input from.
 * @param element Defines the element the controls should be listened from
 * @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
 */
FollowCameraKeyboardMoveInput.prototype.attachControl = function(element, noPreventDefault)
{
	if (this._onCanvasBlurObserver)
		return;

	this._scene = this.camera.getScene();
	this._engine = this._scene.getEngine();

	this._onCanvasBlurObserver = this._engine.onCanvasBlurObservable.add(() => {
		this._keys = [];
		this._shiftKey = false;
	});

	this._onKeyboardObserver = this._scene.onKeyboardObservable.add(info => {
		let evt = info.event;

		// Store the shift key state
		this._shiftKey = (evt.shiftKey != false);

		if (info.type === KeyboardEventTypes.KEYDOWN)
		{
			// A key is pressed
			if (this.keysUp.indexOf(evt.keyCode) !== -1 ||
				this.keysDown.indexOf(evt.keyCode) !== -1 ||
				this.keysLeft.indexOf(evt.keyCode) !== -1 ||
				this.keysRight.indexOf(evt.keyCode) !== -1)
			{
				var i = this._keys.indexOf(evt.keyCode);

				// Add the key to the list of pressed keys
				if (i === -1)
					this._keys.push(evt.keyCode);

				if (!noPreventDefault)
					evt.preventDefault();
			}
		}
		else
		{
			// A key is no longer pressed
			if (this.keysUp.indexOf(evt.keyCode) !== -1 ||
				this.keysDown.indexOf(evt.keyCode) !== -1 ||
				this.keysLeft.indexOf(evt.keyCode) !== -1 ||
				this.keysRight.indexOf(evt.keyCode) !== -1)
			{
				var i = this._keys.indexOf(evt.keyCode);

				// Remove the key to the list of pressed keys
				if (i >= 0)
					this._keys.splice(i, 1);

				if (!noPreventDefault)
					evt.preventDefault();
			}
		}
	});
};

/**
 * Detach the current controls from the specified dom element.
 * @param element Defines the element to stop listening the inputs from
 */
FollowCameraKeyboardMoveInput.prototype.detachControl = function(element)
{
	if (this._scene)
	{
		if (this._onKeyboardObserver)
			this._scene.onKeyboardObservable.remove(this._onKeyboardObserver);

		if (this._onCanvasBlurObserver)
			this._engine.onCanvasBlurObservable.remove(this._onCanvasBlurObserver);

		this._onKeyboardObserver = null;
		this._onCanvasBlurObserver = null;
	}

	this._keys = [];
	this._shiftKey = false;
};

/**
 * Update the current camera state depending on the inputs that have been used this frame.
 * This is a dynamically created lambda to avoid the performance penalty of looping for inputs in the render loop.
 */
FollowCameraKeyboardMoveInput.prototype.checkInputs = function()
{
	if (this._onKeyboardObserver)
	{
		// Keyboard
		for (var i = 0; i < this._keys.length; i++)
		{
			var keyCode = this._keys[i];
			var speed = this.camera._computeLocalCameraSpeed();

			if (this.keysLeft.indexOf(keyCode) !== -1)
			{
				// Left = rotate clockwise around target
				this.camera.rotationOffset = (this.camera.rotationOffset + -speed) % 360;
			}
			else if (this.keysUp.indexOf(keyCode) !== -1)
			{
				// Up = move closer to target (or with shift pressed: heighten altitude)
				if (this._shiftKey)
					this.camera.heightOffset += speed;
				else
					this.camera.radius += -speed;
			}
			else if (this.keysRight.indexOf(keyCode) !== -1)
			{
				// Right = rotate counter-clockwise around target
				this.camera.rotationOffset = (this.camera.rotationOffset + speed) % 360;
			}
			else if (this.keysDown.indexOf(keyCode) !== -1)
			{
				// Down = move further away from target (or with shift pressed: lower altitude)
				if (this._shiftKey)
					this.camera.heightOffset += -speed;
				else
					this.camera.radius += speed;
			}

/*
			// TODO take this into account or not?
			if (this.camera.getScene().useRightHandedSystem)
				this.camera._localDirection.z *= -1;
*/
/*
			// TODO replace with updating the camera position and rotation...
			this.camera.getViewMatrix().invertToRef(this.camera._cameraTransformMatrix);
			Vector3.TransformNormalToRef(this.camera._localDirection, this.camera._cameraTransformMatrix, this.camera._transformedDirection);
			this.camera.cameraDirection.addInPlace(this.camera._transformedDirection);
*/
		}
	}
}

/**
 * Gets the class name of the current intput.
 * @returns the class name
 */
FollowCameraKeyboardMoveInput.prototype.getClassName = function()
{
	return "FollowCameraKeyboardMoveInput";
};

/** @hidden */
FollowCameraKeyboardMoveInput.prototype._onLostFocus = function (e)
{
	this._keys = [];
	this._shiftKey = false;
};

/**
 * Get the friendly name associated with the input class.
 * @returns the input friendly name
 */
FollowCameraKeyboardMoveInput.prototype.getSimpleName = function()
{
	return "keyboard";
};

FollowCameraInputsManager:

And the untested inputs manager to associate with it:

/**
 * Default Inputs manager for the FollowCamera.
 * It groups all the default supported inputs for ease of use.
 * @version 1 Mouse only
 * @see http://www.html5gamedevs.com/topic/40164-missing-camerainputmanager-support-for-followcamera-camerainputs/
 */
var FollowCameraInputsManager = function(camera)
{
	BABYLON.CameraInputsManager.call(this, camera);
};

FollowCameraInputsManager.prototype = Object.create(BABYLON.CameraInputsManager.prototype);
FollowCameraInputsManager.prototype.constructor = FollowCameraInputsManager;

/**
 * Add keyboard input support to the input manager.
 * @returns the current input manager
 */
FollowCameraInputsManager.prototype.addKeyboard = function()
{
	this.add(new FollowCameraKeyboardMoveInput());
	return this;
};

/**
 * Add mouse input support to the input manager.
 * @returns the current input manager
 */
FollowCameraInputsManager.prototype.addMouse = function(touchEnabled = true)
{
	//this.add(new FollowCameraMouseMoveInput(touchEnabled));
	return this;
}

/**
 * Add orientation input support to the input manager.
 * @returns the current input manager
 */
FollowCameraInputsManager.prototype.addDeviceOrientation = function()
{
	//this.add(new FollowCameraDeviceOrientationInput());
	return this;
}

/**
 * Add touch input support to the input manager.
 * @returns the current input manager
 */
FollowCameraInputsManager.prototype.addTouch = function()
{
	//this.add(new FollowCameraTouchInput());
	return this;
}

/**
 * Add virtual joystick input support to the input manager.
 * @returns the current input manager
 */
FollowCameraInputsManager.prototype.addVirtualJoystick = function()
{
	//this.add(new FollowCameraVirtualJoystickInput());
	return this;
}

FollowCamera

And then also some minor changes to the existing FollowCamera are needed to add support in the first place of this camera for working with an input manager... do I need to spell this syntax out? 🙂

Bonus: FollowCameraMouseMoveInput

Untested code for click+dragging and mouse wheel.

Simply uncomment in above input manager to add support.

/**
 * Mouse input to control the 'radius' (wheel), 'rotationOffset' (left/right drag) and 'heightOffset' (up/down drag) parameters of FollowCamera.
 * @see http://www.html5gamedevs.com/topic/40164-missing-camerainputmanager-support-for-followcamera-camerainputs/
 */
var FollowCameraMouseMoveInput = function (touchEnabled = true)
{
	/**
	 * Defines the camera the input is attached to.
	 */
	this.camera = null;

	/**
	 * Defines the buttons associated with the input to handle camera move.
	 */
	this.buttons = [0, 1, 2];

	/**
	 * Defines the pointer angular sensibility  along the X and Y axis or how fast is the camera rotating.
	 */
	this.angularSensibility = 2000.0;

	/**
	 * Gets or Set the mouse wheel precision or how fast is the camera zooming.
	 */
	this.wheelPrecision = 3.0;

	/**
	 * wheelDeltaPercentage will be used instead of wheelPrecision if different from 0. 
	 * It defines the percentage of current camera.radius to use as delta when wheel is used.
	 */
	this.wheelDeltaPercentage = 0;

	/**
	 * Define if touch is enabled in the mouse input
	 */
	this.touchEnabled = touchEnabled;

	this._pointerInput = null;
	this._onMouseMove = null;
	this._observer = null;
	this.previousPosition = null;
};

/**
 * Attach the input controls to a specific dom element to get the input from.
 * @param element Defines the element the controls should be listened from
 * @param noPreventDefault Defines whether event caught by the controls should call preventdefault() (https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
 */
FollowCameraMouseMoveInput.prototype.attachControl = function (element, noPreventDefault)
{
	var engine = this.camera.getEngine();

	if (!this._pointerInput)
	{
		this._pointerInput = (p, s) => {
			var evt = p.event;

			if (engine.isInVRExclusivePointerMode)
				return;

			if (!this.touchEnabled && evt.pointerType === "touch")
				return;

			// FIXME update this condition for wheel
			if (p.type !== PointerEventTypes.POINTERMOVE && this.buttons.indexOf(evt.button) === -1)
				return;

			let srcElement = (evt.srcElement || evt.target);

			if (p.type === PointerEventTypes.POINTERDOWN && srcElement)
			{
				try {
					srcElement.setPointerCapture(evt.pointerId);
				} catch (e) {
					//Nothing to do with the error. Execution will continue.
				}

				this.previousPosition =
					{
						x: evt.clientX,
						y: evt.clientY
					};

				if (!noPreventDefault)
				{
					evt.preventDefault();
					element.focus();
				}
			}
			else if (p.type === PointerEventTypes.POINTERUP && srcElement)
			{
				try {
					srcElement.releasePointerCapture(evt.pointerId);
				} catch (e) {
					//Nothing to do with the error.
				}

				this.previousPosition = null;
				if (!noPreventDefault)
					evt.preventDefault();
			}
			else if (p.type === PointerEventTypes.POINTERMOVE)
			{
				if (!this.previousPosition || engine.isPointerLock)
					return;

				var offsetX = evt.clientX - this.previousPosition.x;
				if (this.camera.getScene().useRightHandedSystem) offsetX *= -1;
				if (this.camera.parent && this.camera.parent._getWorldMatrixDeterminant() < 0) offsetX *= -1;
				this.camera.rotationOffset += offsetX / this.angularSensibility;

				var offsetY = evt.clientY - this.previousPosition.y;
				this.camera.heightOffset += offsetY / this.angularSensibility;

				this.previousPosition =
					{
						x: evt.clientX,
						y: evt.clientY
					};

				if (!noPreventDefault)
					evt.preventDefault();
			}
			else if (p.type === PointerEventTypes.POINTERWHEEL)
			{
				var delta = 0;

				if (evt.wheelDelta) {
					if (this.wheelDeltaPercentage) {
						var wheelDelta = (evt.wheelDelta * 0.01 * this.wheelDeltaPercentage) * this.camera.radius;
						if (evt.wheelDelta > 0) {
							delta = wheelDelta / (1.0 + this.wheelDeltaPercentage);
						} else {
							delta = wheelDelta * (1.0 + this.wheelDeltaPercentage);
						}
					} else {
						delta = evt.wheelDelta / (this.wheelPrecision * 40);
					}
				} else if (evt.detail) {
					delta = -evt.detail / this.wheelPrecision;
				}

				if (delta)
					this.camera.radius += delta;

				if (evt.preventDefault)
				{
					if (!noPreventDefault)
						evt.preventDefault();
				}
			}
		};
	}

	if (!this._onMouseMove)
	{
		this._onMouseMove = evt => {
			if (!engine.isPointerLock)
				return;

			if (engine.isInVRExclusivePointerMode)
				return;

			var offsetX = evt.movementX || evt.mozMovementX || evt.webkitMovementX || evt.msMovementX || 0;
			if (this.camera.getScene().useRightHandedSystem) offsetX *= -1;
			if (this.camera.parent && this.camera.parent._getWorldMatrixDeterminant() < 0) offsetX *= -1;
			this.camera.rotationOffset += offsetX / this.angularSensibility;

			var offsetY = evt.movementY || evt.mozMovementY || evt.webkitMovementY || evt.msMovementY || 0;
			this.camera.heightOffset += offsetY / this.angularSensibility;

			this.previousPosition = null;

			if (!noPreventDefault)
				evt.preventDefault();
		};
	}

	this._observer = this.camera.getScene().onPointerObservable.add(this._pointerInput, PointerEventTypes.POINTERDOWN | PointerEventTypes.POINTERUP | PointerEventTypes.POINTERMOVE | PointerEventTypes.POINTERWHEEL);
	element.addEventListener("mousemove", this._onMouseMove, false);
}

/**
 * Detach the current controls from the specified dom element.
 * @param element Defines the element to stop listening the inputs from
 */
FollowCameraMouseMoveInput.prototype.detachControl = function (element)
{
	if (this._observer && element)
	{
		this.camera.getScene().onPointerObservable.remove(this._observer);
		if (this._onMouseMove)
			element.removeEventListener("mousemove", this._onMouseMove);
		this._observer = null;
		this._onMouseMove = null;
		this.previousPosition = null;
	}
}

/**
 * Gets the class name of the current intput.
 * @returns the class name
 */
FollowCameraMouseMoveInput.prototype.getClassName = function ()
{
	return "FollowCameraMouseMoveInput";
}

/**
 * Get the friendly name associated with the input class.
 * @returns the input friendly name
 */
FollowCameraMouseMoveInput.prototype.getSimpleName = function ()
{
	return "mouse";
}

 

Q

Edited by QuintusHegie
added mouse input template as well

Share this post


Link to post
Share on other sites
On 9/25/2018 at 10:00 PM, Deltakosh said:

This will need to be in TS in order to be merged

(read this as a good kickstarter: http://doc.babylonjs.com/how_to/how_to_start)

Ok. Thanks for the kickstarter guide. This will take me some time for me to learn and get started with, but seems doable. Q

Share this post


Link to post
Share on other sites

Here's a start that adds keyboard bindings to `FollowCamera`:

https://github.com/mrdunk/Babylon.js/commit/e0cecc50caf425bee0b5d4ffbb68bc74ed480214

Is this something that's worth pushing?

I've modeled it after `arcRotateCameraInputsManager`.
I'm new to Babylon so feel free to make suggestions.

 

To test, put the following in `localDev/src/index.js` or Playground:
 

var createScene = function () {
  // This creates a basic Babylon Scene object (non-mesh)
  var scene = new BABYLON.Scene(engine);

  // Our built-in 'sphere' shape. Params: name, subdivs, size, scene
  var sphere = BABYLON.Mesh.CreateSphere("sphere1", 16, 2, scene);
  var frameRate = 10;
  var xSlide = new BABYLON.Animation("xSlide", "position.x", frameRate, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
  var keyFrames = [ {frame: 0, value: 2}, {frame: frameRate, value: -2}, {frame: 2 * frameRate, value: 2} ];
  xSlide.setKeys(keyFrames);

  scene.beginDirectAnimation(sphere, [xSlide], 0, 2 * frameRate, true);

  // This creates and positions a free camera (non-mesh)
  var camera = new BABYLON.FollowCamera("camera1", new BABYLON.Vector3(0, 0, 0), scene);
  camera.lockedTarget = sphere;

  // This targets the camera to scene origin
  camera.setTarget(BABYLON.Vector3.Zero());

  // This attaches the camera to the canvas
  camera.attachControl(canvas, true);

  // This creates a light, aiming 0,1,0 - to the sky (non-mesh)
  var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
  // Default intensity is 1. Let's dim the light a small amount
  light.intensity = 0.7;

  // Move the sphere upward 1/2 its height
  sphere.position.y = 1;

  // Our built-in 'ground' shape. Params: name, width, depth, subdivs, scene
  var ground = BABYLON.Mesh.CreateGround("ground1", 6, 6, 2, scene);

  return scene;
};

dunk.

Edited by dunk
added test example.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.