Leaves One

Alan Richard's Blog

Use LWJGL 3 with ThinMatrix's OpenGL 3D Game Tutorial (Part 1)

What is this?

Lately I’ve been learning OpenGL and game development with ThinMatrix’s OpenGL 3D Game Tutorial which is a great resource for beginners. However, this tutorial uses LWJGL 2, which is a bit outdated. For this reason, I’ll be using LWJGL 3.3.1 and Java 17 with this tutorial.

This note is where I document my journey of using LWJGL 3 with the tutorial. For the most part of the note, it will be how I modify the code to make it work with LWJGL 3. I will also include additional information that I think is useful.

In headings, I will include the tutorial episode number in the format of T1, T2, etc. This is to make it easier to find the corresponding episode.

This note covers problems I encountered or changes I made to the code in the first 10 episodes. Click here to read the second part of this note.

GLFW: Where’s the Display class? (T1)

Note: This section is based on Tutorial 1: The Display.

I’m using Maven to manage dependencies. Use this link to generate the pom.xml file.

If you are using macOS, be sure to add -XstartOnFirstThread to the VM arguments.

The first thing I noticed is that the Display class is gone in LWJGL 3. We will be using GLFW instead. GLFW is a library comes with LWJGL for creating windows and handling input. I found this guide (Internet Archive link) to be very informative.

The following is the code for creating a window. Here I adjusted the code in the guide to make it work with the tutorial, including solving the GLFW_INVALID_VALUE error: Context profiles are only defined for OpenGL version 3.2 and above error.

DisplayManager.java

package renderEngine;

import org.lwjgl.*;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.*;
import org.lwjgl.system.*;

import java.nio.*;

import static org.lwjgl.glfw.Callbacks.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryStack.*;
import static org.lwjgl.system.MemoryUtil.*;

public class DisplayManager {

private static final int WIDTH = 1280;
private static final int HEIGHT = 720;
private static final String TITLE = "Alan's OpenGL 3D Game Engine";

// The window handle
public static long window;

public static void createDisplay() { // <- ❗️ IMPORTANT: This method is now static
System.out.println("LWJGL " + Version.getVersion());

// Setup an error callback. The default implementation
// will print the error message in System.err.
GLFWErrorCallback.createPrint(System.err).set();

// Initialize GLFW. Most GLFW functions will not work before doing this.
if (!glfwInit()) {
throw new IllegalStateException("Unable to initialize GLFW");
}

// Configure GLFW
glfwDefaultWindowHints(); // optional, the current window hints are already the default
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // <- ❗️ IMPORTANT: Set context version to 3.2
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); // <- ❗️
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);// <- ❗️ IMPORTANT: Set forward compatibility, or GLFW_INVALID_VALUE error will occur
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); // the window will stay hidden after creation
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); // the window will be resizable

// Create the window
window = glfwCreateWindow(WIDTH, HEIGHT, TITLE, NULL, NULL);
if (window == NULL) {
throw new RuntimeException("Failed to create the GLFW window");
}

// Setup a key callback. It will be called every time a key is pressed, repeated or released.
glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
glfwSetWindowShouldClose(window, true); // We will detect this in the rendering loop
}
});

// Get the thread stack and push a new frame
try (MemoryStack stack = stackPush()) {
IntBuffer pWidth = stack.mallocInt(1); // int*
IntBuffer pHeight = stack.mallocInt(1); // int*

// Get the window size passed to glfwCreateWindow
glfwGetWindowSize(window, pWidth, pHeight);

// Get the resolution of the primary monitor
GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());

// Center the window
glfwSetWindowPos(window, (vidmode.width() - pWidth.get(0)) / 2, (vidmode.height() - pHeight.get(0)) / 2
);
} // the stack frame is popped automatically

// Make the OpenGL context current
glfwMakeContextCurrent(window);
// Enable v-sync
glfwSwapInterval(1);

// Make the window visible
glfwShowWindow(window);

