Skip to main content

Overview

IndexBuffer contains vertex indices into a VertexBuffer. Indices can be 16-bit or 32-bit integers. The buffer itself is a GPU resource, therefore mutating the data can be relatively slow. Typically these buffers are constant.
It is possible, and even encouraged, to use a single index buffer for several Renderables.

IndexType Enum

enum class IndexType : uint8_t {
    USHORT,  // 16-bit indices
    UINT     // 32-bit indices
}
Specifies the data type of indices in the buffer.
  • USHORT - 16-bit unsigned integer indices (maximum 65,535 vertices)
  • UINT - 32-bit unsigned integer indices (billions of vertices)

Builder

The Builder class is used to construct IndexBuffer objects.

Constructor

Builder() noexcept
Creates a new IndexBuffer::Builder instance.

Configuration Methods

indexCount

Builder& indexCount(uint32_t indexCount) noexcept
Sets the size of the index buffer in elements.
indexCount
uint32_t
required
Number of indices the IndexBuffer can hold
Returns: Reference to this Builder for chaining calls. Example:
// For a mesh with 100 triangles
Builder().indexCount(300)  // 100 triangles * 3 indices each

bufferType

Builder& bufferType(IndexType indexType) noexcept
Sets the type of the index buffer, 16-bit or 32-bit.
indexType
IndexType
required
Type of indices stored in the IndexBuffer
Returns: Reference to this Builder for chaining calls.
Use USHORT (16-bit) when your vertex count is less than 65,536. Use UINT (32-bit) for larger meshes.
Example:
Builder()
    .indexCount(300)
    .bufferType(IndexBuffer::IndexType::USHORT)

Asynchronous Creation

async

Builder& async(
    backend::CallbackHandler* handler,
    AsyncCompletionCallback callback = nullptr,
    void* user = nullptr
) noexcept
Specifies a callback that will execute once the resource’s data has been fully allocated within GPU memory.
handler
CallbackHandler*
Handler to dispatch the callback or nullptr for the default handler
callback
AsyncCompletionCallback
Function to be called upon completion of asynchronous creation
user
void*
Custom data passed as second argument to the callback
Returns: Reference to this Builder for chaining calls.
To use this method, the engine must be configured for asynchronous operation.

build

IndexBuffer* build(Engine& engine)
Creates the IndexBuffer object and returns a pointer to it.
engine
Engine&
required
Reference to the filament::Engine to associate this IndexBuffer with
Returns: Pointer to the newly created IndexBuffer object.
After creation, the index buffer is uninitialized. Use IndexBuffer::setBuffer() to initialize it.
Example:
IndexBuffer* ib = IndexBuffer::Builder()
    .indexCount(3)
    .bufferType(IndexBuffer::IndexType::USHORT)
    .build(*engine);

Instance Methods

setBuffer

void setBuffer(
    Engine& engine,
    BufferDescriptor&& buffer,
    uint32_t byteOffset = 0
)
Copy-initializes a region of this IndexBuffer from the data provided.
engine
Engine&
required
Reference to the filament::Engine this IndexBuffer is associated with
buffer
BufferDescriptor&&
required
BufferDescriptor containing the index data. Data will be interpreted as either 16-bit or 32-bit indices based on the buffer’s IndexType
byteOffset
uint32_t
Offset in bytes into the IndexBuffer (must be multiple of 4, default is 0)
Example with 16-bit indices:
uint16_t indices[] = {0, 1, 2};

ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        indices,
        sizeof(indices),
        nullptr  // no callback
    )
);
Example with 32-bit indices:
uint32_t indices[] = {0, 1, 2, 2, 3, 0};

ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        indices,
        sizeof(indices),
        nullptr
    )
);

setBufferAsync

