Skip to main content

Juggling JOGL

March 18, 2004

{cs.r.title}








Contents
Recap
Coming Clean
Get Up and Move
Transformation
Game on!

The 1980s were a time of simple-but-intense video games. There were 2D shooters like Galaga and Robotron: 2084. Their 2D graphics belie their age, but the gameplay was elegant: throw dozens or hundreds of moving objects at the player and
see what he or she can deal with. The simple movement patterns were
straightforward to implement and easy for aspiring game programmers to
grasp with just a little knowledge of high-school trigonometry.
Working in two dimensions is a good way to begin to
understand animation and affine transformations.

This article introduces the concepts in JOGL, the Java bindings to OpenGL, that are applicable to 2D gaming. We start with the handling of coordinate spaces and how
they're scaled from the OpenGL world to the screen. Then we
integrated JOGL's built-in Animator class to provide
motion to our objects. Finally, we introduce three critical affine
transformations that allow us to draw individual objects at arbitrary
locations, sizes, and rotations.

If you're following along with Prof. F. S. Hill Jr.'s Computer Graphics Using OpenGL, the theoretical basis for this article can be found in chapters 3 and 5.

Recap

We need to begin with a recap of some of the installation
and configuration issues that were at the heart of the previous
java.net JOGL article, "Jumping Into JOGL." Chief among these is the fact that as JOGL approaches a 1.0 release, milestone builds have fallen away in favor of nightly
builds. If you follow the "Precompiled binaries and documentation"
link on the JOGL home page, you'll find that the project's "Documents and files" list is empty, with just a link pointing you to "Use nightly build."

JOGL's Builds Download Page shows no release or milestone builds, just nightly builds by platform. These will generally work for everyone except
those on Mac OS X. The nightly Mac build seems to want its native
libraries to be in a Java 1.4.1 folder, a folder that gets deleted by
Apple's Java 1.4.2 installer. Assuming you're running 1.4.2 on Mac OS
X 10.3 ("Panther"), the file you want is a special build contributed by
Gregory Pierce and available on the old project downloads page.

In any case, the download should simply contain a
jogl.jar file, which can go anywhere in your classpath,
and one or two native library files (name and extension vary by
platform) that need to be along the java.library.path
(see the previous article for more on this).

Coming Clean

Back in the first article, a few calls to set up a JOGL drawing
space were taken on faith. It's time to explain what was really going
on with those calls, because this article will begin using non-trivial
values for them.

Recall that a JOGL drawing surface, a GLCanvas, is
retrieved via a factory method that passes in an object representing
the characteristics of the display. This GLCanvas is not
drawn to directly. Instead, we register for events via a
GLEventListener interface. This interface requires we
handle four calls:

  • init()
  • reshape()
  • display()
  • displayChanged()

Each of these passes in, as a GLDrawable, the surface we
are to draw on, by way of getting the GL and
GLU pseudo-objects. ("Pseudo" because they're meaningless
as objects and are instead a very simple mapping to the thousands of
functions in the gl.h and glu.h native
header files.)

In the previous article, our very simple reshape()
implementation obscured two very important facts about
the OpenGL world as it relates to the screen:

  1. OpenGL coordinates don't necessarily map one-to-one with on-screen pixel coordinates.
  2. OpenGL calls are passed through a rendering pipeline that, through a series of affine transformations, allow us to rotate, scale, and translate (move) the entire co-ordinate system.

