Create Torus Collision Shape in Ammo.js

This article explains how to programmatically construct a 3D torus collision shape in the Ammo.js physics engine using compound shapes. Because Ammo.js does not feature a native torus collision primitive, this guide demonstrates how to approximate a torus by assembling multiple cylinder shapes into a ring using btCompoundShape, complete with the necessary mathematical formulas and ready-to-use JavaScript code.

The Concept: Approximating a Torus

To create a torus collision shape, we divide the torus into a series of short, straight segments. Each segment is represented by a cylinder. By placing these cylinders end-to-end along the circumference of the torus’s major radius, we build a closed ring that behaves like a torus in the physics simulation.

We use btCompoundShape to group these individual cylinder shapes into a single rigid collision body.

Step-by-Step Implementation

The following JavaScript function generates a torus collision shape using Ammo.js. It calculates the correct position and rotation for each segment using a Z-aligned cylinder (btCylinderShapeZ), which simplifies the rotation math.

function createTorusCollisionShape(Ammo, majorRadius, minorRadius, segments) {
    // 1. Create the parent compound shape
    const compoundShape = new Ammo.btCompoundShape();

    // 2. Calculate the length of each cylinder segment using the chord length formula
    const segmentLength = 2 * majorRadius * Math.sin(Math.PI / segments);

    // 3. Create the base cylinder shape (Z-aligned)
    // btCylinderShapeZ half-extents are: (radiusX, radiusY, halfLengthZ)
    const halfExtents = new Ammo.btVector3(minorRadius, minorRadius, segmentLength / 2);
    const cylinderShape = new Ammo.btCylinderShapeZ(halfExtents);

    // Temp variables to prevent garbage collection overhead in the loop
    const transform = new Ammo.btTransform();
    const position = new Ammo.btVector3();
    const rotation = new Ammo.btQuaternion();
    const upAxis = new Ammo.btVector3(0, 1, 0);

    for (let i = 0; i < segments; i++) {
        // Calculate the angle for the current segment
        const angle = (i * 2 * Math.PI) / segments;

        // Calculate position on the horizontal XZ plane
        const x = majorRadius * Math.cos(angle);
        const z = majorRadius * Math.sin(angle);
        const y = 0;

        position.setValue(x, y, z);
        transform.setIdentity();
        transform.setOrigin(position);

        // Align the Z-axis of the cylinder along the tangent of the circle
        // Rotating around the Y-axis by -angle aligns the segment perfectly
        rotation.setRotation(upAxis, -angle);
        transform.setRotation(rotation);

        // Add the child shape to the compound shape
        compoundShape.addChildShape(transform, cylinderShape);
    }

    // Clean up temporary Ammo objects from memory
    Ammo.destroy(halfExtents);
    Ammo.destroy(transform);
    Ammo.destroy(position);
    Ammo.destroy(rotation);
    Ammo.destroy(upAxis);

    return compoundShape;
}

How the Alignment Math Works

To arrange the cylinders in a perfect circle, the algorithm relies on trigonometry and rotation matrices:

  1. Segment Length: The length of each cylinder segment is determined using the chord length of the circle sector: \(2 \cdot R \cdot \sin(\pi / N)\), where \(R\) is the major radius and \(N\) is the number of segments. This ensures the ends of the cylinders touch without leaving large gaps or overlapping excessively.
  2. Positioning: For each segment at index \(i\), we compute its angle \(\theta\) around the circle. The center of the segment is placed at \((R \cos\theta, 0, R \sin\theta)\).
  3. Orientation: A native btCylinderShapeZ points along the Z-axis. At \(\theta = 0\), the tangent of the circle points exactly along the Z-axis, meaning no rotation is required. For any other angle \(\theta\), rotating the cylinder around the Y-axis (upward axis) by \(-\theta\) aligns it perfectly with the circle’s tangent vector at that point.

Performance and Memory Considerations