Rendering with OpenGL - Fish-In-A-Suit/Conquest GitHub Wiki
Submitting vertex data for rendering requires creating a stream of vertices, and then telling OpenGL how to interpret that stream. It stores the format of the vertex data as well as the Buffer Objects (VBOs) providing the vertex data arrays.
Basics summed up:
- take coordinates of object's vertices
- store them in a VBO
- create a VAO
- store the VBO into one of the attribute lists of the VAO
- create and hook up the shaders
- use id of the VAO to tell OpenGL to render it's contents.
OpenGL rendering pipeline
This image depicts the programmable OpenGL rendering pipeline. Back in the day it was fixed so the programmers were quite restrained regarding rendering. Although it's now more comprehensive, it provides much more flexibility than the fixed rendering pipeline.
Vertex speicification
Setting up a VAO, or any other object for that matter, follows the following rules:
- Request a memory location (returns integer number, the ID of the object)
- Bind the object using the ID
- Manipulate the object (the object is whichever object OpenGL is currently bound to)
- Unbind the currently bound object using the ID
Make sure not to forget to unbind the objects (VAOs, VBOs) once they are no longer needed!
Defining the vertices
OpenGL draws anything in triangles. ** Vertices need to be ordered in a counter-clockwise order**! The default OpenGL coordinate system goes from -1 to 1 in all axes. This coordinate system is mapped to the entire screen:
Before using the vertices by OpenGL methods, they have to be wrapped in a bytebuffer (usually of type FloatBuffer). Make sure to flip the FloatBuffer once you are done writing to it!
Make sure to keep track of the vertex count, as this will be needed by the glDrawArrays() method.
Vertex array object
The vertex array object stores all of the OpenGL state related to the relationship between buffer objects and the generic vertex attributes, as well as the information about the format of the data in the buffer objects. This allows us to quickly return all of this state when rendering.
A vertex array object (VAO) is an OpenGL Object which stores one or more vertex buffer objects (VBOs) and is designed to store information for a complete rendered object. It is an object in which you can store data about a 3D model. It has a 16 slots (able to be accessible through numbers 0 to 15) in which you can store data and these slots are called AttributeLists. You store different sets of data into different attribute lists (in one attribute list, you might store all the vertex positions, in another all the colours of the vertices, texture coordinates, normal vector of each vertex, etc). The aforementioned data sets are stored in attribute lists of the VAO as vertex buffer objects - VBOs.
How to access the data? Each VAO has a unique ID. Whenever a VAO is stored in memory, you can access it by using it's ID.
-
Creation: int glGenVertexArrays(); creates a vertex object array in the memory and returns it's name as an int
-
Deletion: void glDeleteVertexArrays(GLuint array);
- arrays: specifies the address of a VAO to be deleted
-
Binding: void glBindVertexArray(GLuint array);
- array: specifies the name of the vertex array to bind, or 0 to unbind it. VAO cannot be used prior to being bound.
Vertex buffer objects
A vertex buffer object (VBO) is a memory buffer in the high speed memory of your video card designed to hold information about vertices. In it's simplest explanation, a VBO is just data. It may contain the following information:
- coordinates of vertices
- colours associated with each vertex
- normals
- texture coordinates
- indices
Common methods:
-
Creation: (class GL15) public static int glGenBuffers(): creates a buffer object in memory and returns it's name/address as an int
-
Deletion: (class GL15) public static void glDeleteBuffers(int buffer): delete the named buffer object
-
Binding: (class GL15) public static void glBindBuffer(int target, int buffer): binds a specified buffer object and makes it ready for use
The target parameter in glBindBuffer must be one of the following: ARRAY_BUFFER, ELEMENT_ARRAY_BUFFER, PIXEL_PACK_BUFFER, PIXEL_UNPACK_BUFFER, TRANSFORM_FEEDBACK_BUFFER, UNIFORM_BUFFER, TEXTURE_BUFFER, COPY_READ_BUFFER, COPY_WRITE_BUFFER, DRAW_INDIRECT_BUFFER, ATOMIC_COUNTER_BUFFER, DISPATCH_INDIRECT_BUFFER, SHADER_STORAGE_BUFFER PARAMETER_BUFFER_ARB.
Most OpenGL objects must be bound to locations in the OpenGL context called targets for them to be used. A target is a place in the context where objects are bound.
Different object types (buffers, textures) have differents sets of targets. Generally speaking, each target has a specific meaning: to bind one object to one target means that you want to use that object in whatever manner that target uses objects bound to it.
Binding an object to one target doesn't affect whether the object is bound to another target (unless it's a texture object, which are treated a bit differently). There are functions (methods) that modify objects or query data from bound objects. They take a target to which the object they are modifying/querying has been bound.
Using interleaved arrays
Instead of having two or three separate arrays (vertexPositions, colours, indices), you could have all of this information specified inside one array, stored in one vbo located in one list of the vao. Now, the stride parameter of glAttribPointer comes as very useful.
Vertex shader
MOVE THIS ELSEWHERE
Rendering with VAOs, VBOs and glDrawArrays
Rendering to the window consists of two parts: submitting vertices to memory (this operation is performed only once) and actually rendering those vertices on the window (this operation is performed inside the main loop).
a) Submitting vertices to memory
- Initialize a float array of vertices:
float[] vertices = {
// Left bottom triangle
-0.5f, 0.5f, 0f,
-0.5f, -0.5f, 0f,
0.5f, -0.5f, 0f,
// Right top triangle
0.5f, -0.5f, 0f,
0.5f, 0.5f, 0f,
-0.5f, 0.5f, 0f
};
- Store this array of vertices in a floatbuffer and prepare it to be read from (flip it):
FloatBuffer verticesBuffer = BufferUtils.createFloatBuffer(vertices.length);
verticesBuffer.put(vertices);
verticesBuffer.flip();
- Create a new vertex array object in memory and select (bind) it:
vaoId = glGenVertexArrays();
glBindVertexArray(vaoId);
- Create a new VBO in memory and select (bind) it:
vboId = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vboId);
- Send the data from the floatbuffer which contains vertices to the currently selected VBO and specify the frequency of data usage (third parameter):
glBufferData(GL_ARRAY_BUFFER, verticesBuffer, GL_STATIC_DRAW);
To put the data in the VBO, use the public static void glBufferData(int target, buffer data, int usage) method. Take note of the following params:
- the type (we're using GL_ARRAY_BUFFER, a default definition for generic data)
- the buffer (the FloatBuffer that hold the positions of the vertices)
- the usage (our vertices won't move or change, so the usage is GL_STATIC_DRAW)
glBufferData sends the specified buffer (FloatBuffer/IntBuffer/etc) to buffer object currently bound to target.
- Put the VBO in one of the attribute lists of the VAO:
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
When putting a VBO in an attribute list of a certain VAO you should first bind the VAO and then call void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride) which needs the ID of the attribute list (0 to 15 by default). When using the glVertexAttribPointer, which puts the VBO in a specified attribute list of the VAO, you have to be familiar with the following parameters:
- index: Index of the attribute lsit (0 in our case)
- size:How many values define one data definition (3 floats per one vertex in our case)
- type: What types the values have (floats in our case)
- normalized: whether fixed-point data values should be normalized or converted directly as fixed-point values when they are accessed
- The stride and offset (set them to 0, not dealing with interleaving data atm)
- Deselect the VBO and VAO (bind both to 0)
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
Taken all of the above knowledge:
// 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);
b) Rendering with glDrawArrays
- clear the colour buffer with desired colours:
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glClearColor specifies the red, green, blue, and alpha values used by glClear to clear the color buffers.
- Select (bind) the VAO which holds desired info about the vertices:
glBindVertexArray(vaoId);
3.Enable the VBO index in the VAO attributes list:
glEnableVertexAttribArray(0);
If enabled, the values in the VBO stored in the specified attribute list of the VAO will be accessed and used for rendering when calls are made to vertex array commands such as glDrawArrays or glDrawElements.
- Issue a draw command:
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
The glDrawArrays(...) method needs the following params:
- how to draw the vertices (for example, GL_TRIANGLES)
- the first index (start from the beginning at 0)
- The vertex count (number of vertices of the model)
- Deselect the VBO index from the VAO attribute list:
glDisableVertexAttribArray(0);
- Deselect the VAO
glBindVertexArray(0);
Taken all of the knowledge: the rendering code looks like:
GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
GL30.glBindVertexArray(vaoId);
GL20.glEnableVertexAttribArray(0);
GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, vertexCount);
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);
c) Cleaning up the memory before the program ends
- Disable the VBO index from the VAO attributes list:
glDisableVertexAttribArray(0);
- Deselect and delete the VBO:
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteBuffers(vboId);
- Deselect and delete the VAO:
glBindVertexArray(0);
glDeleteVertexArrays(vaoId);
The whole cleanup code:
// Disable the VBO index from the VAO attributes list
GL20.glDisableVertexAttribArray(0);
// Delete the VBO
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GL15.glDeleteBuffers(vboId);
// Delete the VAO
GL30.glBindVertexArray(0);
GL30.glDeleteVertexArrays(vaoId);
Now, just so you can look at it as a whole, a class that draws a white quad to a black background of a window:
import java.nio.FloatBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL;
import static org.lwjgl.system.MemoryUtil.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.GL_FALSE;
import static org.lwjgl.opengl.GL11.GL_TRUE;
import static org.lwjgl.opengl.GL.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;
public class Quad {
public static void main(String[] args) {
new Quad().start();
}
private long window;
private final String WINDOW_TITLE = "The Quad: glDrawArrays";
private final int WIDTH = 1920;
private final int HEIGHT = 1080;
//quad variables
private int vaoId = 0;
private int vboId = 0;
private int vertexCount = 0;
private boolean running = false;
public void start() {
running = true;
setupOpenGL();
setupQuad();
while (running) {
loopCycle();
update();
if(glfwWindowShouldClose(window)) {
running = false;
}
}
destroyOpenGL();
}
public void setupOpenGL() {
if(!glfwInit()) {
throw new IllegalStateException("Unable to initialize GLFW!");
}
glfwDefaultWindowHints();
glfwWindowHint(GLFW_VISIBLE, GL_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GL_TRUE);
window = glfwCreateWindow(WIDTH, HEIGHT, WINDOW_TITLE, NULL, NULL);
if (window == NULL) {
throw new RuntimeException("Cannot create window!");
}
glfwMakeContextCurrent(window);
GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
glfwSetWindowPos(window, (vidmode.width() - WIDTH) / 2, (vidmode.height() - HEIGHT) / 2);
glfwShowWindow(window);
GL.createCapabilities();
}
public void setupQuad() {
// OpenGL expects vertices to be defined counter clockwise by default
float[] vertices = {
// Left bottom triangle
-0.5f, 0.5f, 0f,
-0.5f, -0.5f, 0f,
0.5f, -0.5f, 0f,
// Right top triangle
0.5f, -0.5f, 0f,
0.5f, 0.5f, 0f,
-0.5f, 0.5f, 0f
};
FloatBuffer verticesBuffer = BufferUtils.createFloatBuffer(vertices.length);
verticesBuffer.put(vertices);
verticesBuffer.flip();
vertexCount = 6;
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);
}
public void loopCycle() {
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindVertexArray(vaoId);
glEnableVertexAttribArray(0);
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
glDisableVertexAttribArray(0);
glBindVertexArray(0);
}
public void destroyOpenGL() {
// Disable the VBO index from the VAO attributes list
glDisableVertexAttribArray(0);
// Delete the VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDeleteBuffers(vboId);
// Delete the VAO
glBindVertexArray(0);
glDeleteVertexArrays(vaoId);
glfwDestroyWindow(window);
glfwTerminate();
}
public void update() {
glfwSwapBuffers(window);
glfwPollEvents();
}
}