// This line is critical for LWJGL's interoperation with GLFW's
// OpenGL context, or any context that is managed externally.
// LWJGL detects the context that is current in the current thread,
// creates the GLCapabilities instance and makes the OpenGL
// bindings available for use.
GL.createCapabilities();
}

public static void updateDisplay() { // <- ❗️ IMPORTANT: This method is now static
glfwSwapBuffers(window); // swap the color buffers

// Poll for window events. The key callback above will only be
// invoked during this call.
glfwPollEvents();
}

public static int getWindowWidth() { // <- ❗️ IMPORTANT: Handy method to get window width
IntBuffer w = BufferUtils.createIntBuffer(1);
glfwGetWindowSize(window, w, null);
return w.get(0);
}

public static int getWindowHeight() { // <- ❗️ IMPORTANT: Handy method to get window height
IntBuffer h = BufferUtils.createIntBuffer(1);
glfwGetWindowSize(window, null, h);
return h.get(0);
}

public static void closeDisplay() { // <- ❗️ IMPORTANT: This method is now static
// Free the window callbacks and destroy the window
glfwFreeCallbacks(window);
glfwDestroyWindow(window);

// Terminate GLFW and free the error callback
glfwTerminate();
glfwSetErrorCallback(null).free();
}
}

And the while-loop part of the MainGameLoop class is now:

MainGameLoop.java

// ...
while (!glfwWindowShouldClose(DisplayManager.window)) { // <- ❗️ IMPORTANT: Use this instead of `while (!Display.isCloseRequested())`
DisplayManager.updateDisplay();
}
// ...

The invisible rectangle (T2)

Note: This section is based on Tutorial 2: VAOs and VBOs.

If you run the program on Windows, you should see a window with a rectangle box in the middle of the screen. But if you run it on macOS or linux distro, only the background is visible. The rectangle is there, but it’s invisible. This is because we haven’t added any shaders yet.

invisible rectangle

TextureLoader class not found (T6)

Note: This section is based on Tutorial 6: Texturing.

In the tutorial, Slick-util is used to load textures. But Slick-util is not working with LWJGL 3. Here we implement nikunj arora’s Texture class (which he/she posted in the comments of the tutorial) to load textures.

Texture.java

package texture;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import static org.lwjgl.opengl.GL11.*;

public class Texture {
private int width, height;
private int texture;

public Texture(String path) throws IOException {
texture = load(path);
}

private int load(String path) throws IOException {
int[] pixels = null;

BufferedImage image = ImageIO.read(new FileInputStream(path));
width = image.getWidth();
height = image.getHeight();
pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);


int[] data = new int[width * height];
for (int i = 0; i < width * height; i++) {
int a = (pixels[i] & 0xff000000) >> 24;
int r = (pixels[i] & 0xff0000) >> 16;
int g = (pixels[i] & 0xff00) >> 8;
int b = (pixels[i] & 0xff);

data[i] = a << 24 | b << 16 | g << 8 | r;
}

int result = glGenTextures();
glBindTexture(GL_TEXTURE_2D, result);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

IntBuffer buffer = ByteBuffer.allocateDirect(data.length << 2)
.order(ByteOrder.nativeOrder()).asIntBuffer();
buffer.put(data).flip();

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, buffer);
glBindTexture(GL_TEXTURE_2D, 0);
return result;
}

public void bind() {
glBindTexture(GL_TEXTURE_2D, texture);
}

public void unbind() {
glBindTexture(GL_TEXTURE_2D, 0);
}

public int getTextureID() {
return texture;
}

}

And the loadTexture method in the Loader class is now:

Loader.java

// ...
public int loadTexture(String filename) {
Texture texture;
try {
texture = new Texture("src/main/resources/texture/" + filename + ".png"); // <- ❗️ IMPORTANT: This line differs from the tutorial. Feel free to change the path.
} catch (IOException e) {
throw new RuntimeException(e);
}

int textureID = texture.getTextureID();
textures.add(textureID);
return textureID;
}
// ...

I put textures/shaders in the src/main/resources folder.

