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.

  1. 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.
  2. 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.
  3. Remove Internal Geometry: Delete any internal faces, overlapping geometry, or fine details that do not contribute to the outer silhouette of the object.
  4. 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_visual and mesh_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).

  1. Extract Position Attributes: Access the buffer geometry’s position attribute.
  2. Skip Duplicate Vertices: Do not pass duplicated vertices from indexed geometries. Create a map of unique vector coordinates to filter out redundant points.
  3. 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.