Hello, and welcome to Greg’s Journal!

In this post, we will talk about storing and rendering geometry data in Blaster.

To draw something with OpenGL, we need to store somehow and represent that. To avoid pushing data all the time through the bus, vertices, and indices stored on the GPU side in high-performance graphics memory. There is an opposite approach – legacy OpenGL Immediate Mode [1]. We will have a look at it at some point as well.

To simplify the handling of data needed for rendering passes, I implemented several useful classes – this chapter dedicated to them.

In this post

Array and Element Array Buffers

Everything starts with OpenGL buffers. The ones, which used most frequently are the Array Buffer and the Element Array Buffer. It sounds a bit mouthful, but we have what we have.

The first one intended for storing vertex information – positions, colors, normals, etc. The second allows us to store indices – a way to describe how vertices relate to each other.

From the standpoint of API usage, they are very similar – hence I am using a single class to represent both:

common_gl/com.blaster.gl.GlBuffer

class GlBuffer(
        private val target: Int,
        private val buffer: ByteBuffer,
        private val usage: Int = backend.GL_STATIC_DRAW) : GlBindable

As with most of OpenGL entities, their lifecycles controlled through the handle. We can create and delete those handles. When the handle obtained, we can perform operations on buffer:

common_gl/com.blaster.gl.GlBuffer::handle

private val handle: Int = glCheck { backend.glGenBuffers() }

After the buffer is “registered” on the GPU side, we can push data into it:

common_gl/com.blaster.gl.GlBuffer::uploadBuffer

private fun uploadBuffer() {
    glCheck {
        backend.glBindBuffer(target, handle)
        backend.glBufferData(target, buffer, usage)
        backend.glBindBuffer(target, 0)
    }
}

The buffer also can be “mapped” onto the client-side application memory. That is a useful feature for updating the buffer:

common_gl/com.blaster.gl.GlBuffer::mapBuffer

private fun mapBuffer(access: Int): ByteBuffer = glCheck {  backend.glMapBuffer(target, access, buffer) }

common_gl/com.blaster.gl.GlBuffer::updateBuffer

fun updateBuffer(access : Int = backend.GL_WRITE_ONLY, update: (mapped: ByteBuffer) -> Unit) {
    val mapped = mapBuffer(access)
    update.invoke(mapped)
    unmapBuffer()
}

One example of usage is rendering large amounts of billboards. In that case, I am updating buffer with pre-calculated information about instances positions:

common_gl/com.blaster.techniques.BillboardsTechnique::updatePositions

Mapping the buffer with positions, updating the buffer from provider

private fun updatePositions(provider: BillboardsProvider) {
    glBind(positions) {
        positions.updateBuffer {
            provider.flushPositions(it.asFloatBuffer())
        }
    }
}

Now, let us have a look at how buffers combined into a mesh.

Polygon meshes

To represent a collection of vertices, edges, and faces, I am using a class called GlMesh. In short, GlMesh is an association of multiple Array Buffers, which holds vertex data, and one Element Array Buffer with indices to define the edges between the vertices:

common_gl/com.blaster.gl.GlMesh

class GlMesh(
        private val attributes: List<Pair<GlAttribute, GlBuffer>>,
        private val indicesBuffer: GlBuffer,
        private val indicesCount: Int) : GlBindable

A set of attributes defines each vertex in the mesh. I am using a predefined number of attribute locations in code:

common_gl/com.blaster.gl.GlAttribute

divisor – how many attributes per draw call for instancing (0 – default == 1 item per vertex)

enum class GlAttribute(val size: Int, val location: Int, val divisor: Int = 0) {
    ATTRIBUTE_POSITION  (3, 0),
    ATTRIBUTE_TEXCOORD  (2, 1),
    ATTRIBUTE_NORMAL    (3, 2),
    ATTRIBUTE_COLOR     (3, 3),

    ATTRIBUTE_BILLBOARD_POSITION    (3, 4, 1),
    ATTRIBUTE_BILLBOARD_SCALE       (1, 5, 1),
    ATTRIBUTE_BILLBOARD_TRANSPARENCY(1, 6, 1);
}

For example in this shader we have positions and texture coordinates defined per-vertex:

common_assets/src/main/resources/shaders/simple/no_lighting.vert

#version 300 es

precision mediump float;

layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec2 aTexCoord;

uniform mat4 uModelM;
uniform mat4 uProjectionM;
uniform mat4 uViewM;

out vec2 vTexCoord;

void main() {
    vTexCoord = aTexCoord;
    mat4 mvp =  uProjectionM * uViewM * uModelM;
    gl_Position = mvp * vec4(aPosition, 1.0);
}