Folder structure

JOML: Vectors and matrices (T7)

Note: This section is based on Tutorial 7: Matrices & Uniform Variables.

In the tutorial, the Vector3f class is used to represent a 3D vector. But LWJGL 3 doesn’t have this class. Here we need JOML (Java OpenGL Math Library) to represent vectors and matrices and to do matrix operations.

Add JOML to the project (Maven example. If you use Gradle, you need to find the equivalent configuration):

pom.xml

<!-- ... -->
<repositories>
<repository>
<id>oss.sonatype.org</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>


<dependencies>
<dependency>
<groupId>org.joml</groupId>
<artifactId>joml</artifactId>
<version>1.10.5</version>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->

Once JOML is added, we can use it to represent vectors and matrices in a way similar to the tutorial.

The loadMatrix method in the ShaderProgram class should then be:

ShaderProgram.java

protected void loadMatrix(int location, Matrix4f matrix) {
matrix.get(matrixBuffer); // <- ❗️ Equivalent to `matrix.store(matrixBuffer)` in LWJGL 2
// matrixBuffer.flip(); // <- ❗️ IMPORTANT: This line is not needed in JOML!
GL20.glUniformMatrix4fv(location, false, matrixBuffer);
}

It is important to note that the matrixBuffer.flip() should NOT be called here, or java.lang.IndexOutOfBoundsException will be thrown.

And the Maths class is now (I changed the package name from toolbox to util because it’s closer to what I usually do):

Maths.java

package util;   // Change back to "toolbox" if you need

import org.joml.Matrix4f;
import org.joml.Vector3f;

public class Maths {
public static Matrix4f createTransformationMatrix(Vector3f transformation, float rx, float ry, float rz, float scale) {
Matrix4f matrix = new Matrix4f();
matrix.identity();
matrix.translate(transformation);
matrix.rotate((float) Math.toRadians(rx), new Vector3f(1, 0, 0));
matrix.rotate((float) Math.toRadians(ry), new Vector3f(0, 1, 0));
matrix.rotate((float) Math.toRadians(rz), new Vector3f(0, 0, 1));
matrix.scale(new Vector3f(scale, scale, scale));
return matrix;
}
}

Generally, in JOML, matrices are treated as mutable objects while in the tutorial where LWJGL 2’s matrix classes are used,

Handy perspective matrix comes with JOML (T8)

Note: This section is based on Tutorial 8: Model, View & Projection Matrices.

We can use JOML’s perspective() method of Matrix4f class to create a perspective matrix. So the createProjectionMatrix() method in the Renderer class can be simplified to:

Renderer.java

private void createProjectionMatrix() {
projectionMatrix = new Matrix4f().perspective((float) Math.toRadians(FOV), (float) DisplayManager.getWindowWidth() / (float) DisplayManager.getWindowHeight(), NEAR_PLANE, FAR_PLANE);
}

Keyboard class: Why is the camera not moving? (T8)

Note: This section is based on Tutorial 8: Model, View & Projection Matrices.

We are using GLFW to handle keyboard input instead of LWJGL 2’s Keyboard class.

In the tutorial’s comment section, 0ShinigamiDN0 provided a Keyboard class which logs key states. We can use it to check if a key is pressed or not. Note I have made some changes to the original class:

Keyboard.java

package renderEngine;

import org.lwjgl.glfw.GLFWKeyCallback;

import static org.lwjgl.glfw.GLFW.GLFW_RELEASE;

public class Keyboard extends GLFWKeyCallback {
private static boolean[] keys = new boolean[65536];

@Override
public void invoke(long window, int key, int scancode, int action, int mods) {
if (key < 0) { // <- ❗️ IMPORTANT: This line is added to prevent `ArrayIndexOutOfBoundsException` in some cases.
return;
}
keys[key] = action != GLFW_RELEASE;
}

public static boolean isKeyDown(int keycode) {
return keys[keycode];
}
}

