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.
- Create a parent
THREE.Group. - Add the visual mesh to this group.
- 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.