Playground · app-grounded instrument
iOS craftQuaternion ↔ 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
- 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 — that convention is what the roll axis pivots around.
- 02
The axis triad. Red forward, green left, blue 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 , vertical drag pitches about the rotated — 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
requestAnimationFramespin. 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)
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–atan2chain 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 live; anything off 1.0 gets normalized before angle extraction, because the
asinpitch term silently assumes a unit quaternion. - 03
The pitch clamp. The
asinargument is clamped to . Floating-point drift past ±1 would otherwise yieldNaNand 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 , 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 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.
Gear behind this build
MotionLink stack · 8 picks
Head-tracking hardware8
$199.99WearableApple AirPods Pro 3 Wireless Earbuds, Active Noise Cancellation, Live Translation, Heart Rate Sensing, Hearing Aid Feature, Bluetooth Headphones, Spatial Audio, High-Fidelity Sound, USB-C Charging
MotionLink's head-tracking feature depends on the Headphone Motion API, which requires AirPods Pro.
$132.47Book3D Math Primer for Graphics and Game Development
Chapters 8-10 derive quaternion multiplication, Euler-angle gimbal lock, and the conversion matrix this playground verifies live — the same math driving MotionLink's head-pose.
$24.20MicrocontrollerArduino Nano 33 BLE Rev2 [ABX00071] - nRF52840 Microcontroller, Bluetooth Low Energy (BLE), MicroPython Support, Small Form Factor, 3.3V for IoT & Wireless Projects
nRF52840 board running ArduinoBLE — same chipset family as XIAO nRF52840 in BLExAR builds, exposing GATT structure this page visualizes as byte-level CSV frames.
$39.13SensorAdafruit 9-DOF Absolute Orientation IMU Fusion Breakout - BNO055
Bosch BNO055 fusion IMU breakout — quaternion/Euler output for flight, SLAM, and head-tracking labs. Closest verified Amazon listing to BNO085 workflows.
$32.00SensorBNO055 9-DOF Breakout Sensor
BNO055 9-DoF fusion IMU breakout for head-tracking and orientation demos.
$14.98MicrocontrollerELEGOO UNO R3 Board ATmega328P with USB Cable(Arduino-Compatible) for Arduino
ATmega328P-based Uno R3 clone — used across BLExAR's RFID, GPS, and joystick hardware builds. Also the usual microcontroller for reading an IMU's raw quaternion over I2C.
$43.99SensorEC Buying GY-MPU9250 9 Axis Sensor 9 DOF Accelerometer with Gyroscope and Magnetic Field Sensors, 16 Bit AD Converter Data Output IIC I2C SPI
9-DoF IMU used in BLExAR's calibration builds. The onboard magnetometer is a 3-axis Hall-effect sensor — the same physics modeled by the magnetic field-line tracer — while the DMP outputs attitude as a quaternion matching CMHeadphoneMotionManager's format.
$9.98ApparatusPrecision Metal Gyroscope Toy Rainbow Anti Gravity Spinner - Educational Physics Toys & Gyroscope for Adults Illusion, Balance Desk Fidget for Office, Cool Science Gadgets for Adults & Teens
Spinning rotor demonstrating angular momentum conservation and precession — the physical intuition behind why quaternions avoid gimbal lock where Euler angles fail.
Prices shown were retrieved from the Amazon Product Advertising API on 19 July 2026 and are indicative only — the price and availability on Amazon at the time of purchase apply.
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
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
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 , middle ring pitches around new , inner ring rolls around new . With small pitch, those axes are distinct — you have three independent knobs. Now pitch the middle ring to . 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. . At ,, and product collapses to . Yaw minus roll is what matters; their individual values are not even observable. Jacobian drops rank . Inverse mapping yaw/pitch/roll from a rotation matrix becomes ill-conditioned: tiny noise slams you from to .
It is not a bug in code. You cannot cover — 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 . Any other Euler convention just moves them elsewhere.
Verified, not asserted — same rotation, three labels
(yaw , pitch , roll ) same as (yaw , pitch , roll ) same as (yaw , pitch , roll ). All share . Literally same four numbers after normalization, not just close.
In code, . At , argument approaches and rounding pushes it to . Without clamp you get NaN and your 3D head vanishes. With clamp you get stable but yaw/roll split is arbitrary — we zero roll by convention.
Why quaternion does not lock
. One axis , one angle . No sequence of dependent axes to collapse. Composition is one Hamilton product , interpolation is great-circle slerp , 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
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 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
Continue the experiment