-
Notifications
You must be signed in to change notification settings - Fork 36
2.6.6 LWJGL3 migration
#LWJGL3 Migration Guide This guide is designed to take you through the process of migrating an application from LWJGL2 over to LWJGL3. In order to do this, I have written a simple Pong game in LWJGL2 (2.9.3) and this guide will be written as I migrate it to LWJGL3 (3.0.0b Build #64 - latest stable upon writing). The guide will be split up into commits, one commit for each feature I migrate to LWJGL3 and you can see exactly what I changed by looking at the commits' diff on the Github repository here: Pong Migration Github Repository. The repository contains the original game in the "PongLWJGL2" folder and the "PongLWJGL3" folder contains the updated version.
Before you read this guide, if you wish to use the stability (not feature) improvements of LWJGL3 but do not wish to migrate your game then there is an alternative. Kappa has written a compatibility layer called LWJGLX, see the forum post here. ##Commit 1 - The Switch This commit just switches the natives and libraries to use LWJGL3, nothing else. As you might expect, it does not work. ##Commit 2 - OpenGL command syntax One of the more minor changes introduced with LWJGL3 is a change to the OpenGL syntax to the actual syntax you would find in the C API. As it happens, the only change required in the example program is to change all calls of:
glUniformMatrix4(int location, boolean transpose, FloatBuffer values);
to:
glUniformMatrix4fv(int location, boolean transpose, FloatBuffer values);
The logic behind the LWJGL2 syntax being: it's implied that it is a float matrix since you are passing a FloatBuffer. LWJGL3 returns to the original API in an effort to be more compatible with other sources of information, particularly the official reference.
As a general rule you will be adding 'f's and 'i's and 'fv's and the like to command calls. Should not cause too much of a problem and will probably be fairly clear what to replace things with.
The next commit will cover the biggest change of all - moving over from LWJGL2's own windowing system to GLFW used by LWJGL3. ##Commit 3 - Window/OpenGL Context Creation I think there are three important things to know when it comes to creating a window and an OpenGL context in LWJGL3 (ie. GLFW, the windowing library LWJGL uses).
- GLFW can manage multiple windows and, much like the way OpenGL manages it's objects, they are referenced by a handle which is passed to functions which manipulate that window. LWJGL3's binding uses "long" variables to store this handle.
- Before creating a window, you specify all of the attributes you would like it to have via window hints. All passed using a single function:
glfwWindowHint()
- Since you may be dealing with multiple windows each potentially with their own OpenGL context, you must tell LWJGL3 what context you are using after creating it.
Before we do anything in GLFW, we must initialize GLFW using glfwInit()
, this has an accompanying function to terminate GLFW, realing it's resources glfwTerminate()
. In Pong these are added to the start and end of the init()
and deinit()
functions respectively.
Then it is a good idea to make GLFW print it's errors to the console which it does not do by default, but this is simple and LWJGL provides a utility function to create an appropriate callback (more on callbacks later) in the org.lwjgl.glfw.GLFWErrorCallback class.
glfwSetErrorCallback(errorCallback = GLFWErrorCallback.createPrint(System.err));
This will work perfectly but errorCallback
MUST be a field and NOT a local variable (remember more on callbacks later).
Now to specify the properties of the window. Step by step in Pong: the title is passed as a parameter to the glfwCreateWindow()
function, the resizeable property is set with the GLFW_RESIZABLE
flag, the requested OpenGL version is set with the GLFW_CONTEXT_VERSION_MAJOR
and GLFW_CONTEXT_VERSION_MINOR
flags, the requested profile is set with the GLFW_OPENGL_PROFILE
flag and fullscreen we will not be dealing with just yet.
Finally we must create the window with glfwCreateWindow()
remembering that we must keep the handle that the function returns as this is our window.
In old LWJGL2
Display.setTitle("Pong - LWJGL2");
Display.setResizable(true);
Display.create(
new PixelFormat(),
new ContextAttribs(3, 3).withProfileCore(true)
);
In new LWJGL3
glfwWindowHint(GLFW_RESIZABLE, GL_TRUE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Pong - LWJGL3", 0, 0);
The "0"s in the glfwCreateWindow()
call are to do with fullscreen and shared OpenGL contexts, if you don't care they can be ignored but they shall be explained later. More info on these functions with the official docs: glfwWindowHint(), glfwCreateWindow(). You will also notice that LWJGLException is now never thrown in the code (and has in fact been removed in LWJGL3) so all those throws clauses in Pong can be removed.
This is not all however as we still have check that the window was created successfully, tell LWJGL to use this window's OpenGL context and make the window visible (since GLFW window's are hidden by default).
Now glfwCreateWindow()
will return a '0', representing a NULL
value if it fails for any reason. So a simple check is added to Pong, your error callback should catch the reason if one is forthcoming:
if(window == 0) {
throw new RuntimeException("Failed to create window");
}
Now we must set this window's context as the current context on this thread by calling glfwMakeContextCurrent()
with our window's handle as the parameter and finally call GL.createCapabilities()
to make LWJGL use the current (our window's) context. Read more about contexts [here](https://www.opengl.org/wiki/OpenGL_Context"OpenGL Contexts") and here's the documentation on glfwMakeContextCurrent().
glfwMakeContextCurrent(window);
GL.createCapabilities();
And finally make the window visible with glfwShowWindow()
. Documentation here: glfwShowWindow().
glfwShowWindow(window);
After this you will have a working, visible window that can be rendered into so lets update the uses of the window. Most of the functions, ignoring input and resizing handling code, have direct analogues so this shouldn't take long.
First of all destroying the window once you are done with it. The equivalent of Display.destroy()
is glfwDestroyWindow()
, documentation: glfwDestroyWindow().
glfwDestroyWindow(window);
Now Display.isCloseRequested()
becomes glfwWindowShouldClose()
but this is a C API so it will not return a boolean but rather an integer representing a boolean, check it out here: glfwWindowShouldClose.
while(glfwWindowShouldClose(window) == GL_FALSE &&
remainOpen) {
...
}
And lastly Display.update()
does not have a direct analogue mainly because it does two things: swaps the buffers and polls the input devices. In GLFW you must use two functions, glfwPollEvents()
and glfwSwapBuffers()
, more info here glfwPollEvents() and here glfwSwapBuffers().
glfwPollEvents();
glfwSwapBuffers(window);
At this point if you comment out all the contents of update(), setDisplayMode() and have currentTimeMillis() return 0, Pong will run and will show a resizeable, closable window of the right size. Unfortunately it will just be blank, not because the rendering isn't working but because since we are not dealing with resize events, the projection matrix is all wrong.
This is what we will tackle next. ##Commit 4 - Framebuffer Resize Handling Before continuing I want to make an important distinction between the framebuffer and the window. The framebuffer is the "canvas" of OpenGL - an array of pixels which can be rendered into. The window is what contains the framebuffer and it can generally be moved around and minimized and all sorts. Generally resizing the window will resize the framebuffer but they are two different events and GLFW will let you handle them independently. Here we only focus on framebuffer resize events. Read more about framebuffers here.
To handle framebuffer resize events, and indeed most events, GLFW uses callbacks. In fact we've already used a callback to handle GLFW errors. Now callbacks are essentially another name for listeners, which we use in Swing, JavaFX and pretty much most things. LWJGL3 provides abstract classes to override for all the various GLFW callbacks and even interfaces to use as functional interfaces if you are in to Java8 lambdas, in this case org.lwjgl.glfw.GLFWFramebufferSizeCallback and the functional interface: org.lwjgl.glfw.GLFWFramebufferSizeCallback.SAM.
One thing is absolutely crucial with GLFW callbacks ALWAYS KEEP A STRONG REFERENCE TO THE CALLBACK. Otherwise the callback will be garbage collected which will cause quite a tricky to debug error.
Now the function to set a framebuffer resize callback is glfwSetFramebufferSizeCallback()
info here: glfwSetFramebufferSizeCallback().
In LWJGL2 we had this code in the update() method. Notice how we had to have an extra flag to listen out for fullscreen changes since there was only a default window resize flag:
if(Display.wasResized() || goneFullscreen) {
//Reset goneFullscreen flag.
goneFullscreen = false;
onResize(Display.getWidth(), Display.getHeight());
}
In LWJGL3 with we can get rid of the goneFullscreen
flag and the whole thing is a lot simpler. We have this in the initialization code:
glfwSetFramebufferSizeCallback(window, (framebufferSizeCallback = new GLFWFramebufferSizeCallback() {
@Override
public void invoke(long window, int width, int height) {
onResize(width, height);
}
}));
onResize(WINDOW_WIDTH, WINDOW_HEIGHT);
We call onResize() after setting the framebuffer callback to initialize the projection matrix to it's initial value.
At this point Pong will run, it will create a working window and it will render all the various elements of Pong into this window. What it will not do is respond to input or update any of these elements. This would be the next commit but before we can handle input we need to update our time setting code.
##Commit 5 - Getting the Time
LWJGL2 gave us two functions with which to get a precise time, Sys.getTime()
and Sys.getTimerResolution()
. The latter was required because the former would work in a different unit depending on the platform so that you got the best resolution and precision. The fact is that this functionality was written at a time when the JRE equivalents just weren't up to scratch which is no longer the case so these can be replaced with the functions found in java.lang.System.
However, GLFW also provides an alternative that we shall use here: glfwGetTime()
returns the time in seconds, details here. In Pong we must convert to using double
values to store the current time with the precision this method can provide, currentTimeMillis
therefore becomes:
public static double currentTimeMillis() {
return GLFW.glfwGetTime() * 1000;
}
##Commit 6 - Keyboard Input In Pong and in general, we get input from the keyboard in two ways. Firstly we query the keyboard for it's current state, finding out if a particular key is up or down at that time, second we act on keyboard events that our input system informs us of.
The first method is the simplest and that is what we will look at first. So in LWJGL2 we had Keyboard.isKeyDown()
. In GLFW we have glfwGetKey()
which instead of a boolean will return either GLFW_PRESS
or GLFW_RELEASE
. So in Pong:
updatePaddle(
paddle1, delta,
glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS
);
updatePaddle(
paddle2, delta,
glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS
);
Now onto handling keyboard events and here GLFW uses callbacks. The function this time is glfwSetKeyCallback(), the LWJGL abstract class is org.lwjgl.glfw.GLFWKeyCallback and the functional interface is org.lwjgl.glfw.GLFWKeyCallback.SAM. As ever with callbacks you must REMEMBER TO KEEP A STRONG REFERENCE TO THE CALLBACK. So in Pong we must add this to our init()
function.
glfwSetKeyCallback(window, (keyCallback = new GLFWKeyCallback() {
@Override
public void invoke(long window, int key, int scancode, int action, int mods) {
if(key == GLFW_KEY_SPACE && action == GLFW_RELEASE) {
onPlayPauseToggle();
} else if(key == GLFW_KEY_F5 && action == GLFW_RELEASE) {
setDisplayMode(true);
} else if(key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
//Request close.
remainOpen = false;
}
}
}));
A full description of the parameters of the callback's invoke()
method can be found in the linked description but an important one to note is mods
, a bitfield describing the modifier keys enabled at the time of this event. You will find this very useful, be aware of it. Also worthy of note is that the callback can recieve GLFW_REPEAT
events as well GLFW_PRESS
and GLFW_RELEASE
events.
If you uncomment the ball update code in update()
at this point you will have a fully working game of Pong, albeit missing some of the features of the original. Next commit we will replace the mouse input code.
##Commit 7 - Mouse Input
Before we start, there is one important to difference to note with GLFW compared to LWJGL2. In LWLJGL2, when it came to the cursor position, the bottom left corner was taken to be the origin as with most OpenGL applications. GLFW takes the origin to be the top right corner, as with most GUI frameworks. In Pong this will require a small modification.
As with the Keyboard input, we have to handle our mouse querying and our mouse events and the API follows exactly the same pattern as the keyboard input. However, the easiest way to get the cursor position in GLFW is to use the cursor position callback, glfwSetCursorPosCallback(), org.lwjgl.glfw.GLFWCursorPosCallback. As ever remember to KEEP A STRONG REFERENCE TO THE CALLBACK.
In this snippet cursorPos
is just a field storing the current cursor's x and y position. We subtract the given y position from the framebuffer height to account for GLFW taking the top left corner as the origin when we expect it to be the bottom left.
glfwSetCursorPosCallback(window, (cursorPosCallback = new GLFWCursorPosCallback() {
@Override
public void invoke(long window, double xpos, double ypos) {
cursorPos.x = xpos;
cursorPos.y = framebuffer.height - ypos;
}
}));
One thing to note is that GLFW uses double
s since it supports subpixel accuracy if the platform does. It does mean however that some of the update code has to be updated to use double
s. Now onto the actual updating code.
updateNewBall(Mouse.getX(), Mouse.getY());
becomes:
updateNewBall(cursorPos.x, cursorPos.y);
And the looping through mouse events in update()
becomes:
glfwSetMouseButtonCallback(window, (mouseButtonCallback = new GLFWMouseButtonCallback() {
@Override
public void invoke(long window, int button, int action, int mods) {
if(button == 0) {
if(action == GLFW_PRESS && addBall == null) {
onNewBall(cursorPos.x, cursorPos.y);
} else if(action == GLFW_RELEASE && addBall != null) {
onNewBallRelease(cursorPos.x, cursorPos.y);
}
}
}
}));
And with this change, we have a fully functioning game of Pong. The only thing missing that we had before is the fullscreen capability which will be our next commit. ##Commit 8 - Fullscreen The first thing to say is that in moving to GLFW, we will not be able to implement fullscreen switching since, since as of writing this GLFW does not support fullscreen switching. It is possible to do but involves creating a new window and trying to switch between them seamlessly.
The way we specify fullscreen mode is to pass a non null (non zero) monitor to glfwCreateWindow()
. This argument tells GLFW which monitor to place the fullscreen application over. When you pass a non-null monitor, the width and height become the requested resolution which GLFW will do it's best to comply with. If we want the desktop resolution (which is generally a good idea for smooth start up and performance).
We can get the primary monitor using glfwGetPrimaryMonitor()
(documentation) which returns a handle to the monitor. Choosing a monitor that is not the primary monitor is quite tricky and if you really need that the best option is to let the user pick from a list.
Once we have the monitor, we need the desktop resolution which we can get by querying the current resolution of the monitor. GLFW stores this information in a video mode struct, GLFWVidMode retrievable with glfwGetVideoMode.
monitor = glfwGetPrimaryMonitor();
GLFWVidMode vidMode = glfwGetVideoMode(monitor);
width = vidMode.width();
height = vidMode.height();
Now we have the monitor and the desktop resolution we can create the window. So our old
window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Pong - LWJGL3", 0, 0);
becomes
int windowWidth = WINDOW_WIDTH;
int windowHeight = WINDOW_HEIGHT;
long monitor = 0;
if(START_FULLSCREEN) {
monitor = glfwGetPrimaryMonitor();
Retrieve the desktop resolution
GLFWVidMode vidMode = glfwGetVideoMode(monitor);
windowWidth = vidMode.width();
windowHeight = vidMode.height();
}
window = glfwCreateWindow(windowWidth, windowHeight, "Pong - LWJGL3", monitor, 0);
This will either create a fullscreen window with the desktop resolution or a normal window of size WINDOW_WIDTH
x WINDOW_HEIGHT
based on the value of START_FULSCREEN
.
But there is one more thing to do in Pong - when we initialize our projection matrix with the first call to
onResize(WINDOW_WIDTH, WINDOW_HEIGHT
we don't take into account that the framebuffer will not be that size if we are in fullscreen mode. We must retrieve the framebuffer size using glfwGetFramebufferSize()
(documentation). The LWJGL3 binding of this is a little different however since we don't have pointers. We use java.nio.Buffer
s, in this case IntBuffer
s to give the function somewhere to put the return values and then retrieve the values from the buffers. Hence:
IntBuffer framebufferWidth = BufferUtils.createIntBuffer(1),
framebufferHeight = BufferUtils.createIntBuffer(1);
glfwGetFramebufferSize(window, framebufferWidth, framebufferHeight);
onResize(framebufferWidth.get(), framebufferHeight.get());
Pong will now render correctly and is fully functional.