Skip to content

Rendering

Heiko Brumme edited this page Jan 3, 2018 · 16 revisions

In this part of the tutorial we will finally do some rendering with OpenGL 3.2 core profile.
Note that in the source code there is also a legacy OpenGL 2.1 version available.

Creating the context

Before we start, we want to tell GLFW to use an OpenGL 3.2 core profile context, this is easily done like in the following code example.

glfwDefaultWindowHints();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
long window = glfwCreateWindow(width, height, title, NULL, NULL);

First we reset the window hints to its defaults with glfwDefaultWindowHints() in case there was already created another window with other window hints.
GLFW_CONTEXT_VERSION_MAJOR and GLFW_CONTEXT_VERSION_MINOR should be self-evident, it just tells GLFW to create a 3.2 context.
With GLFW_OPENGL_PROFILE we specify that we want to use core functionality. If you want to use and OpenGL version below 3.2 you have to use GLFW_OPENGL_ANY_PROFILE, which is the default value for this hint.
The GLFW_OPENGL_FORWARD_COMPAT hint specifies if the OpenGL context should be forward compatible, if set to GLFW_TRUE it will deactivate all deprecated functionalities. If this is used with an OpenGL version below 3.0 this hint will get ignored.

If your graphic card doesn't support OpenGL version 3.2 the window will be NULL, in that case try creating a legacy OpenGL 2.1 context like in the following code.

glfwDefaultWindowHints();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
long window = glfwCreateWindow(width, height, title, NULL, NULL);

Vertex Array Objects

Now that we have a window with our desired context we can start with the initialization of the rendering.
The first thing we should do is creating a Vertex Array Object, or just VAO. It is used to store all the links between your Vertex Buffer Object and the attributes. So make sure that the first thing you do is creating a VAO and bind it.

int vao = glGenVertexArrays();
glBindVertexArray(vao);

You can find these two function in the GL30 class of LWJGL3, so don't use this if you have requested an OpenGL 2.1 context.

Vertex Buffer Objects

In the old fixed function pipeline you just passed your vertices in every rendering call between glBegin(target) and glEnd(), but the modern way is to put them into a Vertex Buffer Object (VBO).
The VBO is used to store all your vertex data on your GPU. With LWJGL you have to create a Buffer to pass your vertices to the GPU. In our simple example we will take the triangle from the Introduction tutorial. For creating the buffer we use LWJGL's MemoryStack to create an appropriate FloatBuffer.
You could also use LWJGL's MemoryUtil for creating a buffer, but you would use that for buffers with custom lifecycles where you have explicit memory allocation and freeing. For local buffers it is adviced to use MemoryStack. For more information about memory management in LWJGL3 take a look here.
Note that by convention the vertices are ordered counter-clockwise.

MemoryStack stack = MemoryStack.stackPush();
FloatBuffer vertices = stack.mallocFloat(3 * 6);
vertices.put(-0.6f).put(-0.4f).put(0f).put(1f).put(0f).put(0f);
vertices.put(0.6f).put(-0.4f).put(0f).put(0f).put(1f).put(0f);
vertices.put(0f).put(0.6f).put(0f).put(0f).put(0f).put(1f);
vertices.flip();

Do not forget to do vertices.flip()! This is important, because passing the buffer without flipping will crash your JVM because of a EXCEPTION_ACCESS_VIOLATION.

After creating the buffer you can upload it to your GPU, but before that we need to create and bind a VBO.

int vbo = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
MemoryStack.stackPop();

With that call we have our VBO stored on the GPU. It should be noted, that we use a interleaved VBO, so it contains vertex and color data. We will take a look how to specify the data in a few moments.
You could also use two VBOs for vertex and color data respectively.

For convenience MemoryStack implements the AutoClosable interface, so you can use a try-with-resources Statement. This is recommended instead of using stackPush() and stackPop() manually:

try (MemoryStack stack = MemoryStack.stackPush()) {
    FloatBuffer vertices = stack.mallocFloat(3 * 6);
    vertices.put(-0.6f).put(-0.4f).put(0f).put(1f).put(0f).put(0f);
    vertices.put(0.6f).put(-0.4f).put(0f).put(0f).put(1f).put(0f);
    vertices.put(0f).put(0.6f).put(0f).put(0f).put(0f).put(1f);
    vertices.flip();

    int vbo = glGenBuffers();
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
}

