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.
Yaw, pitch and roll
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.
The order matters
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)})";}Rz * Ry: nose -> (0, 0.866, -0.5) Ry * Rz: nose -> (0, 1, 0)
Gimbal lock
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;}(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
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.
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.
Show a hint
Show the solution
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;}(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.