Convert Ammo.js Transform to Euler Angles

This article explains how to extract and convert an ammo.js transform object into Euler angles (pitch, yaw, and roll) represented in degrees. This process is essential for displaying 3D physics rotations in a standard 2D user interface or inspector panel. We will walk through extracting the quaternion from the btTransform and converting it using both pure JavaScript mathematics and a Three.js helper method.

Extracting the Quaternion from btTransform

In ammo.js (the Emscripten port of the Bullet physics engine), rotation is stored as a quaternion within the btTransform object. To read this rotation, you must first extract the quaternion components (\(x, y, z, w\)).

// Assuming 'transform' is an instance of Ammo.btTransform
const quaternion = transform.getRotation();

const qx = quaternion.x();
const qy = quaternion.y();
const qz = quaternion.z();
const qw = quaternion.w();

Pure JavaScript Mathematical Conversion

Once you have the quaternion components, you can use trigonometric formulas to convert them into Euler angles. The following function converts the quaternion values into Pitch, Yaw, and Roll in radians, and then converts those values into degrees for UI display.

function quaternionToEulerDegrees(qx, qy, qz, qw) {
    // Roll (x-axis rotation)
    const sinr_cosp = 2 * (qw * qx + qy * qz);
    const cosr_cosp = 1 - 2 * (qx * qx + qy * qy);
    const roll = Math.atan2(sinr_cosp, cosr_cosp);

    // Pitch (y-axis rotation)
    const sinp = 2 * (qw * qy - qz * qx);
    let pitch;
    if (Math.abs(sinp) >= 1) {
        pitch = Math.sign(sinp) * (Math.PI / 2); // Use 90 degrees if out of range
    } else {
        pitch = Math.asin(sinp);
    }

    // Yaw (z-axis rotation)
    const siny_cosp = 2 * (qw * qz + qx * qy);
    const cosy_cosp = 1 - 2 * (qy * qy + qz * qz);
    const yaw = Math.atan2(siny_cosp, cosy_cosp);

    // Convert radians to degrees
    const radToDeg = 180 / Math.PI;
    return {
        x: roll * radToDeg,  // Pitch / Roll depending on your coordinate system
        y: pitch * radToDeg,
        z: yaw * radToDeg
    };
}

// Usage:
const eulerAngles = quaternionToEulerDegrees(qx, qy, qz, qw);
console.log(`X: ${eulerAngles.x.toFixed(2)}°, Y: ${eulerAngles.y.toFixed(2)}°, Z: ${eulerAngles.z.toFixed(2)}°`);

If you are using ammo.js alongside Three.js, you can leverage the built-in math utilities of Three.js to handle the conversion. This avoids manual trigonometric calculations and accounts for gimbal lock and rotation orders (such as XYZ, YXZ, etc.) automatically.

// 1. Create reusable Three.js math objects to avoid garbage collection overhead
const threeQuat = new THREE.Quaternion();
const threeEuler = new THREE.Euler();

// 2. Copy values from Ammo quaternion to Three.js quaternion
threeQuat.set(qx, qy, qz, qw);

// 3. Set the Euler angles from the quaternion (default order is 'XYZ')
threeEuler.setFromQuaternion(threeQuat, 'XYZ');

// 4. Convert to degrees for your UI
const uiRotation = {
    x: threeEuler.x * (180 / Math.PI),
    y: threeEuler.y * (180 / Math.PI),
    z: threeEuler.z * (180 / Math.PI)
};