Java OpenGL Renderer
We want to make a game, so we will need a way to draw 2D and 3D objects on the screen. We will do this by using the amazing Open Graphics Library (OpenGL). In this tutorial we will learn how to use OpenGL in Java create a Renderer class that can be used in our games.
Foreword
The interested reader can find in-depth knowledge about OpenGL in the Wikipedia OpenGL article.
We will have to create a Window, tell OpenGL to use it, draw on it, clear it and then draw on it again (and so on). At some point we will also have to initialize a few resources like textures and to keep our system clear, we need to free them at some point as well. To test everything, we also want to implement a function that draws a simple quad on the screen.
All those things are not completely easy to do. We want to make several games in the future, and we certainly don't want to worry about those problems every single time when we start a new game project. To solve this problem, we will create a Renderer. Our Renderer will take care of everything and it has to be re-usable over and over again in different games.
The implementation presented here is simplified as far as possible. When making more complex games, the Render code will get bigger and bigger.
A few things can seem very complicated at first, but we implemented it that way to make it as easy to use as possible. Ever heard of glOrtho and glViewport? Do you know how to switch from 3D to 2D rendering? Those things can be complicated. The good news: the Renderer will take care of all of this. We implement it once and then never worry about it again.
Warning: this tutorial is long and sometimes complicated, but you will see very soon that as result we will be able to make games very easily.**
Note: we are using the Eclipse IDE for Java Developers in this tutorial.
The goal
We want to take the effort to implement a Renderer once, and afterwards we want to be able to use it as simple as that:
public class Game extends Renderer {
public static void main(String[] args) {
// Create game instance
Game game = new Game(width, height, "WindowTitleExample");
}
// onStart will be called by the Renderer. We simply put
// our OpenGL initializations in here.
public void onStart() {
// load a texture
// load a 3d model
// etc.
}
// onUpdate will be called all the time in a loop
public void onUpdate() {
// calculate positions etc.
}
// onDraw3D will be called all the time in a loop. Everything
// that 3D has to be drawn will be drawn here.
public void onDraw3D() {
// draw the sun
// draw the ocean
// etc.
}
// onDraw2D will be called all the time in a loop. Everything
// that 2D has to be drawn will be drawn here.
public void onDraw3D() {
// draw the HUD
// etc.
}
// onEnd will be called by the Renderer. We simply free
// everything that has been initialized here.
public void onEnd() {
// free a texture
// free a 3d model
// etc.
}
}
Note how the Game class extends the Renderer class. The result is that all the rendering logic happens in the Renderer class (that we are going to create), and all we have to do to make our game is fill out onStart, onDraw2D, onDraw3D and onEnd. It's very simple, we don't have to worry about anything at all.
Note: what happens behind the scenes can seem rather complicated to beginners, that's why we want to do it only one time, put it all into our Renderer and then never think about it again. A few things in this tutorial, like abstract methods or setting up OpenGL can be a bit tricky, just read over it a few times and play around with the code to understand it completely.
OpenGL in Java with LWJGL
Let's create the Renderer. Our first problem is that we don't know how to create a OpenGL window in Java yet. Luckily someone already created a very nice library that does that, called The Lightweight Java Game Library LWJGL). All we have to do is download it from the LWJGL website, put it into our project and include it. Here is how it's done:
- Don't get scared! The following steps might seem a bit unusual at first, but it's actually really simple in compare to programming languages like C++. When doing those steps the second time, it won't even take you longer than 10 seconds.
- Download the latest LWJGL version here: LWJGL Download.
- Extract the downloaded file with the zip unpacker of choice (for example 7-zip).
- Move the LWJGL folder to our project. To keep things clean, we create a new folder called "libs" in our project and then move LWJGL in there like in the picture. If eclipse doesn't see the libs folder after it was created, simply select the project and press F5 to refresh everything.
- Include LWJGL in our project. Simply right-click the game project in Eclipse, go to Properties, Java Build Path and select the Libraries tab. Click Add JARs..., navigate to project/libslwjgl-version/jar/ and select lwjgl.jar. Repeat the process for lwjgl-util.jar.
- Native Libraries: the last thing to do is to set the Native Library path for the lwjgl.jar and lwjgl-utils.jar files that we just included. Go to the Libraries tab again, click the small arrow left of lwjgl.jar to expand the menu. You will see Native Library Location: (None) there. Simply double click it, click on Workspace and then navigate to libs/lwgj-version/native/ and then select the folder depending on your operating system. Repeat the same for lwjgl-util.jar.
Creating the Renderer Class
We want to create a Renderer class now. Right-click the project, select New -> Class and name it Renderer. It will look like this:
public class Renderer {
}
Our Renderer will need a few member variables. We have to store the width and height of the window (in case they are needed somewhere), and we need a small helper class called Interval to measure the elapsed time between frames. This is very important for framerate independent games. For example, a car in a game should always drive with the same speed, independent of how fast our computer is and how many FPS we get.
Our in-depth tutorial about Interval can be found here: Java Interval.
Now it's time to add a few member variables:
public class Renderer {
// Window Size
int m_width;
int m_height;
// Timing
Interval m_elapsed = new Interval();
public int getWidth() {
return m_width;
}
public int getHeight() {
return m_height;
}
}
Creating the Renderer Window
We need to create a window and put OpenGL onto it. LWJGL makes this very easy, here is how it's done:
public class Renderer {
// Window Size
int m_width;
int m_height;
// Timing
Interval m_elapsed = new Interval();
// Constructor
public Renderer(int width, int height, String title) {
m_width = width;
m_height = height;
// Create the Window
try {
Display.setDisplayMode(new DisplayMode(width, height));
Display.setFullscreen(false);
Display.setVSyncEnabled(true);
Display.create();
Display.setTitle(title);
} catch (LWJGLException e) {
System.out.println("LWJGLException @ Renderer start");
System.exit(0);
}
}
}
That's as easy as it gets. All we have to do is pass the width and height of the window, as well as the title and LWJGL will do the rest for us.
Initializing OpenGL
There are a few OpenGL standard values that we want to set in the beginning. Examples are the background color, the draw color or more complicated things like Depth buffer settings and Shading Models. Here are the most important OpenGL settings:
void initGL() {
// Set some default opengl settings
GL11.glShadeModel(GL11.GL_SMOOTH);
GL11.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL11.glClearDepth(1.0f);
GL11.glClearStencil(0);
GL11.glEnable(GL11.GL_DEPTH_TEST);
GL11.glDepthFunc(GL11.GL_LEQUAL);
GL11.glHint(GL11.GL_PERSPECTIVE_CORRECTION_HINT, GL11.GL_NICEST);
GL11.glDisable(GL11.GL_DITHER);
GL11.glCullFace(GL11.GL_BACK);
GL11.glEnable(GL11.GL_CULL_FACE);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glColor3f(1.0f, 1.0f, 1.0f);
}
While every single setting has a good reason behind it, it would blast the dimensions of this tutorial if we would go into detail about each one. For now we will stick with those settings and not worry about them at all.
We will call this function in the constructor as well, so our constructor now looks like this:
// Constructor
public Renderer(int width, int height, String title) {
m_width = width;
m_height = height;
// Create the Window
try {
Display.setDisplayMode(new DisplayMode(width, height));
Display.setFullscreen(false);
Display.setVSyncEnabled(true);
Display.create();
Display.setTitle(title);
} catch (LWJGLException e) {
System.out.println("LWJGLException @ Renderer start");
System.exit(0);
}
// Initialize OpenGL
initGL();
}
Implementation of the Game Loop
Now the interesting part: the Main Game Loop. The Main Game Loop does basically this:
- Start
- While Running: Update & Draw
- End
It will be implemented in the Renderer. In the Start function we will be able to initialize things like Textures, the Update function could calculate their positions, the Draw function puts them on the screen and the End function frees them again.
Here is how we will modify our Renderer Constructor to make this possible:
// Constructor
public Renderer(int width, int height, String title) {
m_width = width;
m_height = height;
// Create the Window
try {
Display.setDisplayMode(new DisplayMode(width, height));
Display.setFullscreen(false);
Display.setVSyncEnabled(true);
Display.create();
Display.setTitle(title);
} catch (LWJGLException e) {
System.out.println("LWJGLException @ Renderer start");
System.exit(0);
}
// Initialize OpenGL
initGL();
// Call onStart
onStart();
// Loop while running
while (!Display.isCloseRequested()) {
// Calculate how much time has elapsed
long dT = m_elapsed.value();
m_elapsed.reset();
// Update
onUpdate(dT);
// Draw: Clear the screen and depth buffer
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_STENCIL_BUFFER_BIT);
GL11.glLoadIdentity();
// Draw: 3D
refresh3D();
onDraw3D(dT);
// Draw: 2D
refresh2D();
onDraw2D(dT);
// Draw: update display
Display.update();
}
// Free resources in onEnd
onEnd();
// Destroy the Window
Display.destroy();
}
That's it, or almost it. We can see a few functions that simply have to be there and have to happen in order to do things right. Let's focus on the important parts: onStart(), onUpdate(), onDraw3D(), onDraw2D(), onEnd().
Here is what they do:
public abstract void onStart();
public abstract void onUpdate(long dT);
public abstract void onDraw2D(long dT);
public abstract void onDraw3D(long dT);
public abstract void onEnd();
They do nothing, exactly. Keep reading to figure out why.
Note: the dT parameter stands for delta time, which means how much time has elapsed (in milliseconds) since the last time the function was called (since the last frame).
The abstract functions onStart, onUpdate, onDraw2D, onDraw3D and onEnd
Previously we set up our Main Game Loop, but we just learned that onStart(), onUpdate(), onDraw3D(), onDraw2D() and onEnd() don't seem to do much at all, why is that?
The reason is simple: those are the functions that we will fill out in every single game. In the Renderer class they are abstract, which means that they are placeholders. Now if we create a new game, all we have to do is fill them out, that's basically it, that's how we make a game!
To make things more clear: this is what we wanted to do in the beginning, we wanted to move all the complicated OpenGL stuff into our Renderer class and then only fill out those abstract functions in every game. Scroll to the top to see it again.
About refresh2D and refresh3D
If we look at our Renderer again, we can see calls to refresh2D and refresh3D before onDraw2D and onDraw3D are being called. The reason is that in OpenGL we need to switch around certain things in order to draw in a three dimensional space or just in a two dimensional space (like the HUD on the screen). Because we don't want to memorize how to switch between 2D and 3D drawing, we put it all into two little functions and then use them in our Renderer:
void refresh2D() {
GL11.glViewport(0, 0, m_width, m_height);
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glOrtho(0.0f, m_width, 0.0f, m_height, 0.0f, 1.0f);
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glLoadIdentity();
}
void refresh3D() {
GL11.glViewport(0, 0, m_width, m_height);
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glLoadIdentity();
GLU.gluPerspective(45.0f, (float)m_width / (float)m_height, 0.001f, Float.MAX_VALUE);
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glLoadIdentity();
}
Again we can see a lot of confusing OpenGL calls. Basically all we do is adjust the viewport size and then switch to 2D mode (glOrtho) or 3D mode (gluPerspective). That's all we need to know, the details about why this works belong into an in-depth tutorial.
The basic Renderer
After all those functions, let's take a look at the big picture. The following code shows everything that we created so far:
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;
// abstract because onStart etc. are abstract
public abstract class Renderer {
// Window Size
int m_width;
int m_height;
// Timing
Interval m_elapsed = new Interval();
// Constructor
public Renderer(int width, int height, String title) {
m_width = width;
m_height = height;
// Create the Window
try {
Display.setDisplayMode(new DisplayMode(width, height));
Display.setFullscreen(false);
Display.setVSyncEnabled(true);
Display.create();
Display.setTitle(title);
} catch (LWJGLException e) {
System.out.println("LWJGLException @ Renderer start");
System.exit(0);
}
// Initialize OpenGL
initGL();
// call onStart
onStart();
// Update
while (!Display.isCloseRequested()) {
// Calculate how much time has elapsed
long dT = m_elapsed.value();
m_elapsed.reset();
// Update
onUpdate(dT);
// Draw: Clear the screen and depth buffer
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_STENCIL_BUFFER_BIT);
GL11.glLoadIdentity();
// Draw: 3D
refresh3D();
onDraw3D(dT);
// Draw: 2D
refresh2D();
onDraw2D(dT);
// Draw: update display
Display.update();
}
// free resources in onEnd
onEnd();
// destroy window
Display.destroy();
}
// Access to window width and height
public int getWidth() {
return m_width;
}
public int getHeight() {
return m_height;
}
void initGL() {
// Set some default opengl settings
GL11.glShadeModel(GL11.GL_SMOOTH);
GL11.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL11.glClearDepth(1.0f);
GL11.glClearStencil(0);
GL11.glEnable(GL11.GL_DEPTH_TEST);
GL11.glDepthFunc(GL11.GL_LEQUAL);
GL11.glHint(GL11.GL_PERSPECTIVE_CORRECTION_HINT, GL11.GL_NICEST);
GL11.glDisable(GL11.GL_DITHER);
GL11.glCullFace(GL11.GL_BACK);
GL11.glEnable(GL11.GL_CULL_FACE);
GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
GL11.glColor3f(1.0f, 1.0f, 1.0f);
}
// placeholders
public abstract void onStart();
public abstract void onUpdate(long dT);
public abstract void onDraw2D(long dT);
public abstract void onDraw3D(long dT);
public abstract void onEnd();
// refresh functions
void refresh2D() {
GL11.glViewport(0, 0, m_width, m_height);
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glOrtho(0.0f, m_width, 0.0f, m_height, 0.0f, 1.0f);
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glLoadIdentity();
}
void refresh3D() {
GL11.glViewport(0, 0, m_width, m_height);
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glLoadIdentity();
GLU.gluPerspective(45.0f, (float)m_width / (float)m_height, 0.001f, Float.MAX_VALUE);
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glLoadIdentity();
}
}
Good news: the complicated part is done. Now we have our Renderer, we have a basic idea of what it's doing and now it's time to use it.
But wait, one more thing. We will need to draw quads all the time, so why not already create a function for this and put it into our Renderer as well. It will save us a lot of work later on.
Implementation of drawQuad
If we want to draw a quad in OpenGL, all we have to do is tell OpenGL that we want to start drawing a quad, then tell it where the four points are, and then tell it to stop drawing a quad:
public void drawQuad(float x, float y, float width, float height) {
// Draws a quad like this:
//
// height
// |
// v________
// | |
// | |
// | |
// | |
// |________|<--width
// xy
//
GL11.glBegin(GL11.GL_QUADS);
GL11.glTexCoord2f(0, 0); GL11.glVertex2f(x, y);
GL11.glTexCoord2f(1, 0); GL11.glVertex2f(x + width, y);
GL11.glTexCoord2f(1, 1); GL11.glVertex2f(x + width, y + height);
GL11.glTexCoord2f(0, 1); GL11.glVertex2f(x, y + height);
GL11.glEnd();
}
This function draws a quad in the 2D dimension, so we can use it for HUDs (and more). The glVertex2f function sets the points of the quad. The glTexCoord2f function is there in case we want to put a texture on the quad, but we don't have to worry about it now. Please make sure to move the drawQuad function into the Renderer.
Usage Examples
So, we want to create a new game and draw a few things on the screen with OpenGL. The first one will be a cube (2D) and the second one will be a quad (3D). Here is a preview:
If we would start from scratch, this would be a complicated task. But luckily we already have our Renderer, so all we have to do is create a new project, copy the Renderer class into it, include LWJGL like described above, and then create the Game class. Here is how the game class looks if we want to draw only the quad (2D):
import org.lwjgl.opengl.GL11;
public class Game extends Renderer {
// Constructor that calls Renderer constructor
public Game(int width, int height, String title) {
super(width, height, title);
}
@Override
public void onStart() {
}
@Override
public void onUpdate(long dT) {
}
@Override
public void onDraw3D(long dT) {
}
@Override
public void onDraw2D(long dT) {
GL11.glColor3f(1.0f, 0.0f, 0.0f); // set color to red
drawQuad(5.0f, 5.0f, 100.0f, 80.0f); // draw quad
GL11.glColor3f(1.0f, 1.0f, 1.0f); // reset color
}
@Override
public void onEnd() {
}
public static void main(String[] args) {
// Create a game instance and start it
Game game = new Game(200, 112, "Example2D");
}
}
It's so ridiculously simple now that we have the Renderer class, I am sure your 5 year old sister could do it.
Note: @Override is not part of the code, it's just an annotation.
Let's take a look at the cube (3D) example. Since we didn't create a drawCube function in our Renderer yet, we will have to do this manually (that's why it looks so long).
import org.lwjgl.opengl.GL11;
public class Game extends Renderer {
// Constructor that calls Renderer constructor
public Game(int width, int height, String title) {
super(width, height, title);
}
@Override
public void onStart() {
}
@Override
public void onUpdate(long dT) {
}
@Override
public void onDraw3D(long dT) {
// position it in front of the screen and rotate a bit
GL11.glPushMatrix();
GL11.glTranslatef(0.0f, 0.0f, -7.0f);
GL11.glRotatef(45.0f, 1.0f, 1.0f, 0.0f);
// draw the faces of the cube
GL11.glBegin(GL11.GL_QUADS);
// green face
GL11.glColor3f(0.0f, 1.0f, 0.0f);
GL11.glVertex3f(1.0f, 1.0f, -1.0f);
GL11.glVertex3f(-1.0f, 1.0f, -1.0f);
GL11.glVertex3f(-1.0f, 1.0f, 1.0f);
GL11.glVertex3f(1.0f, 1.0f, 1.0f);
// orange face
GL11.glColor3f(1.0f, 0.5f, 0.0f);
GL11.glVertex3f(1.0f, -1.0f, 1.0f);
GL11.glVertex3f(-1.0f, -1.0f, 1.0f);
GL11.glVertex3f(-1.0f, -1.0f, -1.0f);
GL11.glVertex3f(1.0f, -1.0f, -1.0f);
// red face
GL11.glColor3f(1.0f, 0.0f, 0.0f);
GL11.glVertex3f(1.0f, 1.0f, 1.0f);
GL11.glVertex3f(-1.0f, 1.0f, 1.0f);
GL11.glVertex3f(-1.0f, -1.0f, 1.0f);
GL11.glVertex3f(1.0f, -1.0f, 1.0f);
// yellow face (back)
GL11.glColor3f(1.0f, 1.0f, 0.0f);
GL11.glVertex3f(1.0f, -1.0f, -1.0f);
GL11.glVertex3f(-1.0f, -1.0f, -1.0f);
GL11.glVertex3f(-1.0f, 1.0f, -1.0f);
GL11.glVertex3f(1.0f, 1.0f, -1.0f);
// blue face (left)
GL11.glColor3f(0.0f, 0.0f, 1.0f);
GL11.glVertex3f(-1.0f, 1.0f, 1.0f);
GL11.glVertex3f(-1.0f, 1.0f, -1.0f);
GL11.glVertex3f(-1.0f, -1.0f, -1.0f);
GL11.glVertex3f(-1.0f, -1.0f, 1.0f);
// violet face (right)
GL11.glColor3f(1.0f, 0.0f, 1.0f);
GL11.glVertex3f(1.0f, 1.0f, -1.0f);
GL11.glVertex3f(1.0f, 1.0f, 1.0f);
GL11.glVertex3f(1.0f, -1.0f, 1.0f);
GL11.glVertex3f(1.0f, -1.0f, -1.0f);
// stop drawing faces
GL11.glEnd();
// reset matrix
GL11.glPopMatrix();
// reset draw color
GL11.glColor3f(1.0f, 1.0f, 1.0f);
}
@Override
public void onDraw2D(long dT) {
}
@Override
public void onEnd() {
}
public static void main(String[] args) {
// Create a game instance and start it
Game game = new Game(200, 112, "Example3D");
}
}
Summary
We just created one of the most complicated parts of every game: the Renderer. Even though it was fairly complicated, it's very simple to use now, so it was worth the effort.
From now on, every time we create a new game we don't have to worry about anything at all. We just create the project, add our Renderer and life is good.
Please note that this Renderer implementation can be improved in countless ways. A few examples that the interested reader could implement: Fullscreen, Resolution/Windowsize difference, drawLine, drawSphere etc., checkOpenGLErrors, fixed FPS, no FPS limit, force FPS to 5 if window is not active, update in an extra Thread, ...