<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. BIM Math
  3. Rotations in depth
All lessons in this course

Lesson 30 of 44 14 minAdvanced

Euler angles and gimbal lock

Describe an orientation with three angles (yaw, pitch and roll), learn why the order of the three turns matters, and see why one pitch angle makes two of them do the same thing.

A rotation in 3D has three degrees of freedom, so the most obvious way to store one is as three angles. A camera in a walkthrough has a heading, a tilt up or down and maybe a bank. An inclined column might be stored as a turn in plan followed by a lean. These three-angle descriptions are called Euler angles. They are easy to type and easy to read, which is why dialogs and file formats use them.

The widget shows the same idea the way a physical gimbal (the frame of rings that holds a ship’s compass or a camera stabiliser) does: each ring turns about one axis and carries the rings inside it.

Interactive

Euler angles on three gimbal rings

Blue ring: yaw about Z. Green: pitch about the turned Y. Red: roll about the turned X, the nose. Move the sliders, then try them again with pitch at 90°.

XYZ
R = Rz·Ry·Rx[0.81 −0.50 0.30]
[0.47 0.87 0.17]
[−0.34 0.00 0.94]
nose points to(0.81, 0.47, −0.34)
yaw axis ∠ roll axis70°
Three angles, applied in order: yaw about Z, then pitch about the turned Y, then roll about the turned X. The yaw and roll axes are 70° apart (90° − |pitch|), so the three rings still turn the body three independent ways.

Yaw, pitch and roll

With Z up, the usual convention in BIM is:

  • Yaw (heading): a turn about the vertical Z axis, the angle you see in plan.
  • Pitch: a turn about the object’s own Y axis, after the yaw. It tips the nose up or down.
  • Roll (bank): a turn about the object’s own X axis, the nose, after both of the others.

As one matrix this is R = Rz(yaw) · Ry(pitch) · Rx(roll). Read from the right it says “roll about world X, then pitch about world Y, then yaw about world Z”. Read from the left it says “yaw, then pitch about the turned Y, then roll about the turned X”. Both readings give the same matrix, so you will see the convention described both ways: as turns about fixed axes in one order, or about moving axes in the reverse order.

The order matters

Three angles mean nothing until you also know the order and the axes. There are twelve possible conventions (ZYX, XYZ, ZXZ and so on), and different programs, file formats and libraries pick different ones. The same three numbers in a different order give a different orientation:

EulerOrder.cs
var nose = new V3(1, 0, 0);double yaw = 90, pitch = 30; var zyx = Mul(Rz(yaw), Ry(pitch));      // yaw, then pitch about the turned Yvar yzx = Mul(Ry(pitch), Rz(yaw));      // pitch about world Y first, then yawConsole.WriteLine($"Rz * Ry: nose -> {Apply(zyx, nose)}");Console.WriteLine($"Ry * Rz: nose -> {Apply(yzx, nose)}"); static double[,] Rz(double d) { double c = Cos(d), s = Sin(d); return new[,] { { c, -s, 0 }, { s, c, 0 }, { 0, 0, 1 } }; }static double[,] Ry(double d) { double c = Cos(d), s = Sin(d); return new[,] { { c, 0, s }, { 0, 1, 0 }, { -s, 0, c } }; }static double Cos(double d) => Math.Cos(d * Math.PI / 180);static double Sin(double d) => Math.Sin(d * Math.PI / 180); static double[,] Mul(double[,] a, double[,] b){    var m = new double[3, 3];    for (int i = 0; i < 3; i++)        for (int j = 0; j < 3; j++)            for (int k = 0; k < 3; k++) m[i, j] += a[i, k] * b[k, j];    return m;} static V3 Apply(double[,] m, V3 v) => new(    m[0, 0] * v.X + m[0, 1] * v.Y + m[0, 2] * v.Z,    m[1, 0] * v.X + m[1, 1] * v.Y + m[1, 2] * v.Z,    m[2, 0] * v.X + m[2, 1] * v.Y + m[2, 2] * v.Z); readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
Rz * Ry: nose -> (0, 0.866, -0.5)
Ry * Rz: nose -> (0, 1, 0)

