Aligning 3D Model Pivot with Ammo.js Center of Mass

This article explains how to precisely align a 3D model’s visual pivot point with the center of mass of an ammo.js physics body. By centering the geometry of your 3D mesh (such as in Three.js) and matching it to the rigid body’s local origin, you can prevent visual misalignment and unnatural rotational behavior during physics simulations.

The Problem: Origin Mismatch

In physics engines like ammo.js (a port of Bullet Physics), the origin of a rigid body’s local coordinate system is always its center of mass. However, 3D models exported from modeling software often have pivot points located at their base, corner, or an arbitrary offset.

If you apply an ammo.js transform directly to a mesh with an offset pivot, the visual mesh will rotate around its original pivot rather than its physical center of mass, resulting in unrealistic wobbling or floating.


Step 1: Center the Visual Geometry

To align the pivot point, you must offset the visual mesh’s geometry so that its center of mass sits at the local origin \((0, 0, 0)\). In Three.js, you can achieve this automatically using the center() method on the geometry.

// Load or create your visual mesh
const geometry = mesh.geometry;

// Compute the bounding box to find the current center
geometry.computeBoundingBox();
const centerOffset = new THREE.Vector3();
geometry.boundingBox.getCenter(centerOffset);

// Shift the geometry vertices so the bounding box center is at (0, 0, 0)
geometry.center();

By calling geometry.center(), you shift the internal vertex data. The mesh’s local origin \((0, 0, 0)\) is now perfectly aligned with the center of its bounding box, which acts as the center of mass for uniform density shapes.


Step 2: Retain the Offset (Compound Method)

If you cannot modify the geometry directly (for example, with animated skinned meshes or complex hierarchies), you must use a nested Group structure to act as a pivot compensation layer.

  1. Create a parent THREE.Group.
  2. Add the visual mesh to this group.
  3. Position the visual mesh inside the group using the negative offset of the center of mass.
const visualGroup = new THREE.Group();

// Offset the mesh inside the group to align center of mass with group origin
mesh.position.set(-centerOffset.x, -centerOffset.y, -centerOffset.z);
visualGroup.add(mesh);

scene.add(visualGroup);

In this setup, you will apply the ammo.js physics transforms to the parent visualGroup rather than the mesh itself.


Step 3: Create the Ammo.js Collision Shape

When creating the corresponding ammo.js collision shape, use the dimensions of the centered geometry.

// Get half-extents for a box shape
const size = new THREE.Vector3();
geometry.boundingBox.getSize(size);

const halfExtents = new Ammo.btVector3(size.x * 0.5, size.y * 0.5, size.z * 0.5);
const colShape = new Ammo.btBoxShape(halfExtents);

Because the collision shape’s local origin is at its center, it now perfectly mirrors the centered visual geometry or the origin of your parent visualGroup.


Step 4: Synchronize Physics and Visuals

During your render loop, extract the transform from the ammo.js motion state and apply it directly to the centered mesh (or the parent visualGroup if using the compound method).

function updatePhysicsObject(ammoRigidBody, visualObject) {
    const transform = new Ammo.btTransform();
    const motionState = ammoRigidBody.getMotionState();
    
    if (motionState) {
        motionState.getWorldTransform(transform);
        const origin = transform.getOrigin();
        const rotation = transform.getRotation();
        
        // Update visual object position
        visualObject.position.set(origin.x(), origin.y(), origin.z());
        
        // Update visual object rotation
        visualObject.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
    }
}

By ensuring the visual mesh (or group) rotates around the exact same local origin as the btCollisionShape, your 3D model will behave realistically during collisions, falls, and angular rotations.