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:
- 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.
- 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)\).
- Orientation: A native
btCylinderShapeZpoints 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
- Segment Count: A higher segment count (e.g., 24 to 32) creates a smoother torus but increases collision detection overhead. For most gameplay applications, 12 to 16 segments provide an ideal balance between physical accuracy and performance.
- Memory Management: Ammo.js is a WebAssembly/C++
port and does not garbage-collect its objects automatically. Ensure you
call
Ammo.destroy()on temporary vectors and quaternions as shown in the code to avoid memory leaks. The childcylinderShapeonly needs to be instantiated once and can be shared among all segments in the compound shape.