Skip to content

Batching

Heiko Brumme edited this page Jan 3, 2018 · 5 revisions

In the last few chapters we have always made static scenes, but in a real application that won't be sufficient. We need to be able to make dynamic scenes. With batch rendering we can reduce state changes and collect the data before uploading it to the GPU.

Initializing a batch

Before we initialize a batch we should take a look what we need for batch rendering.
By now it should be clear that we need a VAO, a VBO, a vertex shader, a fragment shader and also a shader program. For batch rendering we need a FloatBuffer, the count of the vertices and a boolean for telling if the batch is drawing right now.

private FloatBuffer vertices;
private int numVertices;
private boolean drawing;

Now we can initialize the shader program almost like in the previous tutorials. First we create and bind the VAO, after that we create the VBO, but this time there is something different. First we create the VBO and bind it, then we create our FloatBuffer, it will have the maximal size of the batch. The optimal size for a batch is between 1-4 MB. But in this tutorial we use a batch with about 16 KB, that are 4096 floats. This time we will use LWJGL's MemoryUtil because we want to use the buffer as long as our application is running. When disposing the buffer you have to call MemoryUtil.memFree(buffer).

/* Generate Vertex Buffer Object */
vbo = new VertexBufferObject();
vbo.bind(GL_ARRAY_BUFFER);

/* Create FloatBuffer */
vertices = MemoryUtil.memAllocFloat(4096);

Normally we would now fill the FloatBuffer with our vertex data, but since we want to fill it dynamically we will do glBufferData with no data at all, but we tell OpenGL the size of data, so that the storage for our VBO gets allocated. We also use a different usage value, instead of GL_STATIC_DRAW we will use GL_DYNAMIC_DRAW.

/* Upload null data to allocate storage for the VBO */
long size = vertices.capacity() * Float.BYTES;
glBufferData(GL_ARRAY_BUFFER, size, GL_DYNAMIC_DRAW);

Now is a good time to intodruce the different usage hints for a buffer. The first part (called frequency of access) can be one of these:

  • STREAM: The data store contents will be modified once and used at most a few times.
  • DYNAMIC: The data store contents will be modified repeatedly and used many times.
  • STATIC: The data store contents will be modified once and used many times.

And the second part (called nature of access) can be one of these:

  • DRAW: The data store contents are modified by the application, and used as the source for GL drawing and image specification commands.
  • READ: The data store contents are modified by reading data from the GL, and used to return that data when queried by the application.
  • COPY: The data store contents are modified by reading data from the GL, and used as the source for GL drawing and image specification commands.

Now we just set numVertices = 0 and drawing = false and we go on with initializing the shaders and the shader program.

Drawing with a batch

We already saw how to do that in the font rendering tutorial, we are starting the batch with begin() and ending it with end().
So for drawing the code will look something like this:

texture.bind();
batch.begin();
drawObjects();
batch.end();

Binding the texture should be clear by now, and begin() is also straight-forward:

public void begin() {
    numVertices = 0;
    drawing = true;
}

When ending we flush all the data to the GPU and end our drawing.

public void end() {
    flush();
    drawing = false;
}

We will take a look into the flush() method in a few moments, first let us see how to fill the buffer with data. You may remember drawTextureRegion(texture, drawX, drawY, g.x, g.y, g.width, g.height, c) from the font rendering tutorial, but this time we will see what that method really does.

public void drawTextureRegion(Texture texture, float x, float y, float regX, float regY, float regWidth, float regHeight, Color c) {
    /* Calculate Vertex positions */
    float x1 = x;
    float y1 = y;
    float x2 = x + regWidth;
    float y2 = y + regHeight;

    /* Calculate Texture coordinates */
    float s1 = regX / texture.getWidth();
    float t1 = regY / texture.getHeight();
    float s2 = (regX + regWidth) / texture.getWidth();
    float t2 = (regY + regHeight) / texture.getHeight();

    /* Get colors */
    float r = c.getRed();
    float g = c.getGreen();
    float b = c.getBlue();

    /* Put data into buffer */
    vertices.put(x1).put(y1).put(r).put(g).put(b).put(s1).put(t1);
    vertices.put(x1).put(y2).put(r).put(g).put(b).put(s1).put(t2);
    vertices.put(x2).put(y2).put(r).put(g).put(b).put(s2).put(t2);

    vertices.put(x1).put(y1).put(r).put(g).put(b).put(s1).put(t1);
    vertices.put(x2).put(y2).put(r).put(g).put(b).put(s2).put(t2);
    vertices.put(x2).put(y1).put(r).put(g).put(b).put(s2).put(t1);

    /* We drawed 6 vertices */
    numVertices += 6;
}

That code should be self-evident, it is important to increment numVertices at the end of each draw method, we need that value when flushing.

When flushing we should first check if there are any vertices in the buffers, there is no point in drawing nothing.
After that we know that there are vertices inside the buffer, so we flip() it, then we are binding our VAO (or we bind the VBO and specify the vertex attributes if using OpenGL 2.1) and start using our shader program.

vertices.flip();
vao.bind();
program.use();

Now comes the part where we are uploading the data to the GPU, normally you would use glBufferData, but since we already have allocated GPU memory we use glBufferSubData for uploading.

glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, vertices);

This will put the batched data on the GPU and we can draw it with glDrawArrays.

glDrawArrays(GL_TRIANGLES, 0, numVertices);

The last thing we should do after flushing is clearing the buffer and setting the number of vertices to 0.

vertices.clear();
numVertices = 0;

Buffer mapping

With small data chunks it is okay to use glBufferSubData, but when uploading more than 1 MB of data you should use another method to bring the data on the GPU. To improve performance you could use buffer mapping, for this you just have to rewrite begin() and end(). You also have to use a ByteBuffer instead of a FloatBuffer.
Well of course you could also make use of asFloatBuffer() if you really want to use a FloatBuffer.

public void begin() {
    vbo.bind(GL_ARRAY_BUFFER);
    vertices = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY, vertices.capacity(), vertices);

    numVertices = 0;
    drawing = true;
}

public void end() {
    glUnmapBuffer(GL_ARRAY_BUFFER);

    flush();
    drawing = false;
}

The call to glMapBuffer will get you a pointer to the buffer's data storage, so that you can directly write to it, instead of GL_WRITE_ONLY you can also use GL_READ_ONLY or GL_READ_WRITE.
Before using the buffer you should unmap it with glUnmapBuffer. After that you don't have to upload the data to the GPU, the data is already there, so you can draw it directly.

Next Steps

Well, that's all for this short tutorial. In the next part we will take a look how to handle input.


Source

References

Clone this wiki locally