Shaders

The next step of initialization is to create and compile the shaders. In OpenGL you use the OpenGL Shading Language, in short just GLSL, which has a syntax similar to C.
For this tutorial we will use two simple shaders, a vertex shader and a fragment shader which are also the two shaders that have to be in every shader program.

Vertex Shaders

The vertex shader processes each vertex and its attributes to calculate its final vertex position (or just an updated vertex position if you use a tesselation or geometry shader afterwards). It also passes through the data that the fragment shader requires like color and texture coordinates.

So let's take a look at our example shader.

#version 150 core

in vec3 position;
in vec3 color;

out vec3 vertexColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    vertexColor = color;
    mat4 mvp = projection * view * model;
    gl_Position = mvp * vec4(position, 1.0);
}

In the first line we specify which GLSL version we like to use. For OpenGL 3.2 it is GLSL version 1.50, with OpenGL 2.1 you have to use GLSL version 1.20. For a quick reference which GLSL version you can use look at the table found at the end of this tutorial.

The in keyword specifies that those variables are passed in by your program code, in this case two three-tuple vectors for position and color, the value get provided per vertex.
The out variables will get passed to the next shader, which in this case is the fragment shader, the values also get provided per vertex.
Finally the uniform variables are global GLSL variables, they are also passed by your program code, the difference is, that they have the same value for each vertex.

After that comes the main method, in the vertex shader you actually just have to set gl_Position for the final vertex position.
In this example we have the out variable vertexColor, so we have to tell the shader what's inside that variable.
We also have the model, view and projection matrices which we use to calculate the MVP matrix, note that you have to calculate them in reversed order.
Finally we multiply our MVP matrix to the position to get the final vertex position.

If you are using OpenGL 2.1, then use the following shader instead.

#version 120

attribute vec3 position;
attribute vec3 color;

varying vec3 vertexColor;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    vertexColor = color;
    mat4 mvp = projection * view * model;
    gl_Position = mvp * vec4(position, 1.0);
}

That shader is similar to the other vertex shader, only the keywords have changed a bit.
You could compare attribute to an in variable, they get passed in by your program code.
The varying could be compared with an out variable, it will get passed to the next shader.

Fragment Shaders

After passing through the vertex shader the vertices get interpolated over the pixels that are covered by the primitive, those pixel are called fragments. The fragment shader will calculate the final output color for each fragment.

#version 150 core

in vec3 vertexColor;

out vec4 fragColor;

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

Notice that the in variable has the same name as the out variable passed in by the vertex shader.
The out variable is used to store the output color of the fragment.
Inside the main we just pass the interpolated vertexColor to our out variable which is then the output color of the fragment.

If you are using OpenGL 2.1, then use the following shader instead.

#version 120

varying vec3 vertexColor;

void main() {
    gl_FragColor = vec4(vertexColor, 1.0);
}

You will see that it is almost similar to the other fragment shader.
In this case the varying variable could be compared to the in variable, it also have to be the same name as the varying variable of the vertex shader.
With legacy OpenGL 2.1 you can't use an out variable, so you have to use the built-in variable gl_FragColor for the output color.

Compiling Shaders

Now that we know how those shaders work, we finally can create and compile them.
Getting the handle of a shader is similar to all the other creations with a call to glCreateShader(type). After that we set the shader source and compile it. The shader source is just the concatenated GLSL code, you could write a string with the shader code, but it should be noted that you need a \n at least after declaring the version.

int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, vertexSource);
glCompileShader(vertexShader);

int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, fragmentSource);
glCompileShader(fragmentShader);

It would be a good idea to check if compilation was successful. You can do it with the shader info log.

int status = glGetShaderi(shader, GL_COMPILE_STATUS);
if (status != GL_TRUE) {
    throw new RuntimeException(glGetShaderInfoLog(shader));
}

Shader Programs

After compiling the shaders there is just one last step before we get to rendering, we have to link a shader program.
By now you could guess how to get the handle for the shader program, it is done by calling glCreateProgram(). Then you have to attach your shaders to this program and finally link it.

int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glBindFragDataLocation(shaderProgram, 0, "fragColor");
glLinkProgram(shaderProgram);

