Switch from Indexed to Non-Indexed in Three.js

This article explains how to dynamically convert an indexed BufferGeometry to a non-indexed one in Three.js by disposing of and removing its index buffer. You will learn how to restructure the vertex attributes in-place, release the GPU memory associated with the index, and notify the WebGL renderer to switch from indexed drawing to non-indexed drawing.

The Challenge of Dynamic Conversion

In Three.js, an indexed geometry uses an index buffer attribute to define the order in which vertices are connected to form faces. A non-indexed geometry, however, reads vertices sequentially.

Simply setting geometry.index = null is not enough to switch rendering modes. If you remove the index without updating the other vertex attributes (like position, normal, and uv), the geometry will render incorrectly because the vertices are still packed for indexed rendering. To successfully switch dynamically, you must duplicate the shared vertices and safely dispose of the index buffer from GPU memory.

Step-by-Step Implementation

To perform this switch dynamically on an existing geometry instance (to preserve material assignments and scene graph references), follow these steps:

1. Generate Non-Indexed Attributes

First, use the built-in toNonIndexed() helper method. This creates a temporary geometry with duplicated, sequential vertex attributes.

const tempGeometry = geometry.toNonIndexed();

2. Dispose of the Index Buffer

To free up WebGL resources and prevent memory leaks, you must explicitly dispose of the existing index attribute. Setting the index to null tells the Three.js WebGLRenderer to stop using drawElements and switch to drawArrays.

if (geometry.index !== null) {
    // Release WebGL buffer resource
    geometry.index.dispose(); 
    
    // Remove the index from the geometry
    geometry.setIndex(null); 
}

3. Replace the Vertex Attributes

Copy the expanded, sequential attributes from the temporary geometry to your original geometry. This ensures the vertices are now aligned for sequential drawing.

// Replace position attribute
geometry.setAttribute('position', tempGeometry.getAttribute('position'));

// Replace normal attribute if it exists
if (tempGeometry.hasAttribute('normal')) {
    geometry.setAttribute('normal', tempGeometry.getAttribute('normal'));
}

// Replace UV attribute if it exists
if (tempGeometry.hasAttribute('uv')) {
    geometry.setAttribute('uv', tempGeometry.getAttribute('uv'));
}

4. Clean Up and Flag Updates

Dispose of the temporary geometry to avoid memory leaks, and flag the original geometry’s attributes as needing an update so the GPU receives the new data.

// Dispose of the temporary geometry container
tempGeometry.dispose();

// Force Three.js to upload the new attributes to the GPU
geometry.attributes.position.needsUpdate = true;
if (geometry.attributes.normal) geometry.attributes.normal.needsUpdate = true;
if (geometry.attributes.uv) geometry.attributes.uv.needsUpdate = true;

Summary of Renderer Behavior

Once geometry.index is set to null, the WebGLRenderer automatically detects the change during the next render frame. It will rebind the updated vertex attributes and switch its internal draw call from gl.drawElements() (used for indexed geometries) to gl.drawArrays() (used for non-indexed geometries), completing the dynamic transition.