More on Rendering

In this chapter we will continue talking about how OpenGL renders things. In order to tidy up our code a little bit let’s create a new class called Mesh which, taking as an input an array of positions, creates the VBO and VAO objects needed to load that model into the graphics card.

package org.lwjglb.engine.graph;

import java.nio.FloatBuffer;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;
import org.lwjgl.system.MemoryUtil;

public class Mesh {

    private final int vaoId;

    private final int vboId;

    private final int vertexCount;

    public Mesh(float[] positions) {
        FloatBuffer verticesBuffer = null;
        try {
            verticesBuffer = MemoryUtil.memAllocFloat(positions.length);
            vertexCount = positions.length / 3;
            verticesBuffer.put(positions).flip();

            vaoId = glGenVertexArrays();
            glBindVertexArray(vaoId);

            vboId = glGenBuffers();
            glBindBuffer(GL_ARRAY_BUFFER, vboId);
            glBufferData(GL_ARRAY_BUFFER, verticesBuffer, GL_STATIC_DRAW);            
            glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
            glBindBuffer(GL_ARRAY_BUFFER, 0);

            glBindVertexArray(0);         
        } finally {
            if (verticesBuffer  != null) {
                MemoryUtil.memFree(verticesBuffer);
            }
        }
    }

    public int getVaoId() {
        return vaoId;
    }

    public int getVertexCount() {
        return vertexCount;
    }

    public void cleanUp() {
        glDisableVertexAttribArray(0);

        // Delete the VBO
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glDeleteBuffers(vboId);

        // Delete the VAO
        glBindVertexArray(0);
        glDeleteVertexArrays(vaoId);
    }
}

We will create our Mesh instance in our DummyGame class, removing the VAO and VBO code from Renderer init method. Our render method in the Renderer class will accept also a Mesh instance to render. The cleanup method will also be simplified since the Mesh class already provides one for freeing VAO and VBO resources.

public void render(Mesh mesh) {
    clear();

    if ( window.isResized() ) {
        glViewport(0, 0, window.getWidth(), window.getHeight());
        window.setResized(false);
    }

    shaderProgram.bind();

    // Draw the mesh
    glBindVertexArray(mesh.getVaoId());
    glEnableVertexAttribArray(0);
    glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());

    // Restore state
    glDisableVertexAttribArray(0);
    glBindVertexArray(0);

    shaderProgram.unbind();
}

public void cleanup() {
    if (shaderProgram != null) {
        shaderProgram.cleanup();
    }
}

One important thing to note is this line:

glDrawArrays(GL_TRIANGLES, 0, mesh.getVertexCount());

Our Mesh counts the number of vertices by dividing the position array by 3 (since we are passing X, Y and Z coordinates)). Now that we can render more complex shapes, let us try to render a more complex shape. Let us render a quad. A quad can be constructed by using two triangles as shown in the next figure.

Quad coordinates

As you can see each of the two triangles is composed of three vertices. The first one formed by the vertices V1, V2 and V4 (the orange one) and the second one formed by the vertices V4, V2 and V3 (the green one). Vertices are specified in a counter-clockwise order, so the float array to be passed will be [V1, V2, V4, V4, V2, V3]. Thus, the init method in our DummyGame class will be:

@Override
public void init() throws Exception {
    renderer.init();
    float[] positions = new float[]{
        -0.5f,  0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
         0.5f,  0.5f, 0.0f,
         0.5f,  0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
    };
    mesh = new Mesh(positions);
}

Now you should see a quad rendered like this:

Quad rendered

Are we done yet? Unfortunately not. The code above still presents some issues. We are repeating coordinates to represent the quad. We are passing twice V2 and V4 coordinates. With this small shape it may not seem a big deal, but imagine a much more complex 3D model. We would be repeating the coordinates many times. Keep in mind also that now we are just using three floats for representing the position of a vertex. But later on we will need more data to represent the texture, etc. Also take into consideration that in more complex shapes the number of vertices shared between triangles can be even higher like in the figure below (where a vertex can be shared between six triangles).

Dolphin

At the end we would need much more memory because of that duplicate information and this is where Index Buffers come to the rescue. For drawing the quad we only need to specify each vertex once this way: V1, V2, V3, V4). Each vertex has a position in the array. V1 has position 0, V2 has position 1, etc:

V1 V2 V3 V4
0 1 2 3

Then we specify the order in which those vertices should be drawn by referring to their position:

0 1 3 3 1 2
V1 V2 V4 V4 V2 V3

So we need to modify our Mesh class to accept another parameter, an array of indices, and now the number of vertices to draw will be the length of that indices array.