You may notice glBindFragDataLocation(shaderProgram, 0, "fragColor") which wasn't mentioned above, this is because it is optional when we have just one out variable in our fragment shader and it gets bound to the color number 0 by default.
That command is also only available in GL30, so don't use it if you have an OpenGL 2.1 context.
After linking the program you could check if it was successful by using the program info log.

int status = glGetProgrami(shaderProgram, GL_LINK_STATUS);
if (status != GL_TRUE) {
    throw new RuntimeException(glGetProgramInfoLog(shaderProgram));
}

After that we can use the shader program.

glUseProgram(shaderProgram);

Specify Vertex Attributes

At the very start of this tutorial we put our vertex and color data in our VBO, but we didn't told the program how to use that data. This can be done by using vertex attributes.
Setting up a vertex attribute requires three steps, first you get the location of the attribute, then you enable it and finally you point the vertex attribute.

int floatSize = 4;

int posAttrib = glGetAttribLocation(shaderProgram, "position");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, false, 6 * floatSize, 0);

int colAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, false, 6 * floatSize, 3 * floatSize);

The first two commands are pretty straight forward, but the glVertexAttribPointer(location, size, type, normalized, stride, offset) requires a bit of explaining.
The location is self-evident, it is just the attribute location we just got.
With size we tell the program how many values the data contains per vertex.
After that type specifies what data type this attribute uses, in this case it is a float.
If you want you could let the program normalize the values to be between -1.0 and 1.0, or 0.0 and 1.0 if you pass a unsigned type, but most of the time you set normalized just to false, for more info when you could use normalized values take a look here.
Now to stride and offset, it is important to know, that you need to pass in the number of bytes. The stride tells how many bytes are between consecutive generic vertex attributes, in this case every vertex has 3 floats for the 3D coordinates and another 3 floats for the RGB values, so the stride is 6 * floatSize = 6 * 4 = 24.
And finally the offset specifies the offset to the first component of the first generic vertex attribute, in our VBO the vertex position is at the start of each vertex, so the offset is just 0. Because our position has 3 floats the offset of the color attribute is 3 * floatSize = 3 * 4 = 12.

Set uniform variables

Our initialization is almost done, the last thing is setting the uniform variables.
For this tutorial we have just three matrices as uniforms, but you could also pass other types of data. If you don't already know how to do vector and matrix maths you should take a look here and here or in the link provided as reference.

But you don't have to implement it yourself if you don't want to, because in this tutorial there are very basic vector and matrix classes provided, you can look at them here.
For a more complete math library you may want to take a look at the Java OpenGL Math Library (JOML) which you can also add in the LWJGL download tool.

Setting a uniform is almost similar to setting a vertex attribute, you get the location first and then you set it.

int uniModel = glGetUniformLocation(shaderProgram, "model");
Matrix4f model = new Matrix4f();
glUniformMatrix4fv(uniModel, false, model.getBuffer());

int uniView = glGetUniformLocation(shaderProgram, "view");
Matrix4f view = new Matrix4f();
glUniformMatrix4fv(uniView, false, view.getBuffer());

int uniProjection = glGetUniformLocation(shaderProgram, "projection");
float ratio = 640f / 480f;
Matrix4f projection = Matrix4f.orthographic(-ratio, ratio, -1f, 1f, -1f, 1f);
glUniformMatrix4fv(uniProjection, false, projection.getBuffer());

The parameter of the glUniformMatrix4fv(location, transpose, values) are pretty straight forward, location is the location we just got.
If transpose is set to true the matrix will get transposed before it gets used.
Like usual the values get provided by putting them into a Buffer.

For calculating a orthographic matrix you could take a look at glOrtho(left, right, bottom, top, near, far), where it is described how to calculate it, but in the source code that calculation is already provided.
When writing it yourself you should note, that when you put the values inside a FloatBuffer you should know that in OpenGL the matrices are column-major.

Now is a good time to explain model, view and projection matrices.
The model matrix will calculate the local object coordinates to the world coordinates. In OpenGL you use it for example for scaling, translating or rotating the object.
From the world coordinates you use the view matrix to calculate the eye coordinates, this is used for the camera position. But you won't move the camera, instead you move the world, so to get the correct view matrix you have to transform the world with the inverse of the camera transformation.
Finally the projection will take the eye coordinates and calculates the clip coordinates, you use that matrix to apply an orthographic or a perspective matrix.
It is already mentioned above but one important note about calculation the MVP matrix is that you have to multiply those three matrices in reversed order. This is because how the vector and matrix calculation works: newVector = projection * view * model * vector, so actually it gets calculated like this: newVector = projection * (view * (model * vector)).

