Skip to main content

Playground · app-grounded instrument

iOS craft

Quaternion ↔ Euler Converter

Convert quaternions to yaw/pitch/roll and back in real time. These are the same round-trip-verified formulas MotionLink uses for CMHeadphoneMotionManager, including the pitch clamping to avoid NaN drift.

Live preview

Drag left/right to yaw, up/down to pitch. Shift-drag to roll around the head's local nose-to-back axis. The 3D view constructs the rotation matrix straight from the quaternion. Axes: +X forward (red), +Y left (green), +Z up (blue). The default 180° yaw shows the face; "Reset to 0°" places the camera behind the head (the mathematical zero orientation).

See also: Head-Tracked Stereo Pan, which uses this same quaternion-to-yaw extraction for spatial audio tracking.

From Quaternion → Euler panel

3D model: "Practice Head Sculpt" by OverlyWiseBat, via Sketchfab.

Yaw · Z
0.00°
Pitch · Y
0.00°
Roll · X
0.00°

Converters

Quaternion → Euler

Implements yawPitchRoll(from:) below. Non-unit input is normalized before conversion.

Magnitude: 1.0000 (already unit length)

Yaw

0.00°

Pitch

0.00°

Roll

0.00°

Euler → Quaternion

Implements quaternion(fromYaw:pitch:roll:) below. Degrees in, unit quaternion out.

x

0.0000

y

0.0000

z

0.0000

w

0.0000

Anatomy of the instrument

Every pixel above answers to the math below. Here is what each piece of the visualization and both converter panels is actually doing, and why it is built that way.

The 3D viewport

  1. 01

    The head. An 18k-vertex sculpt (simplified from 185k with gltf-transform), painted matte brand-blue so specular highlights trace the geometry without skin-tone distraction. The nose defines local +X+X — that convention is what the roll axis pivots around.

  2. 02

    The axis triad. Red +X+X forward, green +Y+Y left, blue +Z+Z up. The arrows are deliberately not parented to the head — they are the inertial world frame, the same frame CoreMotion reports attitude against. You watch the body rotate against a fixed reference, which is exactly how sensor fusion thinks.

  3. 03

    The drag mapping. Horizontal drag integrates yaw about world ZZ, vertical drag pitches about the rotated YY — deltas pre-multiplied into the current quaternion (world-frame composition). Shift-drag post-multiplies a roll about the head's local nose axis. Two Hamilton-product orders on one canvas: world pre-multiply versus local post-multiply, the distinction that bit us in the bug story below.

  4. 04

    Lighting and shadow. Warm key from upper-front-right, cool fill from the opposite flank, warm rim from behind — a classic three-point rig — plus a low-intensity room environment for ambient. Neutral tone mapping keeps mid-tones from crushing. The ground shadow is a radial-gradient texture on a plane: a cheap depth cue with zero shadow-map cost.

  5. 05

    The render loop. There is no idle requestAnimationFrame spin. The scene re-renders on state change only — drag frames (rAF-throttled), panel inputs, resets. Sixty frames per second while you interact, zero while you read.

What the view constructs every frame — R(q)

R(q)=[12(y2+z2)2(xyzw)2(xz+yw)2(xy+zw)12(x2+z2)2(yzxw)2(xzyw)2(yz+xw)12(x2+y2)]R(q)=\begin{bmatrix} 1-2(y^2+z^2) & 2(xy-zw) & 2(xz+yw) \\ 2(xy+zw) & 1-2(x^2+z^2) & 2(yz-xw) \\ 2(xz-yw) & 2(yz+xw) & 1-2(x^2+y^2) \end{bmatrix}

The mesh's orientation matrix comes straight from the quaternion — never from Euler angles — so the viewport itself is immune to gimbal lock. Compare the diagonal entries against the denominators in the yaw/roll atan2 calls: they are the same terms.

