Command Palette

Search for a command to run...

Hectal
Course outline
Topic 2.6Beginner ~23 min · step 6 of 7

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.

0/7 · 0%

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. 1

    Change the order

    In the first example, change "XYZ" to "YXZ" in the first Euler (a). Both lines now say order YXZ sends (1, 0, 0) to 0, 0, -1: the same angles in the same order give the same turn.

  2. 2

    Step out of the lock

    In the second example, replace Math.PI / 2 in lock() with THREE.MathUtils.degToRad(80). The two quaternions are now different (0.198, 0.621, 0.166, 0.74 and 0.166, 0.621, 0.198, 0.74) and the line says same 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

Radians, Euler order and a full turn New tab

This example is numbers only. The first line is a mistake on purpose: 90 is read as 90 radians.

idle

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 degrees

Try 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.

Gimbal lock and slerp, in numbers New tab

No picture: the lock and the smooth turn are shown with numbers.

idle

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 degrees
lookAt and a slerp swing New tab

The 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.

idle

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.000

Break 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;.

terminal
$ Run
── what you'll see ──
degToRad(90) = 90.0000 (that is Math.PI / 2 = 1.5708)

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 rotation and quaternion in sync with change callbacks (_onChange). Setting rotation.y updates 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.

  • ▸

    slerp takes 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. 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. 2

    Euler angles, in radians. An Euler angle triple (x, y, z) says: turn this much around each axis. In three.js, object.rotation is a THREE.Euler, and every angle is in radians, not degrees. A half turn is Math.PI (3.14159...), a quarter turn is Math.PI / 2, a full turn is 2 * Math.PI. Convert with THREE.MathUtils.degToRad(90) and radToDeg. Writing rotation.y = 90 is not an error: it is 90 radians, which turns by 116.6°.

  3. 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. 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. 5

    Quaternions. A THREE.Quaternion stores a rotation as four numbers (x, y, z, w): an axis and an angle, packed. It has no order and no lock. object.quaternion and object.rotation are two views of the same turn: change one and three.js updates the other. Make one with setFromAxisAngle(axis, angle), setFromEuler(euler) or lookAt.

  6. 6

    **slerp: smooth turns.** a.slerp(b, t) (spherical linear interpolation) moves a quaternion a fraction t (0 to 1) of the way along the shortest arc to b, 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

01

What is gimbal lock, when does it happen with order XYZ, and how does a quaternion avoid it?

02

Why is writing object.rotation.y = 90 a bug in three.js, and how do you find it?

Practice

01

Make a cube turn smoothly from its current pose to a 90° turn around y over one second, using slerp. Give the loop code.

02

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