Skip to main content

Overview

Filament represents mesh geometry using VertexBuffer and IndexBuffer objects. Vertex buffers store per-vertex attributes like position, normals, and UVs, while index buffers define triangle connectivity.

VertexBuffer

Basic Setup

#include <filament/VertexBuffer.h>

struct Vertex {
    float3 position;
    float2 uv;
};

Vertex vertices[4] = {
    {{-1, -1, 0}, {0, 0}},
    {{ 1, -1, 0}, {1, 0}},
    {{ 1,  1, 0}, {1, 1}},
    {{-1,  1, 0}, {0, 1}}
};

VertexBuffer* vb = VertexBuffer::Builder()
    .vertexCount(4)
    .bufferCount(1)
    .attribute(VertexAttribute::POSITION, 0, 
        VertexBuffer::AttributeType::FLOAT3, 0, sizeof(Vertex))
    .attribute(VertexAttribute::UV0, 0,
        VertexBuffer::AttributeType::FLOAT2, 
        offsetof(Vertex, uv), sizeof(Vertex))
    .build(*engine);

vb->setBufferAt(*engine, 0,
    VertexBuffer::BufferDescriptor(vertices, sizeof(vertices)));

Interleaved vs Separate Buffers

Interleaved (Single Buffer)

struct Vertex {
    float3 position;
    short4 tangents;  // Packed quaternion
    float2 uv;
};

VertexBuffer* vb = VertexBuffer::Builder()
    .vertexCount(vertexCount)
    .bufferCount(1)
    .attribute(VertexAttribute::POSITION, 0,
        VertexBuffer::AttributeType::FLOAT3, 0, sizeof(Vertex))
    .attribute(VertexAttribute::TANGENTS, 0,
        VertexBuffer::AttributeType::SHORT4, 
        offsetof(Vertex, tangents), sizeof(Vertex))
    .normalized(VertexAttribute::TANGENTS)
    .attribute(VertexAttribute::UV0, 0,
        VertexBuffer::AttributeType::FLOAT2,
        offsetof(Vertex, uv), sizeof(Vertex))
    .build(*engine);

Separate Buffers

// Static geometry in buffer 0
float3 positions[count];

// Dynamic data in buffer 1  
float2 uvs[count];

VertexBuffer* vb = VertexBuffer::Builder()
    .vertexCount(count)
    .bufferCount(2)
    .attribute(VertexAttribute::POSITION, 0,
        VertexBuffer::AttributeType::FLOAT3)
    .attribute(VertexAttribute::UV0, 1,
        VertexBuffer::AttributeType::FLOAT2)
    .build(*engine);

vb->setBufferAt(*engine, 0,
    VertexBuffer::BufferDescriptor(positions, sizeof(positions)));
vb->setBufferAt(*engine, 1,
    VertexBuffer::BufferDescriptor(uvs, sizeof(uvs)));

Vertex Attributes

Standard Attributes

AttributeTypeDescription
POSITIONFLOAT3Vertex position
TANGENTSSHORT4 or FLOAT4Packed tangent frame (quaternion)
UV0, UV1FLOAT2Texture coordinates
COLORFLOAT4 or UBYTE4Vertex color
BONE_INDICESUSHORT4Skinning bone indices
BONE_WEIGHTSFLOAT4Skinning bone weights

Tangent Frame

Filament requires tangents as a packed quaternion:
// Pack tangent frame into quaternion
mat3f tangentFrame = mat3f{
    float3{1, 0, 0},  // tangent
    float3{0, 0, 1},  // bitangent
    float3{0, 1, 0}   // normal
};

short4 packedTangents = packSnorm16(
    mat3f::packTangentFrame(tangentFrame).xyzw);

// Use in vertex buffer
VertexBuffer* vb = VertexBuffer::Builder()
    .attribute(VertexAttribute::TANGENTS, 1,
        VertexBuffer::AttributeType::SHORT4)
    .normalized(VertexAttribute::TANGENTS)
    .build(*engine);

Normalized Attributes

Integer attributes can be automatically normalized to [0, 1]:
// Store UVs as 16-bit integers
ushort2 uvs[count];
// ... fill with values 0-65535 ...

VertexBuffer* vb = VertexBuffer::Builder()
    .attribute(VertexAttribute::UV0, 0,
        VertexBuffer::AttributeType::USHORT2)
    .normalized(VertexAttribute::UV0)  // Map to [0, 1]
    .build(*engine);

IndexBuffer

