The Quad with DrawElements

From LWJGL
Jump to: navigation, search

Introduction

In a previous tutorial we’ve used “glDrawArrays” to draw a quad on the screen. The downside with DrawArrays is that you have to specify each vertex of a triangle in your model. A quad may have 4 corners but when a quad is divided in 2 triangles it has 6! With “glDrawElements” we can remove those duplicate vertex definitions. In return we’ll have to tell OpenGL which vertex to use for each triangle by using indices. Let’s see how that works.

Do you remember the following…

We are going to use the “DrawArrays” [1] tutorial as a base for this tutorial. This means that you already know a bit about VAO and VBO’s and how they are set up. You also know that a VAO has multiple attribute lists (16 by default) and that we can link a VBO to one of these lists. You know that a VBO actually contains data. And lastly you also know that you have to bind/unbind the objects (for example a VBO is an object: vertex buffer object) when working with them.

Defining our vertices

Let’s define our new vertex array so it only contains the 4 corners of our quad. The order in which we define our vertices does not matter, we’ll tell OpenGL what the actual order is by using a different object.

// Vertices, the order is not important.
float[] vertices = {
		-0.5f, 0.5f, 0f,	// Left top			ID: 0
		-0.5f, -0.5f, 0f,	// Left bottom		ID: 1
		0.5f, -0.5f, 0f,	// Right bottom		ID: 2
		0.5f, 0.5f, 0f	// Right left		ID: 3
};
// Sending data to OpenGL requires the usage of (flipped) byte buffers
FloatBuffer verticesBuffer = BufferUtils.createFloatBuffer(vertices.length);
verticesBuffer.put(vertices);
verticesBuffer.flip();

As you can see, we only define the 4 vertices. One for each corner of our quad. An array counts its elements beginning with 0 and for each vertex there are 3 values. So each triplet of values is one position and in our mind we can give them an index (see code comment). When we would give these items to OpenGL it wouldn’t know what to do with it. For this reason we have to give OpenGL a guide on how to use these vertices to make the 2 triangles. For this we need another VBO.

Adding indices

The quad is still made up of 2 triangles, in a new VBO we’ll define the order of the previously defined vertices by using indices. Let’s define them now:

// OpenGL expects to draw vertices in counter clockwise order by default
byte[] indices = {
		// Left bottom triangle
		0, 1, 2,
		// Right top triangle
		2, 3, 0
};
indicesCount = indices.length;
ByteBuffer indicesBuffer = BufferUtils.createByteBuffer(indicesCount);
indicesBuffer.put(indices);
indicesBuffer.flip();

As you can see we’ve used the id’s of our vertices to define 2 triangles. The point is that we’ve only defined 4 vertices and use 2 definitions twice. Doing it this way requires less memory, going from 6 to 4 but adding the indices VBO might not look like an improvement but usually a “real” model isn’t made up of 1 quad.

The VBO we have to create is also of a different type, as it’s no longer a generic array buffer (GL_ARRAY_BUFFER) instead now we’re creating an element array (GL_ELEMENT_ARRAY_BUFFER). Again we need to ask for the memory id, bind, and supply the data to the buffer (let’s not forget to unbind it).

// Create a new VBO for the indices and select it (bind)
vboiId = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL15.GL_STATIC_DRAW);
// Deselect (bind to 0) the VBO
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);

vboiId is an integer variable defined globally, also note that we have saved the amount of indices there are (just like we have saved the vertex count when working with “glDrawArrays”).

Rendering with “glDrawElements”

Rendering with “glDrawElements” is a lot like rendering with “glDrawArrays”, the main difference is that we of course use a different call to OpenGL. But also that we have to bind our index VBO before rendering.

GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);

// Bind to the VAO that has all the information about the vertices
GL30.glBindVertexArray(vaoId);
GL20.glEnableVertexAttribArray(0);

// Bind to the index VBO that has all the information about the order of the vertices
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);

// Draw the vertices
GL11.glDrawElements(GL11.GL_TRIANGLES, indicesCount, GL11.GL_UNSIGNED_BYTE, 0);

// Put everything back to default (deselect)
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);

Cleaning up our memory

When doing our memory management we also have to delete the index VBO:

// Delete the index VBO
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
GL15.glDeleteBuffers(vboiId);

The rest of our cleaning code looks exactly the same, here’s the complete cleanup code:

// Disable the VBO index from the VAO attributes list
GL20.glDisableVertexAttribArray(0);

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

// Delete the index VBO
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
GL15.glDeleteBuffers(vboiId);

// Delete the VAO
GL30.glBindVertexArray(0);
GL30.glDeleteVertexArrays(vaoId);

Display.destroy();

The result

Here’s our quad (again):

Result DrawElements.png

Complete source code

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.PixelFormat;

public class TheQuadExampleDrawElements {
	// Entry point for the application
	public static void main(String[] args) {
		new TheQuadExampleDrawElements();
	}
	