public Mesh(float[] positions, int[] indices) {
    vertexCount = indices.length;

After we have created our VBO that stores the positions, we need to create another VBO which will hold the indices. So we rename the identifier that holds the identifier for the positions VBO and create a new one for the index VBO (idxVboId). The process of creating that VBO is similar but the type is now GL_ELEMENT_ARRAY_BUFFER.

idxVboId = glGenBuffers();
indicesBuffer = MemoryUtil.memAllocInt(indices.length);
indicesBuffer.put(indices).flip();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idxVboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
memFree(indicesBuffer);

Since we are dealing with integers we need to create an IntBuffer instead of a FloatBuffer.

And that’s it. The VAO will contain now two VBOs, one for positions and another one that will hold the indices and that will be used for rendering. Our cleanup method in our Mesh class must take into consideration that there is another VBO to free.

public void cleanUp() {
    glDisableVertexAttribArray(0);

    // Delete the VBOs
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glDeleteBuffers(posVboId);
    glDeleteBuffers(idxVboId);

    // Delete the VAO
    glBindVertexArray(0);
    glDeleteVertexArrays(vaoId);
}

Finally, we need to modify our drawing call that used the glDrawArrays method:

glDrawArrays(GL_TRIANGLES, 0, mesh. getVertexCount());

To another call that uses the method glDrawElements:

glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0);

The parameters of that method are:

  • mode: Specifies the primitives for rendering, triangles in this case. No changes here.
  • count: Specifies the number of elements to be rendered.
  • type: Specifies the type of value in the indices data. In this case we are using integers.
  • indices: Specifies the offset to apply to the indices data to start rendering.

And now we can use our newer and much more efficient method of drawing complex models by just specifying the indices.

public void init() throws Exception {
    renderer.init();
    float[] positions = new float[]{
        -0.5f,  0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.5f,  0.5f, 0.0f,
    };
    int[] indices = new int[]{
        0, 1, 3, 3, 1, 2,
    };
    mesh = new Mesh(positions, indices);
}

Now let’s add some colour to our example. We will pass another array of floats to our Mesh class which holds the colour for each coordinate in the quad.

public Mesh(float[] positions, float[] colours, int[] indices) {

With that array, we will create another VBO which will be associated to our VAO.

// Colour VBO
colourVboId = glGenBuffers();
FloatBuffer colourBuffer = memAllocFloat(colours.length);
colourBuffer.put(colours).flip();
glBindBuffer(GL_ARRAY_BUFFER, colourVboId);
glBufferData(GL_ARRAY_BUFFER, colourBuffer, GL_STATIC_DRAW);
memFree(colourBuffer);
glVertexAttribPointer(1, 3, GL_FLOAT, false, 0, 0);

Please notice that in the glVertexAttribPointer call, the first parameter is now a “1”. This is the location where our shader will be expecting that data. (Of course, since we have another VBO we need to free it in the cleanup method).

The next step is to modify the shaders. The vertex shader is now expecting two parameters, the coordinates (in location 0) and the colour (in location 1). The vertex shader will just output the received colour so it can be processed by the fragment shader.

#version 330

layout (location =0) in vec3 position;
layout (location =1) in vec3 inColour;

out vec3 exColour;

void main()
{
    gl_Position = vec4(position, 1.0);
      exColour = inColour;
}

And now our fragment shader receives as an input the colour processed by our vertex shader and uses it to generate the colour.

#version 330

in  vec3 exColour;
out vec4 fragColor;

void main()
{
    fragColor = vec4(exColour, 1.0);
}

The last important thing to do is to modify our rendering code to use that second array of data:

public void render(Window window, Mesh mesh) {
    clear();

    if ( window.isResized() ) {
        glViewport(0, 0, window.getWidth(), window.getHeight());
        window.setResized(false);
    }

    shaderProgram.bind();

    // Draw the mesh
    glBindVertexArray(mesh.getVaoId());
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glDrawElements(GL_TRIANGLES, mesh.getVertexCount(), GL_UNSIGNED_INT, 0);
    // ...

You can see that we need to enable the VAO attribute at position 1 to be used during rendering. We can now pass an array of colours like this to our Mesh class in order to add some colour to our quad.

float[] colours = new float[]{
    0.5f, 0.0f, 0.0f,
    0.0f, 0.5f, 0.0f,
    0.0f, 0.0f, 0.5f,
    0.0f, 0.5f, 0.5f,
};

And we will get a fancy coloured quad like this.

Coloured quad

results matching ""

    No results matching ""