In the first order the nose swings round to +Y and then tips down by 30°. In the second, the pitch happens while the nose still points along X, but the yaw then carries it to +Y, and the pitch was about world Y, which is now the nose itself: the tilt has turned into a roll and the nose stays level. (With Z up, a positive turn about Y tips the nose down, because the right-hand rule carries X towards −Z.)

Gimbal lock

Set the pitch in the widget to 90° and move the yaw and roll sliders. They now do the same thing. The pitch has tipped the red roll ring into the plane of the blue yaw ring, so both turn the body about the same vertical line. Three angles, but only two independent ways to turn: this is gimbal lock.

You can see it in the matrix. At pitch = 90°, Ry turns the X axis onto −Z, so a roll about the turned X is a turn about −Z, which is a yaw in the opposite direction. Only yaw − roll has any effect:

GimbalLock.cs
var pairs = new (double[] A, double[] B)[]{    (new double[] { 30, 90, 0 }, new double[] { 0, 90, -30 }),    (new double[] { 30, 90, 0 }, new double[] { 50, 90, 20 }),    (new double[] { 30, 89, 0 }, new double[] { 0, 89, -30 })};foreach (var (a, b) in pairs){    bool same = Same(Euler(a[0], a[1], a[2]), Euler(b[0], b[1], b[2]));    Console.WriteLine($"({string.Join(", ", a)}) and ({string.Join(", ", b)}): same orientation = {same}");} static double[,] Euler(double yaw, double pitch, double roll) => Mul(Rz(yaw), Mul(Ry(pitch), Rx(roll)));static double[,] Rx(double d) { double c = Cos(d), s = Sin(d); return new[,] { { 1, 0, 0 }, { 0, c, -s }, { 0, s, c } }; }static double[,] Ry(double d) { double c = Cos(d), s = Sin(d); return new[,] { { c, 0, s }, { 0, 1, 0 }, { -s, 0, c } }; }static double[,] Rz(double d) { double c = Cos(d), s = Sin(d); return new[,] { { c, -s, 0 }, { s, c, 0 }, { 0, 0, 1 } }; }static double Cos(double d) => Math.Cos(d * Math.PI / 180);static double Sin(double d) => Math.Sin(d * Math.PI / 180); static double[,] Mul(double[,] a, double[,] b){    var m = new double[3, 3];    for (int i = 0; i < 3; i++)        for (int j = 0; j < 3; j++)            for (int k = 0; k < 3; k++) m[i, j] += a[i, k] * b[k, j];    return m;} static bool Same(double[,] a, double[,] b){    for (int i = 0; i < 3; i++)        for (int j = 0; j < 3; j++)            if (Math.Abs(a[i, j] - b[i, j]) > 1e-9) return false;    return true;}
Output
(30, 90, 0) and (0, 90, -30): same orientation = True
(30, 90, 0) and (50, 90, 20): same orientation = True
(30, 89, 0) and (0, 89, -30): same orientation = False

Nothing is wrong with the orientation itself: the body can still point anywhere. What breaks is the description. Near pitch = ±90°:

  • Going back from a matrix to angles has no unique answer, so a round trip through a file or a dialog can change yaw and roll even though the object has not moved.
  • A small change in orientation can need a huge jump in yaw and roll, so animations and camera paths that blend the angles spin wildly.
  • One direction of turning simply cannot be reached by a small change of the three angles.

Every three-angle convention has a pitch like this somewhere; choosing a different order only moves the problem. In BIM it bites for things that really do point straight up or down: a camera looking straight down for a plan view, a vertical pipe or duct segment, a column whose “nose” is its axis.

Where Euler angles are still the right tool

  • User input and display: “rotate 30° in plan, tilt 5°” is how people think. Keep the angles for the dialog.
  • Rotations that stay well away from the lock: a family that only turns in plan and tilts a few degrees never gets near pitch = 90°.
  • Storage formats that ask for them. Convert once on import, then work with a matrix or a quaternion.

