Extract Pitch, Yaw, Roll from Matrix in GLSL

This article provides the mathematical foundation and shader code to extract pitch, yaw, and roll Euler angles from a 3D rotation matrix in GLSL. It details the trigonometric decomposition under the standard Tait-Bryan convention, outlines how to handle the column-major layout native to GLSL, provides solutions for gimbal lock singularities, and supplies a performant GLSL function ready for graphics applications.

Understanding Matrix Conventions in GLSL

In GLSL, matrices are stored in column-major order. When accessing a matrix variable mat3 m, m[c][r] references the element at column c and row r.

The extraction algorithm depends on the rotation order. A common convention in computer graphics is the Tait-Bryan \(ZYX\) sequence (Roll around \(Z\), Yaw around \(Y\), and Pitch around \(X\)), which corresponds to:

\[R = R_z(\text{roll}) \cdot R_y(\text{yaw}) \cdot R_x(\text{pitch})\]

Multiplying these matrices produces the following combined rotation matrix:

\[R = \begin{bmatrix} \cos\psi\cos\theta & \cos\psi\sin\theta\sin\phi - \sin\psi\cos\phi & \cos\psi\sin\theta\cos\phi + \sin\psi\sin\phi \\ \sin\psi\cos\theta & \sin\psi\sin\theta\sin\phi + \cos\psi\cos\phi & \sin\psi\sin\theta\cos\phi - \cos\psi\sin\phi \\ -\sin\theta & \cos\theta\sin\phi & \cos\theta\cos\phi \end{bmatrix}\]

Where:

  • \(\phi\) represents Pitch (rotation around the \(X\)-axis)
  • \(\theta\) represents Yaw (rotation around the \(Y\)-axis)
  • \(\psi\) represents Roll (rotation around the \(Z\)-axis)

Extraction Algorithm

  1. Yaw (\(\theta\)): The element at row 2, column 0 (\(m_{20}\), accessed as m[0][2] in GLSL) corresponds to \(-\sin\theta\). Yaw is calculated using the inverse sine: \[\theta = \arcsin(-\text{clamp}(m_{20}, -1.0, 1.0))\] Clamping prevents floating-point inaccuracies from causing NaN values in asin.

  2. Gimbal Lock Check: When \(\theta \approx \pm\frac{\pi}{2}\) radians (\(\pm 90^\circ\)), \(\cos\theta = 0\), causing a singularity known as gimbal lock. In this state, pitch and roll become collinear.

    • If \(m_{20} \approx -1.0\) or \(m_{20} \approx 1.0\), set Roll to \(0\) and resolve Pitch using the remaining non-zero terms: \[\phi = \text{atan2}(-m_{12}, m_{11}) = \text{atan2}(-m[2][1], m[1][1])\]
  3. Standard Pitch (\(\phi\)) and Roll (\(\psi\)): Away from gimbal lock, the remaining angles are derived using two-argument arctangent (atan(y, x) in GLSL): \[\phi = \text{atan2}(m_{21}, m_{22}) = \text{atan2}(m[1][2], m[2][2])\] \[\psi = \text{atan2}(m_{10}, m_{00}) = \text{atan2}(m[0][1], m[0][0])\]

GLSL Implementation

// Extracts Euler angles (Pitch, Yaw, Roll) in radians from a 3x3 rotation matrix.
// Rotation order: ZYX (Roll * Yaw * Pitch)
// Output: vec3(pitch, yaw, roll)
vec3 extractEulerAnglesZYX(mat3 m) {
    vec3 angles;

    // Clamp value to safe domain for asin to prevent NaN
    float sinYaw = clamp(-m[0][2], -1.0, 1.0);
    angles.y = asin(sinYaw); // Yaw

    // Check for Gimbal Lock (cos(yaw) near 0)
    if (abs(sinYaw) > 0.99999) {
        // Gimbal lock case: set roll to 0 and calculate pitch
        angles.z = 0.0;
        angles.x = atan(-m[2][1], m[1][1]);
    } else {
        // General non-degenerate case
        angles.x = atan(m[1][2], m[2][2]); // Pitch
        angles.z = atan(m[0][1], m[0][0]); // Roll
    }

    return angles;
}

Considerations

  • Unit Output: The computed angles are returned in radians. Use the built-in degrees() function if degree values are needed.
  • Matrix Normalization: The input matrix must be orthonormal (pure rotation without non-uniform scaling or skewing). If the matrix contains scale factors, normalize each column vector of the matrix before decomposition.