Three.js DeviceOrientationControls Gyroscope Mapping
This article explains how the DeviceOrientationControls
class in Three.js maps a mobile device’s gyroscope and accelerometer
data to rotate the virtual camera. It covers the retrieval of physical
orientation angles, the translation of these angles into 3D quaternions,
and the coordinate system alignments required to synchronize physical
phone movement with virtual camera rotation.
Understanding the Input: Device Orientation API
The browser’s DeviceOrientationEvent provides real-time
rotation data from the device’s gyroscope and accelerometer. This data
is delivered via three Euler angles measured in degrees:
- Alpha (\(\alpha\)): Rotation around the Z-axis (0 to 360 degrees). This represents the compass direction (yaw).
- Beta (\(\beta\)): Rotation around the X-axis (-180 to 180 degrees). This represents front-to-back tilt (pitch).
- Gamma (\(\gamma\)): Rotation around the Y-axis (-90 to 90 degrees). This represents left-to-right tilt (roll).
The Coordinate System Mismatch
The primary challenge in mapping this data is that the device’s coordinate system does not natively match the Three.js 3D world coordinate system.
In the device coordinate system: * The Z-axis points straight up out of the screen. * The X-axis points to the right of the screen. * The Y-axis points to the top of the screen.
In Three.js: * The Y-axis points straight up. * The X-axis points to the right. * The Z-axis points out of the screen toward the viewer.
To map these systems accurately,
DeviceOrientationControls must perform a series of
mathematical rotations.
Step-by-Step Mathematical Mapping
Three.js uses quaternions to represent rotations and avoid gimbal lock. The mapping from raw device angles to the final camera rotation occurs in four distinct steps:
1. Converting Angles to Radians and Euler Ordering
First, the alpha, beta, and gamma degrees are converted into radians.
DeviceOrientationControls instantiates a Three.js
Euler object using these angles.
To prevent gimbal lock during standard phone movement, Three.js applies these rotations in a specific order: ‘YXZ’.
const alpha = deviceOrientation.alpha ? THREE.MathUtils.degToRad(deviceOrientation.alpha) : 0;
const beta = deviceOrientation.beta ? THREE.MathUtils.degToRad(deviceOrientation.beta) : 0;
const gamma = deviceOrientation.gamma ? THREE.MathUtils.degToRad(deviceOrientation.gamma) : 0;
// Set Euler angles with YXZ order
const euler = new THREE.Euler(beta, alpha, -gamma, 'YXZ');2. Converting Euler Angles to a Quaternion
Once the Euler angles are defined, they are converted into a
temporary quaternion (deviceQuaternion). This represents
the raw orientation of the device relative to the earth’s coordinate
frame.
const deviceQuaternion = new THREE.Quaternion().setFromEuler(euler);3. Adjusting for Screen Orientation
Mobile devices can be held in portrait or landscape mode, which
rotates the screen’s coordinate axes relative to the physical device. To
correct for this, DeviceOrientationControls detects the
screen orientation angle (window.orientation) and applies a
correction rotation around the camera’s Z-axis.
const screenResolutionAngle = window.orientation ? THREE.MathUtils.degToRad(window.orientation) : 0;
const screenTransform = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), -screenResolutionAngle);4. Aligning with the Three.js World Frame
Finally, the device’s orientation must be aligned with the virtual world space of Three.js. By default, the device orientation API’s “forward” vector points north, whereas a Three.js camera’s default “forward” vector points down the negative Z-axis.
To reconcile this, a constant sensor-to-world offset quaternion is applied. This is a -90 degree (\(- \pi / 2\) radians) rotation around the physical X-axis:
const worldAlign = new THREE.Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5));Calculating the Final Camera Rotation
The camera’s final rotation is calculated by multiplying these quaternions together in the correct sequence. Because quaternion multiplication is non-commutative (order matters), they are combined as follows:
// final Rotation = worldAlign * deviceQuaternion * screenTransform
camera.quaternion.copy(worldAlign)
.multiply(deviceQuaternion)
.multiply(screenTransform);This sequence rotates the virtual camera so that when the user rotates their physical phone to the left, the virtual camera rotates to the left; when they tilt it up, the camera tilts up, maintaining a 1:1 spatial relationship with the physical world.