For composing, interpolating and storing rotations inside a program, the next two lessons use quaternions, which have no lock and no special angle.

Quick check

With R = Rz(yaw) · Ry(pitch) · Rx(roll) and Z up, which pitch causes gimbal lock?

Quick check

Two programs both store a rotation as three angles (10, 20, 30). Will the objects be oriented the same way?

Practice

Write ToEuler, which takes a rotation matrix and returns (yaw, pitch, roll) for the convention R = Rz·Ry·Rx. Use pitch = asin(−R[2,0]), yaw = atan2(R[1,0], R[0,0]) and roll = atan2(R[2,1], R[2,2]). At gimbal lock (|R[2,0]| close to 1) set roll to 0 and take yaw = atan2(−R[0,1], R[1,1]). Round-trip (40, 25, −60), (−120, −10, 170) and (75, 90, 15).

Show a hint

Build the matrix with Euler from GimbalLock.cs, convert back, and print the three angles rounded to one decimal. The third set cannot come back unchanged: only yaw − roll survives.

Show the solution
Solution.cs
foreach (var a in new[] { (40.0, 25.0, -60.0), (-120.0, -10.0, 170.0), (75.0, 90.0, 15.0) }){    var (y, p, r) = ToEuler(Euler(a.Item1, a.Item2, a.Item3));    Console.WriteLine($"{a} -> ({y:0.#}, {p:0.#}, {r:0.#})");} static (double Yaw, double Pitch, double Roll) ToEuler(double[,] m){    double sp = Math.Clamp(-m[2, 0], -1, 1);    double pitch = Deg(Math.Asin(sp));    if (Math.Abs(sp) > 0.99999)                                  // gimbal lock: only yaw - roll is known        return (Deg(Math.Atan2(-m[0, 1], m[1, 1])), pitch, 0);    return (Deg(Math.Atan2(m[1, 0], m[0, 0])), pitch, Deg(Math.Atan2(m[2, 1], m[2, 2])));} static double Deg(double r) => r * 180 / Math.PI;static double[,] Euler(double yaw, double pitch, double roll) => Mul(Rz(yaw), Mul(Ry(pitch), Rx(roll)));static double[,] Rx(double d) { double c = Cos(d), s = Sin(d); return new[,] { { 1, 0, 0 }, { 0, c, -s }, { 0, s, c } }; }static double[,] Ry(double d) { double c = Cos(d), s = Sin(d); return new[,] { { c, 0, s }, { 0, 1, 0 }, { -s, 0, c } }; }static double[,] Rz(double d) { double c = Cos(d), s = Sin(d); return new[,] { { c, -s, 0 }, { s, c, 0 }, { 0, 0, 1 } }; }static double Cos(double d) => Math.Cos(d * Math.PI / 180);static double Sin(double d) => Math.Sin(d * Math.PI / 180); static double[,] Mul(double[,] a, double[,] b){    var m = new double[3, 3];    for (int i = 0; i < 3; i++)        for (int j = 0; j < 3; j++)            for (int k = 0; k < 3; k++) m[i, j] += a[i, k] * b[k, j];    return m;}
Output
(40, 25, -60) -> (40, 25, -60)
(-120, -10, 170) -> (-120, -10, 170)
(75, 90, 15) -> (60, 90, 0)

Remember

  • Euler angles describe an orientation as three turns, such as yaw about Z, pitch about the turned Y and roll about the turned X.
  • The meaning depends on the axes, their order and the sign convention; the same three numbers in another convention are another orientation.
  • Rz·Ry·Rx can be read as fixed axes from the right or moving axes from the left: it is the same matrix.
  • At pitch = ±90° yaw and roll turn about the same line (gimbal lock): angles are not unique and small moves can need big jumps.
  • Use Euler angles for input and display; compose, store and interpolate with matrices or quaternions.
0 of 44 lessons completed