AsyncCallId setBufferAsync(
    Engine& engine,
    BufferDescriptor&& buffer,
    uint32_t byteOffset,
    backend::CallbackHandler* handler,
    AsyncCompletionCallback callback,
    void* user = nullptr
)
Asynchronous version of setBuffer().
engine
Engine&
required
Reference to the filament::Engine
buffer
BufferDescriptor&&
required
BufferDescriptor containing the index data
byteOffset
uint32_t
required
Offset in bytes into the IndexBuffer (must be multiple of 4)
handler
CallbackHandler*
Handler to dispatch the callback or nullptr for the default handler
callback
AsyncCompletionCallback
required
Function to be called upon completion
user
void*
Custom data passed to the callback
Returns: AsyncCallId that can be used to cancel the operation via Engine::cancelAsyncCall().
To use this method, the engine must be configured for asynchronous operation.

getIndexCount

size_t getIndexCount() const noexcept
Returns the size of this IndexBuffer in elements. Returns: The number of indices the IndexBuffer holds. Example:
size_t count = ib->getIndexCount();
std::cout << "Index buffer contains " << count << " indices" << std::endl;

isCreationComplete

bool isCreationComplete() const noexcept
Checks if the resource has finished creation. Returns: true if the resource is fully created and ready for use.
For resources created asynchronously, this returns true only after all related asynchronous tasks are complete. For normally created resources, this always returns true.

Buffer Descriptor

using BufferDescriptor = backend::BufferDescriptor;
Describes a buffer and provides a callback for when the data is consumed. Constructor:
BufferDescriptor(
    void const* buffer,
    size_t size,
    Callback callback = nullptr,
    void* user = nullptr
)
buffer
void const*
required
Pointer to the index data
size
size_t
required
Size of the buffer in bytes
callback
Callback
Optional callback invoked when the data has been consumed by the GPU
user
void*
Optional user pointer passed to the callback

Example Usage

Simple Triangle (16-bit indices)

#include <filament/Engine.h>
#include <filament/IndexBuffer.h>
#include <filament/VertexBuffer.h>
#include <filament/RenderableManager.h>

// Define triangle indices (counter-clockwise winding)
uint16_t indices[] = {0, 1, 2};

// Create index buffer
IndexBuffer* ib = IndexBuffer::Builder()
    .indexCount(3)
    .bufferType(IndexBuffer::IndexType::USHORT)
    .build(*engine);

// Upload index data
ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        indices,
        sizeof(indices),
        nullptr
    )
);

// Use with renderable
RenderableManager::Builder(1)
    .boundingBox({{ -1, -1, -1 }, { 1, 1, 1 }})
    .geometry(0, RenderableManager::PrimitiveType::TRIANGLES,
              vertexBuffer, ib, 0, 3)
    .build(*engine, entity);

// Cleanup
engine->destroy(ib);

Quad with Triangle Strip (16-bit indices)

// Triangle strip: 0-1-2, 1-3-2
uint16_t indices[] = {0, 1, 2, 3};

IndexBuffer* ib = IndexBuffer::Builder()
    .indexCount(4)
    .bufferType(IndexBuffer::IndexType::USHORT)
    .build(*engine);

ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(indices, sizeof(indices), nullptr)
);

RenderableManager::Builder(1)
    .boundingBox({{ -1, -1, 0 }, { 1, 1, 0 }})
    .geometry(0, RenderableManager::PrimitiveType::TRIANGLE_STRIP,
              vertexBuffer, ib, 0, 4)
    .build(*engine, entity);

Cube (16-bit indices)

// Cube with 6 faces, 2 triangles per face, 3 indices per triangle
uint16_t cubeIndices[] = {
    // Front face
    0, 1, 2,  2, 3, 0,
    // Back face
    4, 5, 6,  6, 7, 4,
    // Top face
    8, 9, 10,  10, 11, 8,
    // Bottom face
    12, 13, 14,  14, 15, 12,
    // Right face
    16, 17, 18,  18, 19, 16,
    // Left face
    20, 21, 22,  22, 23, 20
};

IndexBuffer* cubeIB = IndexBuffer::Builder()
    .indexCount(36)  // 6 faces * 2 triangles * 3 indices
    .bufferType(IndexBuffer::IndexType::USHORT)
    .build(*engine);