In OpenGL, the "world" is viewed in Cartesian coordinates, with X
values increasing as we go right, and Y values increasing as we go up
(there is also a Z axis in 3D, but that's for next time). We define a
"world window" as the portion of this world that we want to render to
the screen. This is done with the call glOrtho2D() in the
GLU class. In the previous article, we forced this to be
a size appropriate to counting by on-screen pixels, but this is
neither required nor desirable: OpenGL will scale the contents of the
world window to our on-screen display, called the viewport. The
viewport is set with the glViewport() method in the
GL class. Note that, curiously,
glOrtho2D()'s arguments are left,
right, bottom, and top, while
glViewport() takes left, right,
width, and height.

To exercise this mapping, please check out the ballbounce.tar.gz sample code that accompanies this article. This application provides a "bouncing ball" demo to show off animation, scaling, and affine transforms. The application consists of three classes:

  1. BouncingBall, which represents the location and movement of a ball.
  2. BallBouncer, which manages the movement of the balls inside of a 2D box.
  3. BallBouncerFrame, which presents the BallBouncer in a AWT Frame, with some
    widgets to control its behavior.

When run via java BallBouncerFrame, the application looks like Figure 1.

Figure 1
Figure 1. Default BallBouncerFrame

There's nothing particularly interesting unless you notice the black
box that has been drawn just inside the panel. The
BallBouncer creates a box that is just a bit smaller than
the original world window size passed to it, and this box is always
drawn as the first step of our display():

// load identity matrix
gl.glMatrixMode (GL.GL_MODELVIEW);
gl.glLoadIdentity();

// clear screen
gl.glClear (GL.GL_COLOR_BUFFER_BIT);

// draw the wallInterior
gl.glColor3f( 0.0f, 0.0f, 0.0f );
gl.glBegin (GL.GL_LINE_LOOP);
gl.glVertex2i (wallInterior.x,
               wallInterior.y);
gl.glVertex2i (wallInterior.x + wallInterior.width,
               wallInterior.y);
gl.glVertex2i (wallInterior.x + wallInterior.width,
               wallInterior.y + wallInterior.height);
gl.glVertex2i (wallInterior.x,
               wallInterior.y + wallInterior.height);
gl.glEnd();

Notice the bit about load identity matrix. That will be
important later.

You might be saying, "Wow, it's a box. Big deal."
Well, it's bigger
than you might think. That box has a lower left corner at (5, 5) and
the sides are 2950 long. But the on-screen panel comes up at 300 by
300. What you're seeing is OpenGL scaling at work. The world window
is set to a rectangle at (0, 0) with sides 3000 long, but the viewport
is always set to be the size of the panel (initially forced to 300 by
300). OpenGL scales the contents of world window to fit in the
viewport.

To exercise this, try using the "View" pop-up. Changing the view
causes us to use a different world window. Smaller values aren't
interesting at this point, but Figure 2 shows what happens when we
expand to 4000 by 4000 or 5000 by 5000.

Figure 2
Figure 2. Zooming out BallBouncerFrame

Remember, the box is still being drawn at the same coordinates:
(5,5) to (2995, 5) to (2995, 2995) to (5, 2995) It's just that using
the "View" choice results in a new call to gluOrtho2d to
reset the world window. And now that our
world window is looking at a larger area, the on-screen box is
smaller.

This happens in our resetWorldWindow() method, called
either on a reshape() or when the user changes
his or her preferred view. Our implementation changes the world window
but keeps the viewport as the size of the on-screen component:

gl.glMatrixMode( GL.GL_PROJECTION );  
gl.glLoadIdentity();
glu.gluOrtho2D (worldWindowRect.x,
                worldWindowRect.x + worldWindowRect.width,
                worldWindowRect.y,
                worldWindowRect.y + worldWindowRect.height);
gl.glViewport( 0, 0, viewportWidth, viewportHeight );

Now we've mostly dispelled the mystery of the "trust me" code from
the first article, since we've covered the difference between
gluOrtho2D(), which establishes the world window, and
glViewport(), which sets up the viewport. But what's
glLoadIdentity()? An explanation for that will come
later.







Get Up and Move

Let's make this more interesting by putting something in the box,
like an arbitrary number of bouncing balls. We need to do three
things: have a concept of where the balls are and where they're going,
have some code to actually recalculate their positions over time (also
handling collisions with the walls of the box), and have a thread
repaint the balls as often as possible, creating the illusion of motion
(i.e., animation).

BouncingBall provides the first of these. It uses
an AWT Rectangle to represent both the location of a ball
and its size. For motion, we use two doubles to
represent the number of coordinates travelled along the X and Y axes
in one second. Taken together, these two doubles, xv and
yv represent a motion vector (in the mathematical sense
of a "vector," not related in any way to a java.util.Vector). Vectors
have all kinds of convenient properties in 2D graphics, but for our
current purposes, we'll only be concerned with them as X and Y
displacement over time.

The BallBouncer class is responsible for moving these
balls: it needs to periodically consider how much time has passed
since the last update, divide the X and Y components of the motion
vector by that time to get the distance the ball has travelled in each
dimension, and update the ball's position. If a ball hits a wall, its
motion vector needs to be adjusted accordingly -- for this
article, we use an extremely simplistic reversal of X or Y velocity
on an impact. For a consideration of more realistic forces (such as
acceleration, friction, spin, etc.), please see David M. Bourg's
Physics for Game Developers.

In the old days, we would typically have a single loop responsible
for handling user input, game logic (such as calculating object
positions, which we are interested in here), and repainting the screen
as often as possible. But this approach has apparently fallen out of
favor somewhat. Game designer Mike Stemmle, who co-designed and co-directed Escape from Monkey Island and the recently (and in my opinion, inexplicably) cancelled Sam and Max: Freelance Police, explains it this way: "Given the power of most
machines these days, we can afford to be more anal about separating
game logic from graphics code." Stemmle explains that the current
practice is to not just separate the code for these functions, but to have them in different
threads, to completely separate the tasks.

In this spirit of this arrangement, BallBouncer does
its updates on the AWT event-dispatch thread, by way of
javax.swing.Timer callbacks. This also ensures that
changes made via the user interface will be thread-safe.

But what about the painting? For that, JOGL provides a class
called Animator that has its own thread to repeatedly
call the display() method for a GLDisplayable, such as our GLCanvas, as
often as the CPU will allow. If that sounds like overkill, check this JOGL forum post for FPSAnimator, which optionally
limits the display() callbacks to a specified rate.

Creating and starting the Animator is simple:
construct it with a GLDisplayable argument, then
start() it. In our example, the code looks like
this:

Animator animator =
    new Animator(bbf.ballBouncer.getCanvas());
animator.start();

One note on the care and feeding of the Animator: the
GLDisplayable apparently needs to be on-screen and fully
initialized before handing it to an Animator. I even
found an occasional not-quite-initialized exception that I worked
around by stalling for a second between the end of my GUI setup and
starting the Animator.

At any rate, having established how the logical animation of the
balls' positions and the Animator-driven painting works,
feel free to exercise it by setting the number of balls to some value
greater than zero. The bouncing balls are shown in Figure 3.

Figure 3
Figure 3. BallBouncer animation

If you think that the use of two threads, one for motion logic and
another for painting, is a potential thread-safety risk ... you're
absolutely right. It's possible to change the number of balls to
paint while the display() method is counting through the
list, resulting in an ArrayIndexOutOfBounds exception,
although the painting is so fast you'll need thousands or tens of
thousands of balls to get into this state. Fixing this is left as an intellectual exercise for the reader. There are a
few interesting options, and choosing one may depend on your
development philosophy and priorities:

  • synchronize access to the ArrayList so
    that only one thread can read or write it at a time.
    Straightforward, but thread synchronization is expensive in some
    JVMs.

  • Set up "gate" logic so that display() doesn't try to
    draw unless the ball positions have updated. This way,
    changes in the update logic can't be concurrent with
    display(). This is a nice optimization, too.

By the way, the last article didn't note anything about drawing
curved surfaces, so how are the balls drawn? Well, they're not
actually curved: it's fast and cheap to just use regular polygons, so
these "balls" are actually 36-sided regular polygons, with each vertex
on a circle that fits neatly within the Rectangle that
describes the ball's position and size. Given a radius
of half the Rectangle's width and a center at
(cx, cy), the "circle" is drawn by:

gl.glBegin (GL.GL_POLYGON);       
for (double theta = 0; theta < TWO_PI; theta += ARC_SEGMENT) {
    int x = (int) (cx + (Math.sin(theta) * radius));
    int y = (int) (cy + (Math.cos(theta) * radius));
    gl.glVertex2i(x,y);
}
gl.glEnd();

Since we're already scaling down coordinates (the rectangles are
150 by 150), the angularity disappears as a side effect of the scaling.