Panels, readouts, and buttons

  1. 01

    Live yaw / pitch / roll readout. Computed per render from the exact quaternion driving the mesh, via the same atan2asinatan2 chain as the Swift functions below. The view and the numbers can never disagree — they share one source of truth.

  2. 02

    Quaternion → Euler panel. Four components in. The magnitude line reports q|q| live; anything off 1.0 gets normalized before angle extraction, because the asin pitch term silently assumes a unit quaternion.

  3. 03

    The pitch clamp. The asin argument is clamped to [1,1][-1, 1]. Floating-point drift past ±1 would otherwise yield NaN and blank every readout downstream. Feed the panel extreme values and watch the clamp hold.

  4. 04

    Euler → Quaternion panel. Degrees in, half-angle products out — literally cos(θ/2)\cos(\theta/2), sin(θ/2)\sin(\theta/2) terms multiplied in ZYX order, matching quaternion(fromYaw:pitch:roll:) line for line.

  5. 05

    Reset buttons. puts the camera behind the head (the mathematical zero orientation), 180° turns the face to you, and the sample pose (180,35,35)(180^\circ, -35^\circ, 35^\circ) exercises all three axes at once so you can verify the round-trip by eye: Euler in → quaternion out → angles back, identical numbers.

  6. 06

    The source line. The small mono label above the canvas ("From Quaternion → Euler panel") tells you which panel last drove the view. Typing in either panel re-derives the scene from scratch — no hidden state accumulates.

Gear behind this build

MotionLink stack · 8 picks

Head-tracking hardware8

More gear across every app: the full Gear list →

The math and physics, in full

Every 3D rotation boils down to four numbers. Here is what they actually mean, why yaw/pitch/roll has a hard singularity, and why rotation order matters. Confusing the order broke the drag controls on this very page, as detailed below.

What a quaternion actually is

Pick a 3D axis (a unit vector (aₓ, a_y, a_z)) and rotate around it by angle θ. Euler's rotation theorem states that any 3D rotation can be written this way. A unit quaternion stores this axis and angle in four components:

Axis–angle form

q=(axsinθ2, aysinθ2, azsinθ2, cosθ2)q = \bigl(a_x\sin\tfrac{\theta}{2},\ a_y\sin\tfrac{\theta}{2},\ a_z\sin\tfrac{\theta}{2},\ \cos\tfrac{\theta}{2}\bigr)

This is quatFromAxisAngle() in the script below. It is what CoreMotion's sensor fusion outputs under attitude.quaternion. If you are fetching raw IMU data, an MPU9250's onboard DMP spits out this exact format.

Unit constraint

x2+y2+z2+w2=1x^2 + y^2 + z^2 + w^2 = 1

To represent pure rotation without scaling or stretching, the quaternion must be normalized. We force-normalize the inputs in the Quaternion → Euler panel before calculating the angles.

Diagram of a rotation axis a with rotation angle theta around itθaROTATION PLANE ⊥ a

The steel-blue arrow is the axis a; the plane it pierces is where the actual spinning happens, by angle θ.

Two panels comparing three independent rotation rings versus gimbal lock at ninety degrees pitch, where the yaw and roll rings collapse onto the same axis

pitch ≈ 0°
three independent axes

pitch = 90°
yaw ≡ roll axis

Blue = yaw ring, green = pitch ring, red = roll ring. At 90° pitch the blue and red rings align (the dashed red ring traces the same axis as the solid blue one), collapsing three degrees of freedom down to two.

Why Euler angles break: gimbal lock

Picture three nested rings. Outer ring yaws around world ZZ, middle ring pitches around new YY, inner ring rolls around new XX. With small pitch, those axes are distinct — you have three independent knobs. Now pitch the middle ring to 9090^\circ. Outer yaw axis folds flat onto inner roll axis. You still have three rings, but only two distinct directions. Twist yaw, you turn roll. Twist roll, you turn yaw. One degree of freedom is gone.

