Chapter 1-Drawing your first Triangle

The "Hello World" of Computer Graphics

At ease, gents! Coffee Bean Code here, with the next part of your Java OpenGL Bootcamp!

Side note, why did I start the chapter counter at 0? Why? Was making a programming joke worth throwing off my ability to count what chapter we're on? Will it be my torturous fate to have my count be inconsistent with the chapter number?

...

Anyways, welcome to the snurkle-th chapter. In this chapter, we will go over the most basic, yet fundamental part of any modern render engine: the Triangle.

Much like how DNA is the building block of life as we know it, Triangles can be seen as the building blocks of modern graphics as we know it. As such, Triangles are known to programmers by their very technical name of "Godly Pointy Points."

Now, there's a two methods of rendering in OpenGL.

  • Immediate Mode, which sucks. This isn't personal bias, by the way, the GPU is directly linked to the program flow in Immediate Mode, and can't begin rendering until the program finishes sending the vertex data.

  • Retained Mode. By using Vertex Buffer Objects, the program passes the Vertex information once to the OpenGL drivers instead of sending the Vertex data for every mesh being rendered every SINGLE render cycle.

But how do we utilize the power of Retained Mode? Well buckle your pants and let's get to it!

Using the project from the last chapter, we'll start by adding a new class to our project. This time, we'll be adding it into a new package. You can do this how we did in the last chapter by typing it into the package dialogue when making your new class, however an easier way is to choose File->New->Package, and create a new package within the tutorials package named "render"

Your new class should be called something along the lines of MeshLoader, and it should be created in the tutorials.render package.

Starting off, we'll be loading these Java/LWJGL classes along with the ones we currently have:

MeshLoader.java
import java.nio.FloatBuffer; //The buffers that the Vertex data is ultimately stored in
import java.util.ArrayList; 
import java.util.List; //List and ArrayLists are containers for storing data, in this case the VBO/VAO IDs

import org.lwjgl.BufferUtils; //For creating the FloatBuffer
import org.lwjgl.opengl.GL11; 
import org.lwjgl.opengl.GL15; 
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;

So, with our arsenal ready, let's begin coding. First, we'll need to create two private Lists named vaos and vbos. These lists will store the IDs for the Vertex Array Objects and Vertex Buffer Objects.

Right now your class should look like this:

MeshLoader.java
public class MeshLoader{
    private static List<Integer> vaos = new ArrayList<Integer>();
	private static List<Integer> vbos = new ArrayList<Integer>();
}

Now, before we continue, a brief explanation. A VAO is an array which contains the VBOs of a mesh. The VBOs contain the mesh's Vertex positions, indices, texture coordinates, and normals. For now, we're only going to be focusing on the Vertex positions and indices.

So, now we'll be making a method to store an array of floats as a FloatBuffer, as well as a method to store an array of integers as an IntBuffer.

MeshLoader.java
private static FloatBuffer createFloatBuffer(float[] data) {
		FloatBuffer buffer = BufferUtils.createFloatBuffer(data.length);
		buffer.put(data);
		buffer.flip();
		return buffer;
}
	
private static IntBuffer createIntBuffer(int[] data) {
		IntBuffer buffer = BufferUtils.createIntBuffer(data.length);
		buffer.put(data);
		buffer.flip();
		return buffer;
}

What this does is it turns arrays of float or int data in a Buffer, a container for primitive data types. When data is put into the buffer in the put(...) method, it is setting it into write mode. By calling flip(), the buffer is set into read mode. This means that code down the line only gets what was written in the buffer, and nothing more.

Now, with the methods for creating both buffer types done, let's make the methods for creating VBOs for the Vertex and Index data:

MeshLoader.java
private static void storeData(int attribute, int dimensions, float[] data) {
		int vbo = GL15.glGenBuffers(); //Creates a VBO ID
		vbos.add(vbo);
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo); //Loads the current VBO to store the data
		FloatBuffer buffer = createFloatBuffer(data);
		GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
		GL20.glVertexAttribPointer(attribute, dimensions, GL11.GL_FLOAT, false, 0, 0);
		GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); //Unloads the current VBO when done.
}

private static void bindIndices(int[] data) {
		int vbo = GL15.glGenBuffers();
		vbos.add(vbo);
		GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vbo);
		IntBuffer buffer = createIntBuffer(data);
		GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
}

