JOGL Tutorial 3 - Creating a Render Loop

Render Loop

Now that you can create a window, it's time to perform some actual rendering. OpenGL is typically used for real-time rendering: graphics which are repeatedly drawn on screen and interactive. This is the type of rendering used in games (excluding pre-rendered cutscenes), for example. This style of rendering contrasts offline rendering, where single images or frames are calculated over a long period of time. Game programming is actually a fantastic place to start, because the so-called "game loop" is very similar to what we would use for most graphics programs:


The above figure represents a nice design to have for a program using real-time rendering. For really simple programs, the three stages of the game loop can be condensed into a single method. However, I prefer to keep some separation between updating the "world state" of the program and the actual rendering. Here's what each stage represents in terms of a JOGL program:

Initialization

  • Choosing a GLProfile and configuring GLCapabilities for a rendering context
  • Creating a window and GLContext through the GLAutoDrawable
  • Making an animator thread
  • Loading resources needed by program

Process Input

  • Listen for mouse and keyboard events
  • Update user's view (often called a camera)

Update (Simulate Game World)

  • Calculate geometry
  • Rearrange data
  • Perform computations

Render

  • Draw scene geometry from a particular view

Shut Down

  • Save persistent data
  • Clean up resources on graphics card

Using this type of design is useful for almost any type of application, because it's flexible and probably won't cause problems later on. It is especially important to separate the update and render stages from each other. If not to adhere to better software engineering practices, consider the following advantages of separating these two stages:
  • You may want to render a scene multiple times from different views or with different effects. You do not want to repeat the update logic for each rendering. In other words, rendering should be able to reproduce the same results until the next update.
  • Keeping a consistent loop, especially when the program is receiving data in real-time, is not as simple as it might initially seem. It's also nice to have a smoother, more consistent framerate (even if it's lower) as opposed to a jittery one. 
  • You may want to allow users to scale performance by setting a target framerate without affecting the rate of computations in the update part. If the user has a poor graphics card, the rendering itself may dramatically slow down the simulation running on the CPU. It may be preferable to render after every other update, for example.

Example - Drawing a Triangle

To illustrate how to implement a render loop, we'll make a program that draws a triangle. We're going to build on top of the SimpleScene.java (AWT version) described in the previous tutorial; instead of having a blank window, the result will be a large multi-colored triangle inside the window. 


Implementing GLEventListener Interface

The first step is to make the SimpleScene class implement GLEventListener so it can listen for rendering events. Modify the class declaration as follows:

public class SimpleScene implements GLEventListener {

If you're using Eclipse, you'll notice SimpleScene is underlined in red, meaning there is an error. This is because the class doesn't implement the methods in the GLEventListener interface yet. If you hover your mouse over the underlined SimpleScene, a popup will give an option to "Add unimplemented methods", which will automatically add stubs in your class such that it conforms to the interface:


If you click the link to add the unimplemented methods, Eclipse will create the methods for you (you can delete the @Override annotations and TODO comments if they bother you):


Otherwise, you must manually type the four methods of GLEventListener inside the SimpleScene class:

public void display(GLAutoDrawable drawable) {
    // put your drawing code here
}

public void init(GLAutoDrawable drawable) {
    // put your OpenGL initialization code here
}

public void dispose(GLAutoDrawable drawable) {
    // put your cleanup code here
}

public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
    // called when user resizes the window
}

You can read the documentation on GLEventListener for more details on what these methods are used for. Having SimpleScene implement this interface allows it to render to a GLAutoDrawble; since this is an AWT example, that means the GLCanvas.

Next, we have to let the GLAutoDrawable (GLCanvas) know we want a SimpleScene object to listen for rendering events. This is easy, and takes one line of code at the end of the main method:

canvas.addGLEventListener(new SimpleScene());

Making the Triangle

At this point, your program is ready to draw something. First, split the display method into an update and render stage:

public void display(GLAutoDrawable drawable) {
    update();
    render(drawable);
}

Create the update method, which for now has nothing in it:

private void update() {
    // nothing to update yet
}

Create the render method, which will draw the triangle:

private void render(GLAutoDrawable drawable) {
    GL2 gl = drawable.getGL().getGL2();
    
    // draw a triangle filling the window
    gl.glBegin(GL.GL_TRIANGLES);
    gl.glColor3f(1, 0, 0);
    gl.glVertex2f(-1, -1);
    gl.glColor3f(0, 1, 0);
    gl.glVertex2f(0, 1);
    gl.glColor3f(0, 0, 1);
    gl.glVertex2f(1, -1);
    gl.glEnd();
}

In an OpenGL program written in C or C++, much of this render method would look the same. What's different is that we have this GL object floating around which exposes the OpenGL API. Notice that to call the glBegin, glEnd, glVertex, and glColor OpenGL functions we have to prefix them with "gl.". The "gl" object is a type of GL2, which in turn is a type of GL. Recall that we have a GLProfile specifying the OpenGL version. This program assumes that the user's computer can handle the GL2 profile, and we have to use GL2 to access the above functions (they are no longer available in GL3, for example). This GL2 object can be retrieved from the GLCanvas (our GLAutoDrawable), which is why it's a parameter for this method.

If you run your program now, you should get something like this:


There are two "problems" with the result here. The first may be obvious (or may not be, depending on your platform): the background is still garbage. It would be much better if we could set it to a solid color to admire the beautiful triangle. The second problem you probably didn't notice is that the display method is not being called repeatedly; we don't actually have a rendering loop yet.