To render the mesh, we need to tell the rendering pipeline the exact association between buffers with data and vertex attributes:

common_gl/com.blaster.gl.GlMesh::bindVertexPointers

private fun bindVertexPointers() {
    attributes.forEach {
        glCheck {
            backend.glEnableVertexAttribArray(it.first.location)
            it.second.bind()
            backend.glVertexAttribPointer(it.first.location, it.first.size, backend.GL_FLOAT, false, 0, 0)
            if (it.first.divisor != 0) {
                backend.glVertexAttribDivisor(it.first.location, it.first.divisor)
            }
        }
    }
    indicesBuffer.bind()
}

Next, I will show how to store preconfigured bindings on the GPU side.

Vertex Array Objects

Specifying attributes for each mesh each rendering pass can be costly, but there is a way to avoid that penalty. With Vertex Array Object I can predefine the bindings beforehand:

common_gl/com.blaster.gl.GlMesh::createVAO

private fun createVAO() {
    handle = glCheck { backend.glGenVertexArrays() }
    check(handle!! > 0)
    glBind(this) { bindVertexPointers() }
}

Vertex Array Object (or VAO) will hold all the necessary bindings on the GPU side after we specify it once. Next time we need to define the mesh – we can bind the VAO, and it will restore all bindings.

Let us see, how geometry can be rendered with my code.

Rendering the mesh

After all of the bindings are established, rendering of the mesh is a straightforward task. We only need to pass which type of geometry we want to output – in most cases, GL_TRIANGLES.

common_gl/com.blaster.gl.GlMesh::draw

fun draw(mode: Int = backend.GL_TRIANGLES) {
    glCheck { backend.glDrawElements(mode, indicesCount, backend.GL_UNSIGNED_INT, 0) }
}

There is one more call for instanced draws – this one accepts multiple instances to render:

common_gl/com.blaster.gl.GlMesh::drawInstanced

fun drawInstanced(mode: Int = backend.GL_TRIANGLES, instances: Int) {
    glCheck { backend.glDrawElementsInstanced(mode, indicesCount, backend.GL_UNSIGNED_INT, 0, instances) }
}

“Teapots in Space.” Author unknown.

Teapots in Space

Procedural example

Quite frequently there is a need for simple procedurally defined meshes: quads, triangles:

common_gl/com.blaster.gl.GlMesh::rect

fun rect(additionalAttributes: List<Pair<GlAttribute, GlBuffer>> = listOf()): GlMesh {
    val vertices = floatArrayOf(
            -1f,  1f, 0f,
            -1f, -1f, 0f,
             1f,  1f, 0f,
             1f, -1f, 0f)
    val texCoords = floatArrayOf(
            0f, 1f,
            0f, 0f,
            1f, 1f,
            1f, 0f)
    val indices = intArrayOf(0, 1, 2, 1, 3, 2)
    val attributes = mutableListOf(
            GlAttribute.ATTRIBUTE_POSITION to
                    GlBuffer.create(backend.GL_ARRAY_BUFFER, vertices),
            GlAttribute.ATTRIBUTE_TEXCOORD to
                    GlBuffer.create(backend.GL_ARRAY_BUFFER, texCoords))
    attributes.addAll(additionalAttributes)
    return GlMesh(attributes,
            GlBuffer.create(backend.GL_ELEMENT_ARRAY_BUFFER, indices), indices.size)
}

common_gl/com.blaster.gl.GlMesh::triangle

fun triangle(additionalAttributes: List<Pair<GlAttribute, GlBuffer>> = listOf()): GlMesh {
    val vertices = floatArrayOf(
             0f,  1f, 0f,
            -1f, -1f, 0f,
             1f, -1f, 0f)
    val texCoords = floatArrayOf(
            0.5f, 1f,
            0f,   0f,
            1f,   0f)
    val indices = intArrayOf(0, 1, 2)
    val attributes = mutableListOf(
            GlAttribute.ATTRIBUTE_POSITION to
                    GlBuffer.create(backend.GL_ARRAY_BUFFER, vertices),
            GlAttribute.ATTRIBUTE_TEXCOORD to
                    GlBuffer.create(backend.GL_ARRAY_BUFFER, texCoords))
    attributes.addAll(additionalAttributes)
    return GlMesh(attributes,
            GlBuffer.create(backend.GL_ELEMENT_ARRAY_BUFFER, indices), indices.size)
}

That can serve as a good example of the classes mentioned above in a typical situation.

In this post, I outlined the primary classes, which simplify geometry operations in my code. I always prefer a little bit of abstraction over low-level native calls on the spot. It simplifies the understanding of the logic by breaking down the scene into familiar pieces.

That is all for today, folks, thank you for your time and hope to see you next time!)

References