Rotation Conventions Cheat Sheet — quaternion order, Euler sequences, by library
Which libraries use xyzw vs wxyz quaternions, intrinsic vs extrinsic Euler angles, and which coordinate handedness — a reference table for ROS, Eigen, SciPy, MuJoCo, and URDF, so you stop silently swapping conventions when porting rotations between tools.
The single most common rotation bug isn't a math error — it's moving a rotation between two libraries that disagree on convention while looking identical on the page. Every row below produces a different, valid-looking rotation for the same four or three numbers. When in doubt, use the rotation converter's live 3D gizmo to check visually rather than trust the numbers.
Quaternion component order
| Library / ecosystem | Order | Notes |
|---|---|---|
ROS (geometry_msgs/Quaternion, tf2) | x, y, z, w | The field order in the message definition itself |
| glTF | x, y, z, w | Specified explicitly in the glTF 2.0 spec |
SciPy (scipy.spatial.transform.Rotation) | x, y, z, w | Rotation.from_quat([x,y,z,w]) — easy to get backwards coming from Eigen |
| Eigen (C++) | w, x, y, z | Eigen::Quaterniond(w,x,y,z) constructor order — but .coeffs() returns x,y,z,w. Both orders exist in the same library, which is exactly the trap. |
MuJoCo (XML quat attribute, mjData) | w, x, y, z | Documented MuJoCo convention throughout the C API and XML schema |
Unity (UnityEngine.Quaternion) | x, y, z, w | Also left-handed, Y-up — the coordinate system differs from ROS/URDF, not just the quaternion field order (see below) |
Verify against your specific library version before trusting this table for anything safety-relevant — component order is usually stable within a library but has changed historically in a few (Bullet physics being a known example), and this table can't track every release.
Euler angles: intrinsic vs extrinsic
Intrinsic ("rotating frame"): each rotation is about the body's own axes, as reoriented by the previous rotation. Extrinsic ("fixed/static frame"): every rotation is about the original, unmoving world axes. The same three numbers and the same axis order (say, X-Y-Z) give a different final orientation depending on which one you mean — they are not interchangeable, and "roll, pitch, yaw" alone doesn't disambiguate.
| Context | Convention |
|---|---|
URDF <origin rpy=""> | Extrinsic X→Y→Z (fixed-axis) — verified in this site's own SDF/DH converters against an independent matrix implementation |
SciPy Rotation.from_euler() | Explicit either way: uppercase sequence (e.g. 'XYZ') = intrinsic, lowercase (e.g. 'xyz') = extrinsic — the cleanest API of any library on this list, precisely because it forces you to say which one you mean |
| Aerospace / many robotics textbooks | Often intrinsic Z→Y→X ("yaw, pitch, roll", written 3-2-1) — different axis order AND different frame convention from URDF's rpy, despite both being called "roll-pitch-yaw" in prose |
There are 12 valid axis sequences total (6 Tait-Bryan: XYZ, XZY, YXZ, YZX, ZXY, ZYX; 6 proper Euler with a repeated axis: XYX, XZX, YXY, YZY, ZXZ, ZYZ), each readable as intrinsic or extrinsic — 24 combinations. The rotation converter supports all of them explicitly rather than assuming you mean XYZ.
Coordinate handedness & up-axis
| Ecosystem | Handedness | Up axis |
|---|---|---|
| ROS / URDF / Gazebo / MuJoCo | Right-handed | Z-up |
| Unity | Left-handed | Y-up |
| Unreal Engine | Left-handed | Z-up |
| glTF, most graphics/OpenGL-derived tools | Right-handed | Y-up |
A rotation ported correctly component-for-component between a right-handed and a left-handed system still comes out mirrored — handedness has to be converted explicitly (typically by negating one axis and one rotation direction), not just relabeled. This is a separate problem from quaternion component order and Euler intrinsic/extrinsic, and all three compound independently when moving a rotation between, say, a CAD tool and a game engine.
Axis-angle and rotation matrices
Axis-angle (an axis vector + an angle) and 3×3 rotation matrices don't have a component-order ambiguity the way quaternions and Euler angles do — but they still inherit whatever handedness convention the surrounding system uses, and a rotation matrix's row-major vs column-major storage is a separate (and separately common) source of transposed-looking bugs when passing raw arrays between libraries.
When two numbers-that-should-match don't, checking visually beats re-deriving the math by hand — paste both representations into the rotation converter and compare the 3D gizmo.