Optimize 3D Mesh for Ammo.js Convex Hull
This article explains how to optimize complex, imported 3D meshes for
use with the ammo.js physics engine’s convex hull generator
(btConvexHullShape). You will learn how to reduce vertex
counts, clean up geometry in 3D modeling software, and properly extract
and pass vertex data in your JavaScript code to prevent performance lag
and browser crashes.
Why Optimization is Necessary
When you import a 3D model (such as a GLTF or OBJ file), it often
contains thousands of vertices designed for visual fidelity. Passing a
high-density mesh directly to the btConvexHullShape
constructor in ammo.js is highly inefficient. Ammo.js must calculate the
outermost boundary wrapping all these points. High vertex counts lead to
slow initialization times, high memory overhead, and severe CPU
bottlenecks during physics steps. To maintain a smooth frame rate, you
must feed the generator a simplified, low-poly representation of the
mesh.
Step 1: Decimate the Mesh in 3D Software
The most effective optimization happens before the model ever reaches your code.
- Create a Collision Proxy: In your 3D modeling tool (like Blender), duplicate your high-resolution visual mesh. You will use this duplicate strictly for collision calculations.
- Apply Decimation: Use a “Decimate” or “ProOptimizer” modifier to drastically reduce the vertex count. For a convex hull, you rarely need more than 32 to 64 vertices to approximate the shape.
- Remove Internal Geometry: Delete any internal faces, overlapping geometry, or fine details that do not contribute to the outer silhouette of the object.
- Export Separately: Export this low-poly collision
mesh alongside your high-poly visual mesh, or pack them into the same
GLTF file with distinct naming conventions (e.g.,
mesh_visualandmesh_collision).
Step 2: Clean and Filter Vertices in JavaScript
If you cannot preprocess the mesh in 3D software, you must optimize the vertex data programmatically upon import (for example, using Three.js).
- Extract Position Attributes: Access the buffer geometry’s position attribute.
- Skip Duplicate Vertices: Do not pass duplicated vertices from indexed geometries. Create a map of unique vector coordinates to filter out redundant points.
- Stride the Array: If the vertex count is still too high, downsample the data by skipping vertices (e.g., taking every 3rd or 5th vertex). While this is a lossy approach, it is a quick way to generate a lightweight hull dynamically.
Step 3: Efficiently Populate the btConvexHullShape
When passing the optimized vertices to ammo.js, do so using a single
instantiated btVector3 object to avoid garbage collection
overhead in the JavaScript loop.
const shape = new Ammo.btConvexHullShape();
const tempBtVector = new Ammo.btVector3();
const positions = geometry.attributes.position.array;
const uniquePoints = getUniqueVertices(positions); // Custom helper to filter duplicates
for (let i = 0; i < uniquePoints.length; i += 3) {
tempBtVector.setValue(uniquePoints[i], uniquePoints[i+1], uniquePoints[i+2]);
shape.addPoint(tempBtVector, i === uniquePoints.length - 3); // Optimize on the last point
}
Ammo.destroy(tempBtVector);Setting the second argument of addPoint to
true on the final iteration tells ammo.js to recalculate
and finalize the hull representation only once, rather than
recalculating it on every single added point.
Step 4: Handle Concave Meshes with Decomposition
A single convex hull cannot represent hollows, indents, or concave features. If your complex mesh is concave (like a bowl or a hollow tube), do not use a single convex hull.
Instead, use Approximate Convex Decomposition
(V-HACD) to split the complex mesh into a collection of
smaller, simplified convex pieces. In your code, generate a
btConvexHullShape for each decomposed piece and add them to
a parent btCompoundShape. This preserves the concave
physics behavior while keeping the CPU calculation costs minimal.