Algebra says same thing. R=Rz(yaw)Ry(pitch)Rx(roll)R = R_z(\text{yaw}) R_y(\text{pitch}) R_x(\text{roll}). At pitch=90pitch=90^\circ,Ry(90)=[001010100]R_y(90^\circ)=\begin{bmatrix}0 & 0 & 1\\0 & 1 & 0\\-1 & 0 & 0\end{bmatrix}, and product collapses to R=Rz(yawroll)Ry(90)R = R_z(yaw - roll) R_y(90^\circ). Yaw minus roll is what matters; their individual values are not even observable. Jacobian R/(yaw,pitch,roll)\partial R / \partial(yaw,pitch,roll) drops rank 323\to2. Inverse mapping yaw/pitch/roll from a rotation matrix becomes ill-conditioned: tiny noise slams you from (30,90,0)(30^\circ,90^\circ,0^\circ) to (130,90,100)(130^\circ,90^\circ,100^\circ).

It is not a bug in code. You cannot cover SO(3)SO(3) — the space of all rotations — with three numbers globally without a singularity. Same reason you cannot comb a sphere flat. Euler chose to put singularities at north-south pitch ±90\pm90^\circ. Any other Euler convention just moves them elsewhere.

Verified, not asserted — same rotation, three labels

(yaw 3030^\circ, pitch 9090^\circ, roll 00^\circ) \to same qq as (yaw 5050^\circ, pitch 9090^\circ, roll 2020^\circ) \to same qq as (yaw 00^\circ, pitch 9090^\circ, roll 30-30^\circ). All share yawroll=30yaw - roll = 30^\circ. Literally same four numbers x,y,z,wx,y,z,w after normalization, not just close.

In code, pitch=arcsin(2(wyzx))pitch = \arcsin(2(w y - z x)). At pitch=±90pitch=\pm90^\circ, argument approaches ±1\pm1 and rounding pushes it to ±1.0000002\pm1.0000002. Without max(1,min(1,))\max(-1,\min(1,\cdot)) clamp you get NaN and your 3D head vanishes. With clamp you get stable 9090^\circ but yaw/roll split is arbitrary — we zero roll by convention.

Why quaternion does not lock

q=(axsin(θ/2),aysin(θ/2),azsin(θ/2),cos(θ/2))q = (a_x \sin(\theta/2), a_y \sin(\theta/2), a_z \sin(\theta/2), \cos(\theta/2)). One axis aa, one angle θ\theta. No sequence of dependent axes to collapse. Composition is one Hamilton product q1q2q_1 \otimes q_2, interpolation is great-circle slerp q(t)=sin((1t)Ω)sinΩq0+sin(tΩ)sinΩq1q(t)=\frac{\sin((1-t)\Omega)}{\sin\Omega} q_0 + \frac{\sin(t\Omega)}{\sin\Omega} q_1, constant angular velocity, shortest path, no poles.

Gimbal lock is loss of ability to tell yaw from roll. Quaternion never had that factorization, so nothing to lose.

Composing rotations: why multiplication order matters

Quaternions compose via the Hamilton product. Order matters here: q₁⊗q₂ isn't the same as q₂⊗q₁. The sequence determines if you are rotating in the fixed world coordinate system or the object's local body frame.

Hamilton product — q = a ⊗ b

w=awbwabw = a_w b_w - \mathbf{a}\cdot\mathbf{b}v=awb+bwa+a×b\mathbf{v} = a_w\,\mathbf{b} + b_w\,\mathbf{a} + \mathbf{a}\times\mathbf{b}

Implemented as quatMul() in the script. Pre-multiplying (quatMul(delta, current)) applies the rotation in world space. Post-multiplying (quatMul(current, delta)) applies it locally.

A real bug this caused

With the head rotated (40° yaw, 25° pitch), adding a 0.6 rad roll using world-space multiplication tumbles the nose by 0.4254 units. Local multiplication keeps the nose locked.

A true roll must never move the nose. The drag controls on this page had this wrong until we flipped the order. See the developer log D-020 for the full numeric breakdown.

Euler angles vs. quaternions, side by side

PropertyEuler angles (yaw/pitch/roll)Unit quaternion
Storage3 numbers4 numbers (1 redundant, via the unit constraint)
Gimbal lockYes (at pitch = ±90° for this ZYX convention)No singularities anywhere
Composing two rotationsMultiply 3×3 matrices or compute intermediate angles. Easy to get backwards.One Hamilton product. Order still matters, but it is a single explicit operation instead of a matrix chain.
Smooth interpolationNaively interpolating each angle can take the long way round or pass through gimbal lock.Slerp gives constant angular velocity along the shortest path, making it the standard choice for animation.
Human-readableYes (which is why the panels show yaw/pitch/roll instead of raw components)No (hence this calculator)