16-bit Indices

#include <filament/IndexBuffer.h>

uint16_t indices[] = {0, 1, 2, 2, 3, 0};

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

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

32-bit Indices

uint32_t indices[largeCount];

IndexBuffer* ib = IndexBuffer::Builder()
    .indexCount(largeCount)
    .bufferType(IndexBuffer::IndexType::UINT)
    .build(*engine);

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

Creating Renderables

Attach vertex and index buffers to entities:
#include <filament/RenderableManager.h>

Entity entity = entityManager.create();

RenderableManager::Builder(1)
    .boundingBox({{
        {-1, -1, -1}, {1, 1, 1}
    }})
    .material(0, materialInstance)
    .geometry(0, RenderableManager::PrimitiveType::TRIANGLES,
        vertexBuffer, indexBuffer, 0, indexCount)
    .culling(true)
    .receiveShadows(true)
    .castShadows(true)
    .build(*engine, entity);

scene->addEntity(entity);

Ground Plane Example

From the gltf_viewer sample:
static const uint32_t indices[] = {
    0, 1, 2, 2, 3, 0
};

static const float3 vertices[] = {
    {-10.0f, 0, -10.0f},
    {-10.0f, 0,  10.0f},
    { 10.0f, 0,  10.0f},
    { 10.0f, 0, -10.0f}
};

// Create tangent frame for ground plane
short4 tbn = packSnorm16(
    mat3f::packTangentFrame(
        mat3f{
            float3{1.0f, 0.0f, 0.0f},  // tangent
            float3{0.0f, 0.0f, 1.0f},  // bitangent  
            float3{0.0f, 1.0f, 0.0f}   // normal
        }
    ).xyzw);

static const short4 normals[] = {tbn, tbn, tbn, tbn};

VertexBuffer* vertexBuffer = VertexBuffer::Builder()
    .vertexCount(4)
    .bufferCount(2)
    .attribute(VertexAttribute::POSITION, 0,
        VertexBuffer::AttributeType::FLOAT3)
    .attribute(VertexAttribute::TANGENTS, 1,
        VertexBuffer::AttributeType::SHORT4)
    .normalized(VertexAttribute::TANGENTS)
    .build(*engine);

vertexBuffer->setBufferAt(*engine, 0,
    VertexBuffer::BufferDescriptor(vertices, sizeof(vertices)));
vertexBuffer->setBufferAt(*engine, 1,
    VertexBuffer::BufferDescriptor(normals, sizeof(normals)));

IndexBuffer* indexBuffer = IndexBuffer::Builder()
    .indexCount(6)
    .build(*engine);

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

BufferObject (Advanced)

Share buffer data between multiple VertexBuffer instances:
// Create shared buffer object
BufferObject* bufferObject = BufferObject::Builder()
    .size(bufferSize)
    .build(*engine);

bufferObject->setBuffer(*engine,
    BufferObject::BufferDescriptor(data, bufferSize));

// Enable buffer objects mode
VertexBuffer* vb = VertexBuffer::Builder()
    .vertexCount(count)
    .bufferCount(1)
    .enableBufferObjects(true)
    .attribute(VertexAttribute::POSITION, 0,
        VertexBuffer::AttributeType::FLOAT3)
    .build(*engine);

// Use buffer object instead of direct data
vb->setBufferObjectAt(*engine, 0, bufferObject);

Asynchronous Updates

Update buffers asynchronously:
auto callback = [](VertexBuffer* vb, void* user) {
    std::cout << "Buffer updated!" << std::endl;
};

vb->setBufferAtAsync(*engine, 0, 
    VertexBuffer::BufferDescriptor(newData, size),
    0, nullptr, callback, nullptr);

Mesh Formats from glTF

When loading glTF files, gltfio automatically handles:
  • Multiple UV sets (UV0, UV1)
  • Vertex colors
  • Skinning data (bones, weights)
  • Morph targets
  • Quantized attributes (KHR_mesh_quantization)
  • Draco compression (KHR_draco_mesh_compression, platform-dependent)

Best Practices

  1. Separate static and dynamic data into different buffers
  2. Use normalized short4 for tangents to save memory
  3. Prefer 16-bit indices when vertex count < 65536
  4. Set accurate bounding boxes for frustum culling
  5. Share BufferObject when the same geometry appears multiple times
  6. Use interleaved buffers for better cache coherency
  7. Batch geometry to reduce draw calls

See Also

Build docs developers (and LLMs) love