Course outline
Rotations: Euler Angles and Quaternions
In one line
three.js turns objects with three angles (Euler angles, in radians) or with a quaternion. Euler angles are easy to read but depend on their order and can lock up; quaternions turn smoothly and never lock.
Think of it like this
A camera on a tripod has three knobs: turn left or right (pan), tilt up or down, and roll sideways. You can aim it anywhere with them. But if you tilt straight up, panning and rolling now do the same thing: a knob has been lost. That is gimbal lock. The fix, used by aircraft and space probes, is to stop thinking in three knobs and say "turn by this much, around this one axis". That is a quaternion.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- radian
- A unit of angle used by three.js and JavaScript's maths: a half turn is π (3.14159...) radians, a full turn is 2π.
- Euler angles
- Three angles (x, y, z) that say how much to turn around each axis, applied in an order.
object.rotation. - Euler order
- Which axis is turned first:
"XYZ"(the default),"YXZ"and so on. The same three numbers in a different order give a different turn. - gimbal lock
- A state where two of the three Euler angles turn the object around the same line, so one way of turning is lost.
Step by step
01Radians, not degrees
The first example starts with a mistake on purpose: rotation.y = 90 is 90 radians, which is 5156.6° and the same pose as 116.6°. Then degToRad(90) gives 1.5708, which is Math.PI / 2. Habit: write angles as THREE.MathUtils.degToRad(deg) when you think in degrees.
02Order changes the answer
The same angles (90°, 90°, 0), in order XYZ and in order YXZ, send the point (1, 0, 0) to different places: (0, 1, 0) and (0, 0, −1). The default order of a new Euler is XYZ.
03A full turn, and PI twice
A rotation of 2π looks like no rotation: the matrix's x axis is 1.000, but the number that should be exactly 0 is −2.45e-16, a rounding leftover. Adding Math.PI twice gives 6.283185, exactly the same as 2π. three.js does not wrap angles: your number keeps growing, which is fine for a spinning object but matters if you compare or print angles.
Reading an angle back from a matrix wraps it: a 270° turn comes back as −90°. Do not expect to get out the number you put in.
04Slerp and lookAt
slerp from "no turn" to a half turn around y gives quaternions that turn 0°, 45°, 90° and 180° at t = 0, 0.25, 0.5 and 1: an even pace. lookAt is one line that replaces working out angles: the pointer turns until its +z points at the blue ball, and the third example swings it between its start pose and that pose with slerp.
Try it yourself
- 1
Change the order
In the first example, change
"XYZ"to"YXZ"in the first Euler (a). Both lines now sayorder YXZ sends (1, 0, 0) to 0, 0, -1: the same angles in the same order give the same turn. - 2
Step out of the lock
In the second example, replace
Math.PI / 2inlock()withTHREE.MathUtils.degToRad(80). The two quaternions are now different (0.198, 0.621, 0.166, 0.74and0.166, 0.621, 0.198, 0.74) and the line sayssame turn from two different Euler triples: false. Ten degrees away from the lock, x and z turn the object differently again. (The label text still says y = 90: it is fixed text.)
Code & diagrams
This example is numbers only. The first line is a mistake on purpose: 90 is read as 90 radians.
Console
Run the scene to see its console.Expected console output
rotation.y = 90 (a mistake: that is 90 radians) is really 5156.6 degrees, which is the same turn as 116.6 degrees
degToRad(90) = 1.5708 (that is Math.PI / 2 = 1.5708)
order XYZ sends (1, 0, 0) to 0, 1, 0
order YXZ sends (1, 0, 0) to 0, 0, -1
the default order is XYZ
rotation.y = 2 PI: x axis of the matrix = 1.000, leftover from rounding = -2.45e-16
PI + PI: rotation.y = 6.283185, still stored as 2 PI exactly
rotation of 270 degrees read back from its matrix: y = -90.0 degreesTry it: interactive
Rotation lab
Turn y to 90° and look at the rings: x and z now turn the frame the same way. Then press a "Slerp to" button and watch the quaternion take the short, steady way.
No picture: the lock and the smooth turn are shown with numbers.
Console
Run the scene to see its console.Expected console output
y = 90, x = 30, z = 0 : quaternion 0.183, 0.683, 0.183, 0.683
y = 90, x = 0, z = 30 : quaternion 0.183, 0.683, 0.183, 0.683
same turn from two different Euler triples: true
typed (30, 90, 0), read back (30.0, 90.0, 0.0)
slerp t=0: quaternion 0, 0, 0, 1, turned 0 degrees
slerp t=0.25: quaternion 0, 0.383, 0, 0.924, turned 45 degrees
slerp t=0.5: quaternion 0, 0.707, 0, 0.707, turned 90 degrees
slerp t=1: quaternion 0, 1, 0, 0, turned 180 degreesThe orange cone swings between its start pose and the pose that faces the blue ball. The camera's direction is printed to show that cameras look down -z.
Console
Run the scene to see its console.Expected console output
pointer's forward after lookAt = 0.832, 0.000, -0.555
direction to target = 0.832, 0.000, -0.555
camera at z = 5, lookAt the origin: getWorldDirection = 0, 0, -1.000Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Degrees in a radians field
In the first example, change cube.rotation.y = THREE.MathUtils.degToRad(90); to cube.rotation.y = 90;.
Myth vs fact
Myth
Quaternions are too hard for beginners.
Fact
You never compute with their four numbers. You make one with setFromAxisAngle, set it on object.quaternion, and blend with slerp. The numbers are for the GPU.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
three.js keeps
rotationandquaternionin sync with change callbacks (_onChange). Settingrotation.yupdates the quaternion straight away, and setting the quaternion updates the Euler angles by decomposing it, which is why you can read back an angle different from the one you wrote. - ▸
slerptakes the shorter arc between two quaternions, because q and −q are the same rotation: if their dot product is negative, it flips one before blending. This is why a slerp never spins the long way round, unlike lerping Euler angles from 350° to 10°, which sweeps 340° the wrong way.
Remember this
- 1
The problem this solves: the bike's tubes, the camera and later the wheels all need to be turned, and turned smoothly. A beginner meets three traps: angles in the wrong unit, angles applied in the wrong order, and animations that spin the long way round. This topic names each one.
- 2
Euler angles, in radians. An Euler angle triple (x, y, z) says: turn this much around each axis. In three.js,
object.rotationis aTHREE.Euler, and every angle is in radians, not degrees. A half turn isMath.PI(3.14159...), a quarter turn isMath.PI / 2, a full turn is2 * Math.PI. Convert withTHREE.MathUtils.degToRad(90)andradToDeg. Writingrotation.y = 90is not an error: it is 90 radians, which turns by 116.6°. - 3
Order matters. Turning around x then y is not the same as y then x. The Euler
order("XYZ"by default;"YXZ"is also common for cameras and first-person views) says which one comes first. With the angles (90°, 90°, 0), the order "XYZ" sends the point (1, 0, 0) to (0, 1, 0) and "YXZ" sends it to (0, 0, −1). Pick one order and keep it. - 4
Gimbal lock. In order "XYZ", when the middle angle (y) is 90°, a change in x and a change in z turn the object about the same line. One of the three ways of turning is lost. The second example prints it: y = 90 with x = 30 gives the same quaternion as y = 90 with z = 30, so x and z do the same thing. Use the rotation lab below to see the rings line up.
- 5
Quaternions. A
THREE.Quaternionstores a rotation as four numbers (x, y, z, w): an axis and an angle, packed. It has no order and no lock.object.quaternionandobject.rotationare two views of the same turn: change one and three.js updates the other. Make one withsetFromAxisAngle(axis, angle),setFromEuler(euler)orlookAt. - 6
**
slerp: smooth turns.**a.slerp(b, t)(spherical linear interpolation) moves a quaternion a fractiont(0 to 1) of the way along the shortest arc tob, at a steady speed. Halfway from "no turn" to "half turn around y" is a quarter turn: the second example prints 45° at t = 0.25 and 90° at t = 0.5. Blending Euler angles number by number does not give a steady turn.
Explain it without notes
What is gimbal lock, when does it happen with order XYZ, and how does a quaternion avoid it?
Why is writing object.rotation.y = 90 a bug in three.js, and how do you find it?
Practice
Make a cube turn smoothly from its current pose to a 90° turn around y over one second, using slerp. Give the loop code.
Write the code that turns an object by exactly a quarter turn around y, then again by a quarter turn, and say what rotation.y holds afterwards.
Done when you can
Write angles in radians and convert with degToRad
Explain Euler order and say which order three.js uses by default
Explain gimbal lock and show it in the rotation lab
Use slerp for a smooth turn and lookAt to aim an object