Rendering

Finally we can render our triangle, this is done by just calling glDrawArrays(type, first, count). Don't forget to clear your screen before that.

glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLES, 0, 3);

The first command just clears the color buffer of the screen, if you don't do that you will get weird results.
Calling glDrawArrays(type, first, count) will draw the VBO that is currently bound. The type is GL_TRIANGLES most of the times, you could also use some other trianlge type like GL_TRIANGLE_STRIP or some other type like GL_POINTS.
Since we want to draw all vertices we set start to 0, and we have three vertices to draw, so count is set to 3.

Interpolating with alpha value

But that static triangle is boring, so let's rotate it like in the introduction! You remember update(float delta) and render(float alpha) from the game loop tutorial? Now it is the time to use them.

For rotating the triangle we need to set our model matrix to a rotation matrix, the source code provides this function already, if you want to implement it yourself you can take a look at glRotate(angle, x, y, z).

Beside the rotation matrix we also need the current angle and what angle it should be rotate per second. In the example code the triangle rotates by 50 degrees per second.

private int uniModel;
private float angle = 0f;
private float anglePerSecond = 50f;

In our update method we will calculate the angle of the current loop by using the delta value, and in the render method we just apply the current angle to the model matrix.

public void update(float delta) {
    angle += delta * anglePerSecond;
}

public void render(float alpha) {
    glClear(GL_COLOR_BUFFER_BIT);

    Matrix4f model = Matrix4f.rotate(angle, 0f, 0f, 1f);
    glUniformMatrix4fv(uniModel, false, model.getBuffer());

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

For a variable timestep this is perfectly okay, but let's take a look at a fixed timestep with a target of 5 UPS, with that loop it will stutter badly.
But what can we do about it? If you remember the game loop tutorial the alpha value is used for interpolating.
So for interpolating we need another variable, the angle of the last loop.

private int uniModel;
private float previousAngle = 0f;
private float angle = 0f;
private float anglePerSecond = 50f;

With that done we can interpolate between the last angle and the current angle. If you think about it for a moment you will realize, that this means, that your screen will 'lag' one frame behind, but that is okay. Every professional game is doing this when using a fixed timestep, you would not even notice it.
Because we have the previous angle as variable we have to set it inside the update method.

public void update(float delta) {
    previousAngle = angle;
    angle += delta * anglePerSecond;
}

The interpolation itself will be made inside the render method and applied to the rotation matrix.

public void render(float alpha) {
    glClear(GL_COLOR_BUFFER_BIT);

    float lerpAngle = (1f - alpha) * previousAngle + alpha * angle;
    Matrix4f model = Matrix4f.rotate(lerpAngle, 0f, 0f, 1f);
    glUniformMatrix4fv(uniModel, false, model.getBuffer());

    glDrawArrays(GL_TRIANGLES, 0, 3);
}

With interpolating between those two states it doesn't matter if you run at 5 UPS or if you are running at 60 UPS, it will always look like it has a smooth transformation.

Cleaning up

When ending your application it is good practice to clean up your used graphic data.

glDeleteVertexArrays(vao);
glDeleteBuffers(vbo);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glDeleteProgram(shaderProgram);

Next steps

In the next part we will see how to use a texture. Until then have fun with trying out shader programming!


Source

References

Appendix

GLSL version table

In the first line of the shader code you should specify the version you want to use via #version <version> <profile>. The <version> can be anything your OpenGL version allows. The <profile> is available only for OpenGL version > 3.2, and it could be core for forward compatibility (e.g. don't allow deprecated syntax) or compatibility for backwards compatibility. Writing no <profile> info defaults it to core.

OpenGL GLSL
2.0 1.10
2.1 1.20
3.0 1.30
3.1 1.40
3.2 1.50
3.3 3.30
4.0 4.00
4.1 4.10
4.2 4.20
4.3 4.30
4.4 4.40
4.5 4.50