easy-gl › Instancing

Instancing

Draw thousands of objects in a single draw call by storing per-instance data in a buffer and using a per-instance vertex attribute divisor.

Shader

const char* vertSrc = R"(#version 330 core
layout (location = 0) in vec2 aPos;      // per-vertex
layout (location = 1) in vec3 aColor;    // per-vertex
layout (location = 2) in vec2 aOffset;   // per-instance
layout (location = 3) in float aScale;   // per-instance

out vec3 vColor;

void main() {
    gl_Position = vec4(aPos * aScale + aOffset, 0.0, 1.0);
    vColor = aColor;
})";

Per-instance data

struct Instance {
    float offsetX, offsetY;
    float scale;
};

std::vector<Instance> instances;
for (int i = 0; i < 1000; ++i) {
    instances.push_back({
        ((float)(rand() % 2000) - 1000.0f) / 1000.0f,
        ((float)(rand() % 2000) - 1000.0f) / 1000.0f,
        0.02f + (float)(rand() % 100) / 1000.0f
    });
}

Setup VAO with two VBOs

easygl::VertexArray vao;
easygl::Buffer      meshVbo, instanceVbo;
vao.create(); meshVbo.create(); instanceVbo.create();

vao.bind();

// Per-vertex attributes (attribute 0 and 1)
meshVbo.bind(easygl::BufferTarget::Array);
meshVbo.set_data(easygl::BufferTarget::Array, quadVerts, sizeof(quadVerts));
constexpr std::size_t meshStride = 5 * sizeof(float);
vao.set_attribute({0, 2, easygl::DataType::Float, false, meshStride, 0, true});
vao.set_attribute({1, 3, easygl::DataType::Float, false, meshStride, 2*sizeof(float), true});

// Per-instance attributes (attribute 2 and 3)
instanceVbo.bind(easygl::BufferTarget::Array);
instanceVbo.set_data(easygl::BufferTarget::Array,
                       instances.data(), instances.size() * sizeof(Instance),
                       easygl::BufferUsage::DynamicDraw);

constexpr std::size_t instStride = sizeof(Instance);

// offset: vec2 at byte 0
vao.set_attribute_pointer(2, 2, easygl::DataType::Float, false,
                            instStride, reinterpret_cast<const void*>(0));
vao.enable_attribute(2);
vao.set_attribute_divisor(2, 1); // advance once per instance

// scale: float at byte 8
vao.set_attribute_pointer(3, 1, easygl::DataType::Float, false,
                            instStride, reinterpret_cast<const void*>(2 * sizeof(float)));
vao.enable_attribute(3);
vao.set_attribute_divisor(3, 1);

Draw call

prog.use();
vao.bind();
device.draw_arrays_instanced(
    easygl::PrimitiveType::Triangles,
    0,                  // first vertex
    meshVertexCount,     // vertices per instance
    1000);               // instance count

Updating instances dynamically

// Move instances on CPU, then re-upload the relevant range
for (auto& inst : instances) { inst.offsetX += speed * dt; }

instanceVbo.bind(easygl::BufferTarget::Array);
instanceVbo.set_sub_data(easygl::BufferTarget::Array,
                           instances.data(),
                           instances.size() * sizeof(Instance),
                           0);

Key points