cubeIB->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        cubeIndices,
        sizeof(cubeIndices),
        nullptr
    )
);

Large Mesh (32-bit indices)

// For meshes with more than 65,535 vertices
std::vector<uint32_t> indices;
// ... generate or load indices ...

IndexBuffer* largeIB = IndexBuffer::Builder()
    .indexCount(indices.size())
    .bufferType(IndexBuffer::IndexType::UINT)  // 32-bit
    .build(*engine);

largeIB->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        indices.data(),
        indices.size() * sizeof(uint32_t),
        nullptr
    )
);

Dynamic Index Buffer with Callback

void onIndexBufferConsumed(void* buffer, size_t size, void* user) {
    // Buffer has been consumed by GPU, safe to free
    delete[] static_cast<uint16_t*>(buffer);
    std::cout << "Index buffer freed" << std::endl;
}

// Allocate dynamic buffer
uint16_t* dynamicIndices = new uint16_t[indexCount];
// ... fill dynamicIndices ...

ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        dynamicIndices,
        indexCount * sizeof(uint16_t),
        onIndexBufferConsumed,  // Callback to free memory
        nullptr                 // User data
    )
);

Partial Update

// Initial indices
uint16_t initialIndices[] = {0, 1, 2, 3, 4, 5};

IndexBuffer* ib = IndexBuffer::Builder()
    .indexCount(6)
    .bufferType(IndexBuffer::IndexType::USHORT)
    .build(*engine);

ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        initialIndices, sizeof(initialIndices), nullptr
    )
);

// Later, update only the last 2 indices
uint16_t partialIndices[] = {4, 5};

ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        partialIndices, sizeof(partialIndices), nullptr
    ),
    4 * sizeof(uint16_t)  // Offset to index 4
);

Sharing Index Buffer Across Renderables

// Create one index buffer
IndexBuffer* sharedIB = IndexBuffer::Builder()
    .indexCount(36)
    .bufferType(IndexBuffer::IndexType::USHORT)
    .build(*engine);

sharedIB->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(cubeIndices, sizeof(cubeIndices), nullptr)
);

// Use same index buffer for multiple renderables
RenderableManager::Builder(1)
    .geometry(0, RenderableManager::PrimitiveType::TRIANGLES,
              vertexBuffer1, sharedIB, 0, 36)
    .build(*engine, entity1);

RenderableManager::Builder(1)
    .geometry(0, RenderableManager::PrimitiveType::TRIANGLES,
              vertexBuffer2, sharedIB, 0, 36)
    .build(*engine, entity2);

// Both renderables share the same index buffer

Best Practices

Choosing Index Type

// Use 16-bit indices when possible (more efficient)
if (vertexCount < 65536) {
    builder.bufferType(IndexBuffer::IndexType::USHORT);
} else {
    builder.bufferType(IndexBuffer::IndexType::UINT);
}

Index Winding Order

Filament uses counter-clockwise (CCW) winding order by default for front-facing triangles. Ensure your indices follow this convention unless you’ve changed the material’s culling mode.
// Counter-clockwise triangle (front-facing)
uint16_t ccwTriangle[] = {0, 1, 2};

// Clockwise triangle (back-facing, will be culled by default)
uint16_t cwTriangle[] = {0, 2, 1};

Memory Management

// Stack-allocated data (no callback needed)
uint16_t stackIndices[] = {0, 1, 2};
ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(stackIndices, sizeof(stackIndices), nullptr)
);

// Heap-allocated data (use callback to free)
uint16_t* heapIndices = new uint16_t[count];
ib->setBuffer(*engine,
    IndexBuffer::BufferDescriptor(
        heapIndices,
        count * sizeof(uint16_t),
        [](void* buf, size_t, void*) { delete[] static_cast<uint16_t*>(buf); },
        nullptr
    )
);

Build docs developers (and LLMs) love