Different from 0ShinigamiDN0’s usage, I prefer not using Keyboard keyboard = new Keyboard(); in the Camera‘s constructor. Instead, I set keyboard callback in the DisplayManager class:

DisplayManager.java

// ...
public class DisplayManager {
// Window size constants stuff...
private static Keyboard keyboard = new Keyboard(); // <- ❗️ Create a Keyboard instance
// ...

public static void createDisplay() {
// ...
window = glfwCreateWindow(DisplayManager.WIDTH, DisplayManager.HEIGHT, TITLE, NULL, NULL); // Unchanged, for reference
if ( window == NULL ) // Unchanged, for reference
throw new RuntimeException("Failed to create the GLFW window"); // Unchanged, for reference

glfwSetKeyCallback(window, new Keyboard()); // <- ❗️ Set keyboard callback here
// ...
}

// ...
}

glfwSetKeyCallback should be called after the window is created.

Now we can use Keyboard.isKeyDown() to check if a key is pressed or not. For example, in the Camera class, the move() method can be:

Camera.java

public void move() {
if (Keyboard.isKeyDown(GLFW_KEY_W) || Keyboard.isKeyDown(GLFW_KEY_UP)) {
position.z -= 0.02f;
}
if (Keyboard.isKeyDown(GLFW_KEY_S) || Keyboard.isKeyDown(GLFW_KEY_DOWN)) {
position.z += 0.02f;
}
if (Keyboard.isKeyDown(GLFW_KEY_A) || Keyboard.isKeyDown(GLFW_KEY_LEFT)) {
position.x -= 0.02f;
}
if (Keyboard.isKeyDown(GLFW_KEY_D) || Keyboard.isKeyDown(GLFW_KEY_RIGHT)) {
position.x += 0.02f;
}
if (Keyboard.isKeyDown(GLFW_KEY_SPACE)) { // Go up
position.y += 0.02f;
}
if (Keyboard.isKeyDown(GLFW_KEY_LEFT_SHIFT)) { // Go down
position.y -= 0.02f;
}
}

The code above also demonstrates how to use multiple keys to control the same action, and SPACE and L SHIFT are used to control the vertical movement.

View matrix (T8)

Note: This section is based on Tutorial 8: Model, View & Projection Matrices.

Since we are using JOML, Math.createViewMatrix(Camera camea) method is slightly different from the tutorial’s code.

Maths.java

// ...
public static Matrix4f createViewMatrix(Camera camera) {
Matrix4f viewMatrix = new Matrix4f();
viewMatrix.identity();
viewMatrix.rotate((float) Math.toRadians(camera.getPitch()), new Vector3f(1, 0, 0));
viewMatrix.rotate((float) Math.toRadians(camera.getYaw()), new Vector3f(0, 1, 0));
viewMatrix.rotate((float) Math.toRadians(camera.getRoll()), new Vector3f(0, 0, 1));
Vector3f cameraPos = camera.getPosition();
Vector3f negativeCameraPos = new Vector3f(-cameraPos.x, -cameraPos.y, -cameraPos.z);
viewMatrix.translate(negativeCameraPos);
return viewMatrix;
}

GL11, GL13, GL15, GL20..!?

Why are we invoking methods from different classes? I found this discussion (Internet Archive link) in the LWJGL forum helpful.

So I did some cleanup. Take Renderer.java as an example.

Firstly, replace the following imports (or other imports that look like this)

import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;

with

import static org.lwjgl.opengl.GL40.*;

Then, remove all GL##. prefixes from the code. For example, GL11.glClearColor(1, 0, 0, 1); becomes glClearColor(1, 0, 0, 1);.

You can use this RegEx to do the cleanup with assistance from your IDE:

(GL)\d\d\.

Replace GLxx. with empty string

Run the program and see if nothing breaks. If everything is fine, you can do the same for other classes.



That’s all for part 1 which covers episode 1 to 10. I’ll continue with part 2 which covers later episodes in this post.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
If you checked “Remember me”, your email address and name will be stored in your browser for your convenience.