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.
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.
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
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 — that convention is what the roll axis pivots around.
02
The axis triad. Red +X forward, green +Y left, blue +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.
03
The drag mapping. Horizontal drag integrates yaw about world Z, vertical drag pitches about the rotated Y — 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.
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.
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.
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
01
Live yaw / pitch / roll readout. Computed per render from the exact quaternion driving the mesh, via the same atan2–asin–atan2 chain as the Swift functions below. The view and the numbers can never disagree — they share one source of truth.
02
Quaternion → Euler panel. Four components in. The magnitude line reports ∣q∣ live; anything off 1.0 gets normalized before angle extraction, because the asin pitch term silently assumes a unit quaternion.
03
The pitch clamp. The asin argument is clamped to [−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.
04
Euler → Quaternion panel. Degrees in, half-angle products out — literally cos(θ/2), sin(θ/2) terms multiplied in ZYX order, matching quaternion(fromYaw:pitch:roll:) line for line.
05
Reset buttons.0° puts the camera behind the head (the mathematical zero orientation), 180° turns the face to you, and the sample pose (180∘,−35∘,35∘) exercises all three axes at once so you can verify the round-trip by eye: Euler in → quaternion out → angles back, identical numbers.
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.
Prices shown were retrieved from the Amazon Product Advertising API on 8 October 2026 and are indicative only — the price and availability on Amazon at the time of purchase apply.
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=(axsin2θ,aysin2θ,azsin2θ,cos2θ)
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=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.
The steel-blue arrow is the axis a; the plane it pierces is where the actual spinning happens, by angle θ.
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 Z, middle ring pitches around new Y, inner ring rolls around new X. With small pitch, those axes are distinct — you have three independent knobs. Now pitch the middle ring to 90∘. 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). At pitch=90∘,Ry(90∘)=00−1010100, and product collapses to R=Rz(yaw−roll)Ry(90∘). Yaw minus roll is what matters; their individual values are not even observable. Jacobian ∂R/∂(yaw,pitch,roll) drops rank 3→2. Inverse mapping yaw/pitch/roll from a rotation matrix becomes ill-conditioned: tiny noise slams you from (30∘,90∘,0∘) to (130∘,90∘,100∘).
It is not a bug in code. You cannot cover 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∘. Any other Euler convention just moves them elsewhere.
Verified, not asserted — same rotation, three labels
(yaw 30∘, pitch 90∘, roll 0∘) → same q as (yaw 50∘, pitch 90∘, roll 20∘) → same q as (yaw 0∘, pitch 90∘, roll −30∘). All share yaw−roll=30∘. Literally same four numbers x,y,z,w after normalization, not just close.
In code, pitch=arcsin(2(wy−zx)). At pitch=±90∘, argument approaches ±1 and rounding pushes it to ±1.0000002. Without max(−1,min(1,⋅)) clamp you get NaN and your 3D head vanishes. With the clamp you get a stable 90∘, but both general atan2 calls then get arguments near zero and return rounding noise, so the converter has a separate branch at the pole: roll =0, and yaw carries the whole determined angle (see the second worked example).
Why quaternion does not lock
q=(axsin(θ/2),aysin(θ/2),azsin(θ/2),cos(θ/2)). One axis a, one angle θ. No sequence of dependent axes to collapse. Composition is one Hamilton product q1⊗q2, interpolation is great-circle slerp q(t)=sinΩsin((1−t)Ω)q0+sinΩsin(tΩ)q1, 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=awbw−a⋅bv=awb+bwa+a×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
Property
Euler angles (yaw/pitch/roll)
Unit quaternion
Storage
3 numbers
4 numbers (1 redundant, via the unit constraint)
Gimbal lock
Yes (at pitch = ±90° for this ZYX convention)
No singularities anywhere
Composing two rotations
Multiply 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 interpolation
Naively 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-readable
Yes (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 s = 2 * (q.w * q.y - q.z * q.x) // Gimbal lock: at pitch ±90° only yaw ∓ roll is defined. Put it all in yaw. if abs(s) >= 1 - 1e-15 { let yaw = atan2(2 * (q.w * q.z - q.x * q.y), 1 - 2 * (q.x * q.x + q.z * q.z)) return (yaw, s > 0 ? .pi / 2 : -.pi / 2, 0) } 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, s))) 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 )}
Two worked examples
Both are solved when the page is built, by the same two functions the converter panels run, and checked in the site's test suite against a rotation matrix multiplied out from the three elementary rotations and converted with Shepperd's method.
Here c and s are the cosine and sine of half of the yaw (y), pitch (p) and roll (r) angles, for R = Rz(yaw) · Ry(pitch) · Rx(roll).
A · A general rotation and its round trip
Yaw 30°, pitch 20°, roll 10°.
w
0.9515
x
0.03813
y
0.1893
z
0.2393
Yaw back
30°
Pitch back
20°
Roll back
10°
The four components have norm 1, and the three angles come back unchanged because the pitch is well away from ±90°.
B · Pitch 90°: one rotation, many angle triples
Yaw 50°, pitch 90°, roll 20°. At pitch 90° only yaw − roll = 30° matters: R = Rz(yaw − roll) · Ry(90°).
w
0.683
x
-0.183
y
0.683
z
0.183
Yaw returned
30°
Pitch returned
90°
Roll returned
0°
Rotation error of returned angles
0°
The converter sees 2(wy − zx) = 1, sets roll to 0 and returns yaw = yaw − roll of the input. The triple differs from the one put in, but it rebuilds the same quaternion: the rotation error is 0°. The general atan2 formulas would have returned rounding noise here, a rotation about 17° away.
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.
Which rotation order and convention does the converter use?⌄
Right-handed axes, Hamilton quaternions stored as x, y, z, w, and R = Rz(yaw) · Ry(pitch) · Rx(roll): yaw about world Z first, then pitch about the new Y, then roll about the new X. That is intrinsic Z-Y-X, which is the same rotation as extrinsic X-Y-Z (roll, pitch, yaw about the fixed axes). Angles are degrees on the page and radians in the Swift listings. A different order, such as X-Y-Z intrinsic, gives a different quaternion for the same three numbers: for yaw 30°, pitch 20°, roll 10° the two orders differ by more than 0.01 in a component.
Are q and −q the same rotation?⌄
Yes. Negating all four components gives the same rotation, so two sensors or libraries can report opposite signs for one orientation. Both convert to the same yaw, pitch and roll here. When you interpolate or compare quaternions, flip one of them if their dot product is negative, otherwise you take the long way round.
What does the converter return at pitch 90°?⌄
At pitch ±90° yaw and roll turn about the same axis, so only their difference (at +90°) or their sum (at −90°) is defined. The converter returns roll = 0 and puts the whole angle in yaw. Yaw 50°, pitch 90°, roll 20° gives w, x, y, z = 0.683, -0.183, 0.683, 0.183, and converts back to yaw 30°, pitch 90°, roll 0°: different numbers, the same rotation. A converter without that branch returns two atan2 results of rounding noise there; before this was fixed, this one returned a rotation about 17° away from the input. Near gimbal lock, store and compare the quaternion, not the angles.
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.
Answers are computed here, in your browser, by the same functions that render the published pages — not looked up, not generated. Every one links to its page and to the public endpoint that returns it as JSON.
No match for
Try a lab name, or an engineering question like lowpass 1 kHz.