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
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 causingNaNvalues inasin.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])\]
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.