	// Setup variables
	private final String WINDOW_TITLE = "The Quad: glDrawElements";
	private final int WIDTH = 320;
	private final int HEIGHT = 240;
	// Quad variables
	private int vaoId = 0;
	private int vboId = 0;
	private int vboiId = 0;
	private int indicesCount = 0;
	
	public TheQuadExampleDrawElements() {
		// Initialize OpenGL (Display)
		this.setupOpenGL();
		
		this.setupQuad();
		
		while (!Display.isCloseRequested()) {
			// Do a single loop (logic/render)
			this.loopCycle();
			
			// Force a maximum FPS of about 60
			Display.sync(60);
			// Let the CPU synchronize with the GPU if GPU is tagging behind
			Display.update();
		}
		
		// Destroy OpenGL (Display)
		this.destroyOpenGL();
	}
	
	public void setupOpenGL() {
		// Setup an OpenGL context with API version 3.2
		try {
			PixelFormat pixelFormat = new PixelFormat();
			ContextAttribs contextAtrributes = new ContextAttribs(3, 2)
				.withProfileCore(true)
				.withForwardCompatible(true);
			
			Display.setDisplayMode(new DisplayMode(WIDTH, HEIGHT));
			Display.setTitle(WINDOW_TITLE);
			Display.create(pixelFormat, contextAtrributes);
			
			GL11.glViewport(0, 0, WIDTH, HEIGHT);
		} catch (LWJGLException e) {
			e.printStackTrace();
			System.exit(-1);
		}
		
		// Setup an XNA like background color
		GL11.glClearColor(0.4f, 0.6f, 0.9f, 0f);
		
		// Map the internal OpenGL coordinate system to the entire screen
		GL11.glViewport(0, 0, WIDTH, HEIGHT);
	}
	
	public void setupQuad() {
		// Vertices, the order is not important.
		float[] vertices = {
				-0.5f, 0.5f, 0f,	// Left top			ID: 0
				-0.5f, -0.5f, 0f,	// Left bottom		ID: 1
				0.5f, -0.5f, 0f,	// Right bottom		ID: 2
				0.5f, 0.5f, 0f		// Right left		ID: 3
		};
		// Sending data to OpenGL requires the usage of (flipped) byte buffers
		FloatBuffer verticesBuffer = BufferUtils.createFloatBuffer(vertices.length);
		verticesBuffer.put(vertices);
		verticesBuffer.flip();
		
		// OpenGL expects to draw vertices in counter clockwise order by default
		byte[] indices = {
				// Left bottom triangle
				0, 1, 2,
				// Right top triangle
				2, 3, 0
		};
		indicesCount = indices.length;
		ByteBuffer indicesBuffer = BufferUtils.createByteBuffer(indicesCount);
		indicesBuffer.put(indices);
		indicesBuffer.flip();
		
		// Create a new Vertex Array Object in memory and select it (bind)
		// A VAO can have up to 16 attributes (VBO's) assigned to it by default
		vaoId = GL30.glGenVertexArrays();
		GL30.glBindVertexArray(vaoId);
		
		// Create a new Vertex Buffer Object in memory and select it (bind)
		// A VBO is a collection of Vectors which in this case resemble the location of each vertex.
		vboId = GL15.glGenBuffers();
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
		GL15.glBufferData(GL15.GL_ARRAY_BUFFER, verticesBuffer, GL15.GL_STATIC_DRAW);
		// Put the VBO in the attributes list at index 0
		GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 0, 0);
		// Deselect (bind to 0) the VBO
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
		
		// Deselect (bind to 0) the VAO
		GL30.glBindVertexArray(0);
		
		// Create a new VBO for the indices and select it (bind)
		vboiId = GL15.glGenBuffers();
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);
		GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL15.GL_STATIC_DRAW);
		// Deselect (bind to 0) the VBO
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
	}
	
	public void loopCycle() {
		GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
		
		// Bind to the VAO that has all the information about the vertices
		GL30.glBindVertexArray(vaoId);
		GL20.glEnableVertexAttribArray(0);
		
		// Bind to the index VBO that has all the information about the order of the vertices
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);
		
		// Draw the vertices
		GL11.glDrawElements(GL11.GL_TRIANGLES, indicesCount, GL11.GL_UNSIGNED_BYTE, 0);
		
		// Put everything back to default (deselect)
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
		GL20.glDisableVertexAttribArray(0);
		GL30.glBindVertexArray(0);
	}
	
	public void destroyOpenGL() {		
		// Disable the VBO index from the VAO attributes list
		GL20.glDisableVertexAttribArray(0);
		
		// Delete the vertex VBO
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
		GL15.glDeleteBuffers(vboId);
		
		// Delete the index VBO
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
		GL15.glDeleteBuffers(vboiId);
		
		// Delete the VAO
		GL30.glBindVertexArray(0);
		GL30.glDeleteVertexArrays(vaoId);
		
		Display.destroy();
	}
}

Credit

Mathias Verboven (moci)