Clearing the Color Buffer

The first problem is a result of having "garbage" in the graphics card's memory, the color buffer. A graphics card stores color values for the pixels in this buffer; when you perform drawing using OpenGL, these values are modified. That's why the area covered by the triangle looks fine - we've manually drawn to those pixels. We haven't asked OpenGL to do anything with the other pixels, however, so whatever values are stored there is anyone's guess. There is an OpenGL function glClear that will efficiently set all the pixels in this buffer to a desired color (default is black). The function can actually clear other buffers too (such as the depth buffer, which you will find out about later), but we only care about clearing colors for now. Add the following line to your render method before drawing the triangle:

gl.glClear(GL.GL_COLOR_BUFFER_BIT);


Starting the Animation Loop

JOGL provides some utility classes for animating our program. An Animator object can be created to ensure the display method of a GLAutoDrawable is repeatedly called. An FPSAnimator allows us make the framerate relatively consistent and can reduce the resource consumption of the program (if it is simple enough to run faster than the target framerate). Add the following lines to the end of the main method:

Animator animator = new FPSAnimator(canvas, 60);
animator.add(canvas);
animator.start();

The animator is attached to the GLCanvas (again, our GLAutoDrawable) and asked to render at roughly 60 frames per second. In other words, the display method will be called approximately every 17 ms (1000 / 60). The animator classes need to be imported, so add the following statement to the top of your program:

import com.jogamp.opengl.util.*;

Now our program is fulfills the rendering loop design above. Because the program is so simple, we don't have any resources to clean up in the dispose method; the window resources are automatically cleaned up for us when the program exits. However, it is important to remember that resources on the graphics card, such as texturesshaders, or vertex buffer objects, should be released. Just because we're using Java doesn't mean we can be sloppy and let garbage collection take care of everything! This isn't part of the current example, however, so the dispose method can be ignored for now.

NOTE:
If you use a regular Animator object instead of the FPSAnimator, your program will render as fast as possible. You can, however, limit the framerate of a regular Animator by asking the graphics driver to synchronize with the refresh rate of the display (v-sync). Because the target framerate is often the same as the refresh rate, which is often 60-75 or so, this method is a great choice as it lets the driver do the work of limiting frame changes. However, some Intel GPUs may ignore this setting. In the init method, active v-sync as follows:

public void init(GLAutoDrawable drawable) {
drawable.getGL().setSwapInterval(1);
}

Then, in the main method, replace the FPSAnimator with a regular Animator:

Animator animator = new Animator(canvas);

Adding Some Animation

Finally, we'll demonstrate the rendering is now animated by making the triangle bounce around inside the window. Create three fields inside the SimpleScene class:

private double theta = 0;
private double s = 0;
private double c = 0;

We'll change theta over time, and the variables s and c refer to the sine and cosine of theta. The proper place to put these calculations is in the update method:

private void update() { theta += 0.01; s = Math.sin(theta); c = Math.cos(theta); }

Now, instead of using hard-coded values for the vertices of the triangle we'll use the s and c variables which are changing over time. Replace the triangle draw code as follows:

gl.glBegin(GL.GL_TRIANGLES); gl.glColor3f(1, 0, 0); gl.glVertex2d(-c, -c); gl.glColor3f(0, 1, 0); gl.glVertex2d(0, c); gl.glColor3f(0, 0, 1); gl.glVertex2d(s, -s); gl.glEnd();


Here is the complete source for the animated triangle:

package windows;

import java.awt.Frame;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.media.opengl.*;
import javax.media.opengl.awt.GLCanvas;
import com.jogamp.opengl.util.*;

public class SimpleScene implements GLEventListener {

    private double theta = 0;
    private double s = 0;
    private double c = 0;

    public static void main(String[] args) {
        GLProfile glp = GLProfile.getDefault();
        GLCapabilities caps = new GLCapabilities(glp);
        GLCanvas canvas = new GLCanvas(caps);

        Frame frame = new Frame("AWT Window Test");
        frame.setSize(300, 300);
        frame.add(canvas);
        frame.setVisible(true);

        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });

        canvas.addGLEventListener(new SimpleScene());

        Animator animator = new FPSAnimator(canvas, 60);
        animator.add(canvas);
        animator.start();
    }

    @Override
    public void display(GLAutoDrawable drawable) {
        update();
        render(drawable);
    }

    @Override
    public void dispose(GLAutoDrawable drawable) {
    }

    @Override
    public void init(GLAutoDrawable drawable) {
    }

    @Override
    public void reshape(GLAutoDrawable drawable, int x, int y, int w, int h) {
    }

    private void update() {
        theta += 0.01;
        s = Math.sin(theta);
        c = Math.cos(theta);
    }

    private void render(GLAutoDrawable drawable) {
        GL2 gl = drawable.getGL().getGL2();

        gl.glClear(GL.GL_COLOR_BUFFER_BIT);

        // draw a triangle filling the window
        gl.glBegin(GL.GL_TRIANGLES);
        gl.glColor3f(1, 0, 0);
        gl.glVertex2d(-c, -c);
        gl.glColor3f(0, 1, 0);
        gl.glVertex2d(0, c);
        gl.glColor3f(0, 0, 1);
        gl.glVertex2d(s, -s);
        gl.glEnd();
    }
}

Comments