Now, the storeData(...) method needs some explaining. The attribute variable is used to signify what the VBO is going to be used as, be it Vertex Position, Texture Coordinates, or Normals. The dimensions variable tells the system whether the coordinates these numbers represent are 2D or 3D. In both the storeData(...) and bindIndices(...) methods, the data array is the data being loaded into the VBO.

Now, the glVertexAttribPointer(...) method lets OpenGL know what the attribute variable is, what type of data has been loaded (Float, Integer, etc), whether the Vertex is normalized, what the offset is between the vertex attributes, and where the first vertex attribute is located in the array.

You'll notice that the bindIndices(...) method doesn't use any vertex attributes. This is because the Indices are used to tell what the order of vertexes is when drawing the mesh.

Moving on, we'll now make the methods to tie all of this together:

MeshLoader.java
public static Mesh createMesh(float[] positions, int[] indices) {
		int vao = genVAO();
		storeData(0,3,positions);
		bindIndices(indices);
		GL30.glBindVertexArray(0);
		return new Mesh(vao,indices.length);
}
	
private static int genVAO() {
		int vao = GL30.glGenVertexArrays();
		vaos.add(vao);
		GL30.glBindVertexArray(vao);
		return vao;
}

Now, we don't have a Mesh class yet, but don't fret, we'll get to that in a bit.

The genVAO() method's purpose is to create a new VAO, store it in the vaos list, and prep it for storing the vertex data and indices. Using our fancy-ass methods for storing the position data and indices, we load the VAO with the mesh data it'll be rendering. Once it's gotten its fill of tasty, tasty data, we unload the VAO and return a new Mesh with the VAO ID and the size of the indices array.

And with that, we're done with the MeshLoader class for now! Now, let's make a new class in the tutorials.render package, and name it Mesh. This part'll be difficult, but I have faith that you will all perservere.

Mesh.java
package tutorials.render;

public class Mesh {

	private int vao;
	private int vertices;
	
	public Mesh(int vao, int vertex) {
		this.vao = vao;
		this.vertices = vertex;
	}

	public int getVaoID() {
		return vao;
	}

	public int getVertexCount() {
		return vertices;
	}
}

Okay, that was a lie. It's a really simple class that only stores the VAO ID and the number of vertices.

So, we learned how to load float and integer data into VBOs, load those VBOs into a VAO, now what? Now, we draw us a gosh dang Godly Pointy Point!

Going into our Boot class, we're going to be making two arrays just before the while(...) loop in the constructor.

Boot.java
float[] vertices = {-0.5f,-0.5f,0f,
                0.5f, -0.5f, 0f,
                0f,0.5f,0f};
int[] indices = {0,1,2};

As you can tell by the names and your newfound knowledge of VAOs and VBOs, these arrays make up the Vertex positions and the Indices of our Mesh. From here, we just need to import the MeshLoader class and call our createMesh method!

Boot.java
Mesh meshmeyek = MeshLoader.createMesh(vertices,indices); //Kudos if you got that reference

Now, going into the while loop, we can finally draw our triangle:

Boot.java
while(!GLFW.glfwWindowShouldClose(window)) {
			GL11.glClear(GL11.GL_COLOR_BUFFER_BIT|GL11.GL_DEPTH_BUFFER_BIT);
			
			GL30.glBindVertexArray(meshmeyek.getVaoID());
			GL20.glEnableVertexAttribArray(0);
			GL11.glDrawElements(GL11.GL_TRIANGLES, meshmeyek.getVertexCount(), GL11.GL_UNSIGNED_INT,0);
			GL20.glDisableVertexAttribArray(0);
			GL30.glBindVertexArray(0);
			
			GLFW.glfwSwapBuffers(window);
			GLFW.glfwPollEvents();
}

So, as we've seen before, the glBindVertexArray(...) method binds the VAO of our mesh. Looking back at our earlier code, we know that we bound the Vertex Positions array to the attribute array 0, so glEnableVertexAttribArray(0) is enabling the Vertex Positions to be drawn. As can be seen in the name, glDrawElements allows us to draw GL_TRIANGLES for the amount of vertices in our mesh. GL_UNSIGNED_INT tells OpenGL the type of values of our index array, and 0 tells OpenGL which vertex to begin drawing from.

Now, let's compile this bad boy and see what we get!

Congratulations! You've drawn your very own Godly Pointy Point. Be sure to love him, feed him, and make sure he does pee on the carpet.

Next time, we'll be going into shaders and textures.

Last updated