Controlling the effects of rotations about multiple axes

Ladies, Gents,

it was a casual remark by @donmccurdy which made me aware of the fact, that rotations about multiple axes pose problems for some. And it reminded me of how I solved that problem for myself when developing the SpaceMouse driver for Three.js. So I thought, I’d share my approach with you.

Humans typically lack the sensual experience of a tilted horizon. So a titlted horizon is typically not intuitively comprehended, which may contribute to any confusion that may arise in that context.

Against that backdrop, I figured that an initial rotation about either the Three.js X- or Y-axis wouldn’t introduce any roll-rotation. Because roll is, what makes the horizon tilt. So Rotation about the Three.js Z-axis initially is what you’ll want to avoid. Or: a compounded rotation about X- and Y-axes.

Unfortunately, once you’ve introduced a rotation about either one of the X- or Y-axes, naïvely rotating about the other one is bound to introduce you to the realm of tilted horizons - which you want to avoid.

In my application I started with a rotation about the X-axis. That was a random choice. Applied to a camera, this means looking up or down. Fair enough.

camera.rotation.set( pitch, 0, 0 );

Imagine yourself having dropped a coin, a key or some other small object. And you expect to retrieve it from within a circle with its center at your feet. So you’ll be looking down (rotation about local (head) x-axis) at maybe - π/4. If you want to scan the whole circular area around you, turning your bowed down(!) head is bound to give you a tilted horizon, which is not what you want and which isn’t going to reveal the area behind yourself. What you’ll naturally do instead is, turn around on your heels (i.e.: the world(!) y-axis).

Fortunately, Three.js has a function for that:

camera.rotateOnWorldAxis( new THREE.Vector3( 0, 1, 0 ), yaw );

What you have now is a complete decoupling of the rotations about the local (camera/head) rotation about its x-axis from the rotation about the world y-axis.

This corresponds to the azimuth, altitude parameters in spherical coordinates, which will allow you to look at any direction in 3D space, and controll each rotation individually, without altering the rotation of the other axis.

What about the 3rd axis of rotation? This is the local roll axis, and you can control that via the following:

camera.rotateOnWorldAxis( cameraLookAt, roll );

Now where does the cameraLookAt Vector3 come from?

This can easily be computed with perfect precision through the following:

// identify the camera orientation "lookAt" unit vector
cosP = Math.cos( pitch );
sinP = Math.sin( pitch );
cosY = Math.cos( yaw );
sinY = Math.sin( yaw );

// this is the camera "lookAt" direction in the Three.js coordinate system:
// positive X: to the right
// positive Y: upwards
// positive Z: out of screen
cameraLookAt.x = - sinY * cosP;
cameraLookAt.y = sinP;
cameraLookAt.z = - cosY * cosP;

With this approach you completely and perfectly separate the effects of a rotation of either of the three axes from the effects of any of the other two axes.