My Notebook

Simple orbital camera controls for THREE.js

Author
Date
Category
Web Development/JavaScript

This post shows how to implement orbital camera controls with only a few lines of code and without a plugin. This enables the user to move the camera with the mouse along the surface of a sphere, whereby the camera always points to the center. At the center of the sphere there is an object that can be intuitively viewed from every angle. By increasing or decreasing the radius, the camera can zoom in or out.

There already exists an arguably much better and more complete plugin for this, but I wanted to understand how it works and make my own minimalistic version of it.

Demo

The following Earth demo uses textures from this great website. The textures are free to use, but there are some copyright limitations. Basically you are not allowed to sell or redistribute them. The higher resolution textures will also cost a small amount of money.

Browser Mouse Events

The following boring piece of code simply listens for browser mouse events and translates them into a little bit higher level events, namely drag, zoomIn and zoomOut.

var Controls = (function(Controls) {
    "use strict";

	// Check for double inclusion
	if (Controls.addMouseHandler)
		return Controls;

	Controls.addMouseHandler = function (domObject, drag, zoomIn, zoomOut) {
		var startDragX = null,
		    startDragY = null;

		function mouseWheelHandler(e) {
			e = window.event || e;
			var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));

			if (delta < 0 && zoomOut) {
				zoomOut(delta);
			} else if (zoomIn) {
				zoomIn(delta);
			}

			e.preventDefault();
		}

		function mouseDownHandler(e) {
			startDragX = e.clientX;
			startDragY = e.clientY;

			e.preventDefault();
		}

		function mouseMoveHandler(e) {
			if (startDragX === null || startDragY === null)
				return;

			if (drag)
				drag(e.clientX - startDragX, e.clientY - startDragY);

			startDragX = e.clientX;
			startDragY = e.clientY;

			e.preventDefault();
		}

		function mouseUpHandler(e) {
			mouseMoveHandler.call(this, e);
			startDragX = null;
			startDragY = null;

			e.preventDefault();
		}

		domObject.addEventListener("mousewheel", mouseWheelHandler);
		domObject.addEventListener("DOMMouseScroll", mouseWheelHandler);
		domObject.addEventListener("mousedown", mouseDownHandler);
		domObject.addEventListener("mousemove", mouseMoveHandler);
		domObject.addEventListener("mouseup", mouseUpHandler);
	};
	return Controls;
}(Controls || {}));

Initialize the Renderer

The next piece of code simply initializes the most basic components of a THREE.js WebGLRenderer. It is very important, to set the camera.up vector, because the method camera.lookAt() uses it. The variable center contains a vector to the center of the sphere the camera moves on. In this case it is equal to the origin of the coordinate system, but it can be any point.

var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(500, 500);
document.body.appendChild(renderer.domElement);

var scene = new THREE.Scene();
var center = new THREE.Vector3();
var camera = new THREE.PerspectiveCamera(45, 1, 0.01, 300);

camera.up = new THREE.Vector3(0, 0, 1);
camera.position.x = 5;
camera.lookAt(center);

Implement Dragging

Spherical coordinate system

This is the crucial part. By dragging the mouse on the canvas element, the camera moves on the surface of a sphere centered around the point center. It works by simply transforming the Cartesian coordinates (x, y, z) of the camera into Spherical coordinates. The reason for this is, that certain operations are easier to do in Spherical than in Cartesian coordinates and vice versa.

Spherical coordinates can describe any point in three-dimensional space by using the radius \(r\) and the two angles \(\theta\) theta and \(\phi\) phi. By keeping the radius constant and only changing \(\theta\) and \(\phi\) the point moves along the surface of a sphere, which is exactly what we need.

function drag(deltaX, deltaY) {
    var radPerPixel = (Math.PI / 450),
	    deltaPhi = radPerPixel * deltaX,
	    deltaTheta = radPerPixel * deltaY,
	    pos = camera.position.sub(center),
	    radius = pos.length(),
	    theta = Math.acos(pos.z / radius),
	    phi = Math.atan2(pos.y, pos.x);

	// Subtract deltaTheta and deltaPhi
	theta = Math.min(Math.max(theta - deltaTheta, 0), Math.PI);
	phi -= deltaPhi;

	// Turn back into Cartesian coordinates
	pos.x = radius * Math.sin(theta) * Math.cos(phi);
	pos.y = radius * Math.sin(theta) * Math.sin(phi);
	pos.z = radius * Math.cos(theta);

	camera.position.add(center);
	camera.lookAt(center);
	redraw();
}

A description of the most important variables used in this function:

deltaX
The number of pixels the mouse was dragged in the X direction since the last event.
deltaY
The number of pixels the mouse was dragged in the Y direction since the last event.
radPerPixel
As the name suggests it determines the speed of the rotation. It relates the pixels on the canvas element to the angle the camera should move.
pos
This is the vector from the center to the camera position. If center is at the origin then this is equal to the camera position.
theta
The angle \(\theta\). Wikipedia contains a section about the conversion of Cartesian to Spherical coordinates. The following formula is taken directly from there: $$\theta = arccos(\frac{z}{r})$$
phi
The angle \(\phi\). The following formula is also taken directly from Wikipedia: $$\phi = arctan(\frac{y}{z})$$

The appropriate formulas for turning Spherical back into Cartesian coordinates can also be found on Wikipedia:

$$x = r\:sin\theta\:cos\phi$$ $$y = r\:sin\theta\:sin\phi$$ $$z = r\:cos\theta$$

It is almost too easy. That's because the function call camera.lookAt(center); near the end does most of the real work for us. As the name suggests it changes the viewing angle of the camera, so that it looks at the point center.

Implement Zooming

The implementation of zooming is pretty trivial.

function zoomIn() {
    camera.position.sub(center).multiplyScalar(0.9).add(center);
	redraw();
}

function zoomOut() {
	camera.position.sub(center).multiplyScalar(1.1).add(center);
	redraw();
}

Everything put together

References