Picking
Click your mouse on the display window to select a 3D object. It sounds easy, but imagine trying to make it work. Your mouse click happens at some integer (x,y) coordinate on the screen, but the object is being projected through a bunch of floating-point matrix operations to its spot on the screen. You have to think of your mouse click shooting an arrow into the "scene" to see which object it hits. Down deep inside, somebody has to figure out which polygon in the scene intersects with the arrow you shot, and then determine which "object" owns that polygon. Java3D has a solution for this, but JOGL leaves it up to you.
I must confess that laziness overwhelmed me here, so I took advantage of the simplicity of my model. I sidestepped the idea of finding a polygon intersection point, because I had no idea how to do it. Rather, I just shoot the mouse-click arrow into the scene looking for the closest midpoints of intervals (just 3D points, not polygons!) and then favoring the closer ones over the further ones. Even my lazy approach wasn't trivial, however! In my code, the picking trick is in JoglFabricView.display(), using special matrix calculations.
The Matrix
Just like Neo in
the movie ,
you have to master the matrix if you want to make it work for you. A matrix is like a lens that transforms one universe of coordinates to another one in very specific ways. It can translate things from one position to another, rotate things around the origin, and scale things wider or narrower in different directions. OpenGL lets you perform these operations individually, with each one actually affecting the current matrix in the graphics card.
In Fluidiom, I display hundreds of intervals, all moving at the same time from frame to frame, and this is all done with hundreds of different matrix operations performed on what is effectively a solitary graphics object. One graphical object is displayed in many different ways, or viewed through different matrix lenses, to make up one image. It's a very economical way to work, since single objects are reused, and this kind of activity is behind most 3D stuff you see.
Here's the code that displays an interval, for example:
// preserve the current matrix for later
theGL.glPushMatrix();
// throw the object out into space
theGL.glTranslatef(
intervalLocation.x,
intervalLocation.y,
intervalLocation.z
);
// twirl it around to the right angle
theGL.glRotatef(
RADIANS_TO_DEGREES*
(float)Math.acos(intervalDirection.z),
-intervalDirection.y, intervalDirection.x, 0
);
// stretch it out along the z-axis
theGL.glScalef(
theIntervalRadius,
theIntervalRadius,
span/2
);
// paint the object
theGL.glCallList(shape);
// restore the preserved matrix
theGL.glPopMatrix();
Pay close attention to the ordering, because it might trick you. You translate first, then rotate, then scale. What you're actually doing is setting up a single new matrix, and the operations are actually happening in the opposite order. First, we scale the object (here, it gets oblong, because it's supposed to represent a springy interval) then rotate it (trigonometry anyone?), and finally, flick it out into space at the desired location.
You can see that here, too, you are responsible for telling the graphics card to save its current matrix before you mangle it, and then telling it to restore the previous one when you're finished. The only non-matrix call here is the glCallList(), which brings us to the next topic.
OpenGL Lists and OOP
OpenGL is not an object-oriented universe (it's really just a flat static C API), and JOGL doesn't do a lot more than represent it as some colossal Java classes with kerjillions of static (native) method calls. (The classes are so large, by the way, that they hang my IDE if I ask for code completion!) That is, of course, not to say that you can't write tidy object-oriented code against this API. You just have to imagine a bridge between the CPU and the GPU, and imagine your classes as having part of their state living "over there."
In OpenGL, you can create what is called a list, which effectively represents a long series of OpenGL commands, or rather, the result of these commands: a graphical object. All you get back to identify the graphical object is an integer, so you use that integer to reach across the CPU-GPU bridge and paint the object with glCallList(shape). In your tidy object-oriented code, the Java-side proxy for the graphical object contains the shape integer as part of its state.
For one Fluidiom rendering I wanted blimp-like oblong ellipsoids, so I created a list that contains a sphere. The matrix operations in the above code snippet stretch it, twirl it, and toss it out into space before anybody ever sees the sphere.
In true object-oriented tradition, I needed to be able to plug in different shape objects to render intervals on the fly, so I abstracted the interface needed for this:
public interface IntervalShape
{
float RADIANS_TO_DEGREES = 180f/(float)Math.PI;
void init(GL theGL, float theRadius);
void display(
GL theGL,
Interval theInterval,
float theIntervalRadius,
FabricOccupant theOccupant
);
}
The init() method gives the implementation a chance to create its OpenGL lists in the GPU and hang on to the integers that represent them, and the display() method does the matrix magic and then calls the list using these shape integers. An IntervalShape is one of these bridging objects that has part of its state (the list) living in the GPU.
There are some strange extra things in the display method that help in setting up the matrix, and the FabricOccupant (which represents you, the viewer) is included for a very special reason. When you want an object to appear smooth in OpenGL, it should have lots of polygons, but too many polygons is inefficient. Long ago, graphics coders figured out the idea of level of detail, which lets you display things in more detail if the viewer is closer, and use simpler versions of the objects if the viewer is far away. The distance from an interval to the FabricOccupant is used for this.
Fluidiom intervals are also supposed to show the viewer how much stress they are under, so there is a list stored for each of the different colors to represent stress (from red=push to blue=pull). The plot thickens.
Putting It All Together
So far, you have an idea as to what is to be displayed (the intervals) and the JOGL code needed for creating the efficient GPU-side lists that are eventually painted (the various IntervalShape implementations). The only important thing missing is the JoglFabric.
The job of the JoglFabric is to observe the fabric and represent it to JOGL through delegated calls to the various IntervalShape implementations. Each visible thing in the fabric is mirrored by an inner class bridge object in the JoglFabric. For example, there is an inner class called IntervalBridge:
private class IntervalBridge {
protected Interval interval;
public IntervalBridge(Interval theInterval) {
interval = theInterval;
}
public Interval getInterval() {
return(interval);
}
public void display(GL theGL) {
intervalShape.display(
theGL,
interval,
intervalRadius,
fabricOccupant
);
}
}
This is an inner class, because that makes it easy to deal with the IntervalShape, which happens to be global, as well as making other global variables available: the intervalRadius and the fabricOccupant. At the same time, the call to display is given the bridge's own interval reference.
At the JoglFabric level, callbacks from observing the fabric are received, and they result in additions and removals to the mirrored inventory of IntervalBridge objects, which are stored in a java.util.Map. When a change happens at interval level, we can quickly look up its proxy here using the map and reflect the change.
Conclusions
Obviously, things get a little dirty when you have to dig in a program against a procedural API from within an object-oriented language, but there's nothing like heating up the transistors on your graphics card a bit with Java code. JOGL lays OpenGL out as flat as can be, and leaves it up to you to make your code as tidy and object-oriented as you please. All you have to do is imagine that part of your object's state is over there on the graphics card instead of in your VM.
It's a little annoying that the JOGL classes have enough methods to actually hang your development environment if they try code completion, but you get used to avoiding that problem. It also takes a little while to learn how to interpret the vast quantity of OpenGL tutorials and code snippets out there on the Web and put them into JOGL form, but you get better at it.
Java3D was doing far too much calculation before talking to the graphics card. The jump to JOGL is all that I needed to get the kind of everything-in-motion performance that I needed to display the evolved running
Fluidiom creatures, and Java Web Start was there to make deployment a breeze.
Resources
Gerald de Jong is a senior Java
coach/architect/programmer in the Netherlands with his own consulting
practise, Beautiful Code BV.