Convert Ammo.js Transform to WebGL Matrix

This article explains the standard approach for converting an Ammo.btTransform object from the ammo.js physics library into a flat 16-element float array compatible with 4x4 WebGL matrices. We will cover the math behind the conversion, the standard extraction of translation and rotation data, and how to structure the final column-major array for your WebGL shaders.

Understanding the Matrix Structure

WebGL applications and shader programs expect 4x4 matrices to be formatted as flat, 16-element, column-major arrays (Float32Array).

A standard 3D transformation matrix containing rotation (\(R\)) and translation (\(T\)) is structured as follows:

\[ \begin{bmatrix} R_{00} & R_{10} & R_{20} & 0 \\ R_{01} & R_{11} & R_{21} & 0 \\ R_{02} & R_{12} & R_{22} & 0 \\ T_x & T_y & T_z & 1 \end{bmatrix} \]

In a flat WebGL-compatible array, the indices map directly to these columns:

[
  R00, R01, R02, 0,  // Column 1 (X basis)
  R10, R11, R12, 0,  // Column 2 (Y basis)
  R20, R21, R22, 0,  // Column 3 (Z basis)
  Tx,  Ty,  Tz,  1   // Column 4 (Translation)
]

Step-by-Step Conversion Process

The most reliable and memory-efficient way to convert an Ammo.btTransform in JavaScript is to extract the translation vector and the rotation quaternion, then manually construct the 16-element array. This avoids allocating temporary Emscripten heap memory.

1. Extract Position and Rotation from Ammo.js

First, retrieve the origin (position) and the rotation (quaternion) from your active btTransform instance:

// Assumes 'transform' is an instance of Ammo.btTransform
const origin = transform.getOrigin();
const rotation = transform.getRotation();

const tx = origin.x();
const ty = origin.y();
const tz = origin.z();

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

2. Math for Quaternion to 4x4 Matrix

To convert the quaternion \((qx, qy, qz, qw)\) and translation \((tx, ty, tz)\) into the 16-element array, use the standard mathematical formula for quaternion-to-rotation-matrix conversion:

function ammoTransformToWebGLArray(transform, outArray = new Float32Array(16)) {
    const origin = transform.getOrigin();
    const rotation = transform.getRotation();

    const tx = origin.x();
    const ty = origin.y();
    const tz = origin.z();

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

    // Pre-calculate products for performance
    const x2 = qx + qx;
    const y2 = qy + qy;
    const z2 = qz + qz;

    const xx = qx * x2;
    const xy = qx * y2;
    const xz = qx * z2;
    const yy = qy * y2;
    const yz = qy * z2;
    const zz = qz * z2;
    const wx = qw * x2;
    const wy = qw * y2;
    const wz = qw * z2;

    // Column 1
    outArray[0] = 1 - (yy + zz);
    outArray[1] = xy + wz;
    outArray[2] = xz - wy;
    outArray[3] = 0;

    // Column 2
    outArray[4] = xy - wz;
    outArray[5] = 1 - (xx + zz);
    outArray[6] = yz + wx;
    outArray[7] = 0;

    // Column 3
    outArray[8] = xz + wy;
    outArray[9] = yz - wx;
    outArray[10] = 1 - (xx + yy);
    outArray[11] = 0;

    // Column 4 (Translation)
    outArray[12] = tx;
    outArray[13] = ty;
    outArray[14] = tz;
    outArray[15] = 1;

    return outArray;
}

Alternative: Using WebGL Math Libraries

If you use standard WebGL helper libraries like glMatrix or Three.js, you can leverage their built-in composition methods instead of writing the raw math.

Using glMatrix

import { mat4, vec3, quat } from 'gl-matrix';

const outMatrix = mat4.create(); // Float32Array(16)

const position = vec3.fromValues(origin.x(), origin.y(), origin.z());
const rotationQuat = quat.fromValues(rotation.x(), rotation.y(), rotation.z(), rotation.w());

mat4.fromRotationTranslation(outMatrix, rotationQuat, position);

Using Three.js

import { Matrix4, Vector3, Quaternion } from 'three';

const matrix = new Matrix4();
const position = new Vector3(origin.x(), origin.y(), origin.z());
const quaternion = new Quaternion(rotation.x(), rotation.y(), rotation.z(), rotation.w());
const scale = new Vector3(1, 1, 1);

matrix.compose(position, quaternion, scale);
// Access flat array via matrix.elements