Physics engines and IMUs output quaternions because they're numerically stable. Humans prefer yaw/pitch/roll. This converter sits right at that boundary.

Two gotchas worth knowing

Gimbal-lock clamp

Floating-point drift will eventually push the pitch asin input outside [-1, 1], causing silent NaN propagation. Always clamp the input. You can trigger this clamp by feeding extreme values into the Quaternion → Euler converter above.

Relative reference frame

CoreMotion headphone tracking does not align to north or gravity. Zero is just whatever direction the headphones were facing when the API started. You have to manage offsets yourself.

If you want the full story on why these quirks wasted half a day of development time on AirPods Pro, read our field note: the field note →

Swift, both directions

These functions drop directly into Xcode. If you're reading raw IMU data from an Arduino instead of using CoreMotion, the same logic ports straight to C/C++.

Quaternion → Euler

func yawPitchRoll(from q: CMQuaternion) -> (yaw: Double, pitch: Double, roll: Double) {
    let yaw = atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z))
    let pitch = asin(max(-1, min(1, 2 * (q.w * q.y - q.z * q.x))))
    let roll = atan2(2 * (q.w * q.x + q.y * q.z), 1 - 2 * (q.x * q.x + q.y * q.y))
    return (yaw, pitch, roll)
}

Euler → Quaternion

func quaternion(fromYaw yaw: Double, pitch: Double, roll: Double) -> CMQuaternion {
    let cy = cos(yaw * 0.5), sy = sin(yaw * 0.5)
    let cp = cos(pitch * 0.5), sp = sin(pitch * 0.5)
    let cr = cos(roll * 0.5), sr = sin(roll * 0.5)
    return CMQuaternion(
        x: sr * cp * cy - cr * sp * sy,
        y: cr * sp * cy + sr * cp * sy,
        z: cr * cp * sy - sr * sp * cy,
        w: cr * cp * cy + sr * sp * sy
    )
}

Frequently asked questions

What is the difference between a quaternion and Euler angles?

Euler angles are intuitive: you rotate around three sequential axes (yaw, pitch, roll). But they lock up at ±90° pitch. Quaternions represent the rotation as a single axis and an angle stored as four numbers. They handle composition and interpolation cleanly without breaking, though they are impossible to read by eye.

Why do quaternions avoid gimbal lock?

Gimbal lock is a sequencing problem. When your pitch hits ±90°, the yaw and roll axes align. You lose a degree of freedom because rotating yaw and rotating roll do the exact same thing. Quaternions rotate around a single arbitrary axis in one step. Since there is no sequence of dependent axes to collapse, the singularity never occurs.

How do I convert a quaternion to Euler angles in Swift?

Grab the yawPitchRoll(from:) function on this page. The math is straightforward, but the critical part is clamping the pitch term. Floating-point precision issues will eventually push the asin argument past 1.0 or -1.0, and without a clamp, your app gets NaNs.

Why does CMHeadphoneMotionManager attitude drift or reset unexpectedly?

CoreMotion headphone tracking does not use absolute references like a compass. Wherever the headphones are when you call startDeviceMotionUpdates is 0, 0, 0. If they drift or the user adjusts them, they are out of alignment. You have to implement recentering: store the baseline orientation on a user click, then subtract it from incoming samples.

Why must a quaternion be a unit quaternion?

Rotation quaternions must have a length of 1 (x² + y² + z² + w² = 1). If you do not normalize them, applying the rotation will scale or distort your 3D models. The converter panels here automatically normalize your inputs to prevent that.

Shareable still

The instrument, captured—not illustrated.

This 16:9 frame is rendered from the real browser instrument above. It is the page's canonical preview for image search, link unfurls, and posts that need to show what the tool actually does.

Download 1280 × 720 JPEG
Quaternion ↔ Euler Converter — live MakerPortal instrument screenshot
Canonical capture · real UI · no generated scientific artwork