Transformation

One valid complaint about the use of circles is that they're terribly
easy: they don't point in a specific direction and it's easy to draw
them larger or smaller by just changing the radius. An arbitrary
shape would be a lot harder.

Fortunately, our drawing in OpenGL can benefit by using affine
transformations
. If you've worked with Java 2D Graphics or
similar graphics APIs (like the QuickDraw package exposed by Apple's
QuickTime Java), then you may have encountered these before. They are
algorithms for transforming points from one coordinate space into
another, often expressed as matrix multiplication.
They have the nice feature that they can be combined. But
what do they do? Here are three that are interesting in 2D:

  • Translation: Move the coordinate system so that drawing occurs in a different location.
  • Rotation: Rotate the coordinate system (specifically, rotate the X/Y plane about the Z axis).
  • Scaling: Recalibrate the coordinate system so that the units represent a different amount of distance.

To exercise the affine transforms, BallBouncer can
replace the balls with arrows that point in the direction in which the ball is
moving. We'll use this to exercise the rotation transformation.
Since the center of the rotation is the origin of the coordinate
system, we'll use translation to move the arrow to its proper
position. And just to show off, we'll draw the arrow at an arbitrary
size, 100 by 100, which doesn't match the size of the balls'
rectangles; the scaling transformation will fix this.

Figure 4 shows how the three transforms will achieve the desired
effect.

Figure 4
Figure 4. The effect of three affine transformations

Here is the code for drawBallAsArrow() that exploits
the affine transformations:

Rectangle rect = ball.getRect();
gl.glMatrixMode (GL.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glTranslated (rect.x+50, rect.y+50, 0);
gl.glScaled (rect.width/100d, rect.height/100d, 0);
gl.glRotated (ball.getAngle(), 0, 0, 1);
gl.glBegin (GL.GL_POLYGON);
gl.glVertex2i (50, 0);
gl.glVertex2i (-50, 50);
gl.glVertex2i (0, 0);
gl.glVertex2i (-50, -50);
gl.glEnd();

The affine transform calls are:

  • glLoadIdentity()
    Resets the affine transforms to the "identity" matrix, in which every transformation returns the
    original point. This clears out any previous transforms we sent to
    OpenGL. Remember from above that this is the last unexplained method
    call in our reshape() and/or
    resetWorldWindow() implementations? Now you know why we
    needed it: we needed to reset our drawing to work with the original,
    non-transformed coordinates.

  • glTranslated()
    Adjusts the coordinate system to move the arrow to the location of the ball, plus 50 pixels right and
    up, since our drawing code uses an arrow whose rectangle's lower left
    corner is at (-50, -50). Notice the trailing 0
    argument: this is our Z axis rotation, and we don't care about the
    Z axis because this is 2D.

  • glScaled()
    Scales our 100 by 100 arrow up to a size appropriate to the ball's Rectangle. Again, the last
    argument refers to the unused third dimension.

  • glRotated()
    Rotates our coordinate system by the number of degrees calculated by the ball the last time we set its
    movement. The (0,0,1) arguments define the Z axis, which
    is what we rotate about.

Finally, having defined this set of transformations, drawing our arrow
requires only that we enter polygon-drawing mode and specify our four points.

To show this off, click the "Draw as arrows" checkbox. Figure 5
shows the full effect of our OpenGL bag of tricks: arrows to exhibit
affine transformations, a zoom to display the effect of setting the
world window's size, and the animation provided by the
Animator class.

Figure 5
Figure 5. BallBouncerFrame in "Draw as arrows" mode

Game on!

Thinking back to the great video games of the 80s discussed in the
introduction, you may already see plenty of similarities. These games
were all about straight-line motion: missiles, bombs, flying ships,
rolling asteroids, etc., all of which can be modeled and animated with
the simple techniques shown here.

But what about interaction? How do we know when two objects, like
a missile and a robot, collide? This is one of the reasons I used AWT
Rectangles to represent the moving objects: the method
Rectangle.intersects(Rectangle r) makes checking for
collisions trivial. It would be simple to extend the method that
updates the ball positions to also do collision checks and take an
appropriate action (bounce the balls, destroy one, etc.). Throw in
player control of one or more objects, and you've got the beginnings of
a high-performance 2D game.

Chris Adamson is the former editor of java.net.
Related Topics >> GUI   |   Programming   |