Eclipse, JOGL and a Simple Scene

Just a warning: This is very old. I provide it only for legacy's sake. These days, please look into WebGL and Three.js.

In the first tutorial, I covered my initial exploration of JOGL in Eclipse. The example doesn't do much (a cube and a triangle - woohoo!) but did show how to set up JOGL itself, configure a project and the basic instantiation of the GLCanvas and the implementation of the methods of the GLEventListener interface.

That was useful, doesn't provide much of an extensible framework for exploring JOGL - which was part of the intent. The intent of this second installment is to present a much more complete framework. The overall goal was to develop a framework that:

With a little work, I believe that I have achieved most if to all of these aims. The rest of this article details how this was done. It doesn't really cover the examples themselves, focusing more on the framework itself.

The complete sources are here.

One warning: This code does NOT run on Eclipse 3.3. Works fine in Eclipse 3.2, but cannot load the OpenGLView class in 3.3. If you walk down into the bowels, you will see that in ClassPathManager.java:findLocalClassImpl, it says the classloader cannot find the class. They changed the behaviour of the classloader in 3.3, but I cannot figure out what is wrong with my manifest or bundle info or whatever. If some classloading maven can explain this, I would be appreciative. TIA. You can email me at rkwright@geofx.com.

Acknowledgements

I'd like to thank several people who gave me good advice along the way, including Bo Majewski (now of Google), Laurent M. and Ken Russell (Sun), who is currently the keeper of the JOGL flame. I also learned a fair amount from scoping out the fine work by Pepijn Van Eeckhoudt and from the excellent work by all the contributors at NeHe. Many thanks to all these folks and others who have published tutorials and comments on the web. Any errors are of course my own.

Helping Out: If (when!) you see any errors, omissions, or misleading info, please let me know. I am always willing to learn and want to make these tutorials as accurate and helpful as possible. rkwright@geofx.com. TIA.

The Container

Like the first tutorial, this one is based on simple Eclipse plugin with a view (implemented as a ViewPart). As before, there are two key classes in the main package (com.geofx.opengl.view), the view (OpenGLView) and the plugin activator (Activator). There is also a class that contains all the static constants used throughout the plugin.

The Activator class is a bit more complex than before, but we'll cover the extensions in a little more detail below. The View is actually a little simpler than before as it no longer implements GLEventListener or instantiates the GLCanvas. Instead, those implementations are delegated to the GLScene class (which is covered in detail below).

So the view class is really pretty simple:

public void createPartControl( Composite parent )
{
    sceneName = Activator.getSceneName();
    composite = new Composite(parent, SWT.EMBEDDED);
    composite.setLayout( new FillLayout() );

    glFrame = SWT_AWT.new_Frame(composite);

    this.scene = Activator.constructScene(sceneName, composite, glFrame);
}

Note that the last call is to constructScene. This is where it gets interesting.

The Base Scene Class

As noted above, one of the goals is to have the examples based on a single base class that takes care of all the housekeeping and non-example specific code. The class that implements this is GLScene in the package com.geofx.opengl.scene. The class owns the GLCanvas and implements GLEventListener. It also has some other interesting methods that we will cover below. GLScene's implementation of the GLEventListener interface methods covers the basics, but each of the derived classes can override these to implement the functionality that is specific to the example.

The constructor for the class is pretty simple:

public GLScene(Composite parent, java.awt.Frame frame )
{
    glCapabilities = new GLCapabilities();
    glCapabilities.setDoubleBuffered(true);
    glCapabilities.setHardwareAccelerated(true);
    this.canvas = new GLCanvas( glCapabilities );
    this.canvas.addGLEventListener( this ); this.glFrame = frame;
    this.glFrame.add( this.canvas );
    Rectangle clientArea = parent.getClientArea();
    this.canvas.setSize(clientArea.width, clientArea.height);

Note that we pass into the constructor the handle to the containing parent (actually the child of the parent with the SWT.EMBEDDED flag) and the AWT frame. These resources are independent of the example classes, which get created and destroyed as one switches examples. Note that there is also a trivial constructor for each example scene class as well. This is a convenience so we don't have to do any heavy lifting when we instantiate the class in order to get the label and description.

But by itself, GLScene doesn't render anything whatsoever. Moreover, GLScene has a couple of abstract methods (getLabel and getDescription) so it cannot be instantiated on its own. Instantiating the scenes is rather cool as it uses some neat tricks in Java to instantiate the current example class-object on the fly. This is done by using Eclipse's bundles and Java reflection.

Instantiating the Scene

Enumerating the Example Classes

The plugin figures out what to instantiate by enumerating the classes in the bundle at startup. It does this in the Activator class. Once the plugin is initialized, there is a call to Activator.start() which is passed the initialized bundle. This method calls enumClasses(), which walks the bundle and finds all the classes in the com.geofx.opengl.examples package that aren't subclasses. Here's the code:

public void enumClasses()
{
    try
    {
        Enumeration entries = Platform.getBundle(PluginConstants.PLUGIN_ID).findEntries("/", "*" + ".class", true);
        while (entries.hasMoreElements())
        {
            URL entry = (URL) entries.nextElement();
            // Change the URLs to have Java class names
            String path = entry.getPath().replace('/', '.');
            // see if the class is in the package we are interested in and isn't a subclass
            int start = path.indexOf(PluginConstants.EXAMPLES_PACKAGE);
            int subClass = path.indexOf("$");
            if (start >= 0 && subClass == -1 )
            {
                // strip off the class suffix and we have what we need
                String name = path.substring(start, path.length() - ".class".length());

                // now construct the object and get the label and description and save them
                GLScene scene = constructScene( name, null, null );
                classInfo.add( getClassInfo(name, scene) );
            }
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
}    

For each class-name it gets, it actually instantiates the class and then calls the new object to get the example's label and description. These strings, along with the name of the class, are stored in a small object (ClassInfo) and added to a list for later use.

Dynamically Constructing a Scene

So how does one dynamically construct a scene? The answer is ... easily. Seriously, it turns out to be pretty easy, if you know how. Basically, Java does it all the time under the covers, we just need to do it the same way.

As we saw above, we walked through the classes in the example package and got all the class names. In order to construct the class all we need is that name and arguments (and their) types we need to pass to the constructor. Here's how it is done:

public static GLScene constructScene( String name, Composite composite, java.awt.Frame frame )
{
    GLScene newScene = null;
    Object[] args = {};
    Class[] types = {};
    Class<?> classe;

    try
    {
        classe = Class.forName(name);

        if (composite != null && frame != null)
        {
            args = new Object[2];
            args[0] = composite;
            args[1] = frame;
            types = new Class[2];
            types[0] = Composite.class;
            types[1] = java.awt.Frame.class;
            newScene = (GLScene) classe.getConstructor(types).newInstance(args);
        }
        else
            newScene = (GLScene) classe.getConstructor(types).newInstance();
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
    // bunch more catches here but omitted for brevity
}

The real key is the call to

newScene = (GLScene) classe.getConstructor(types).newInstance(args);

which takes the argument types, the arguments themselves and invokes the classloader to actually construct the Scene object. Note that we actually have two versions, one for the trivial constructor and one for the real Scene construction when we pass in the the Composite and AWT Frame.

And that's it! This method is in the Activator class and acts as a kind of factory for the generation of the scenes. The trivial version is called from the Activator class itself to build the ClassInfo list as described previously. The real GLScene version is called from the OpenGLView object to construct the example classes as needed.

A Simple Example Class

So let's see how this works in practice with a single simple class. We'll use the nearly trivial SolidObjects Scene. We extend the base class, so we don't have to explicitly implement the GLEventListener - we'll get the events anyway and can handle them if we wish. Usually, we want to override init, display and possibly reshape. There is rarely any need to override dispose and never any need to override displayChanged. The JOGL documents note that even the reference implementation doesn't implement displayChanged.

The constructor is essentially trivial as we just leave the defaults from the base class, GLScene. Similarly the init doesn't require anything beyond the defaults set in the base class. In each case, we just call the base class' implementation.

public class SolidObjectsScene extends GLScene
{
    public SolidObjectsScene(Composite parent, java.awt.Frame glFrame )
    {
        super(parent, glFrame);
    }

    // a default constructor used by the ClassInfo enumerator
    public SolidObjectsScene()
    {
        super();
    }

    public void init ( GLAutoDrawable drawable )
    {
        super.init(drawable);
    }   

There isn't much to the display method either. We just fetch the GL object from the drawable, move the view slightly and call the same pyramid and cube routines from the simple view.

public void display(GLAutoDrawable drawable)
{
    super.display(drawable);
    GL gl = drawable.getGL();

    gl.glTranslatef(-1.5f,-8.0f,-3.0f);
    drawPyramid(gl);

    gl.glTranslatef(1.5f,0.0f,-7.0f);
    drawCube(gl);

Finally, we implement the two abstract methods in GLScene to provide a label for the selection dialog and a description.

public String getDescription()
{
    return "A set of simple solid 3D objects";
}

public String getLabel()
{
    return "Solid Objects";
}

And that's pretty much it. Of course, a real example class has a lot more in it, but this illustrates the basics. Now, to add a new example, we just clone this simple class and modify it as we need. As long as we put it in the example package, it will be automatically added to the list. The user can select which example to view from the selection dialog.

The Selection Dialog

The selection dialog is pretty simple. We set up the action in the plugin's plugin.xml:

<extension point="org.eclipse.ui.actionSets">
    <actionSet id="com.geofx.opengl.view.actionSet" label="Select Scene" visible="true">
        <menu id="sceneSelect" label="&OpenGL">
        <separator name="selectGroup"/>
        </menu>
        <action
          class="com.geofx.opengl.scene.SceneSelect"
          icon="icons/ogl_sm_square.gif"
          id="com.geofx.opengl.view.actions.SelectScene"
          label="&Select Scene"
          menubarPath="sceneSelect/selectGroup"
          toolbarPath="selectGroup"
          tooltip="Select Scene" />
    </actionSet>
</extension>

The dialog is pretty straightforward. We implement the IWorkBenchActionDelegate interface. Then we handle the run() event:

public void run(IAction action)
{
    ArrayList classList = Activator.getClassInfo();
    ListDialog dialog = new ListDialog(window.getShell());
    dialog.setTitle("Select Scene");
    dialog.setMessage("Choose a scene from the list...");
    dialog.setContentProvider(new ListContentProvider(classList));
    dialog.setLabelProvider( new ListLabelProvider() );
    dialog.setInput(classList);
    dialog.setInitialSelections(new Object[] {Activator.getSceneName()});
    if ( dialog.open() == Window.OK)
    {
        Object[] results = dialog.getResult();
        if (results.length > 0 && results[0] instanceof ClassInfo)
        {
            ClassInfo info = (ClassInfo)results[0];
            Activator.setSceneName( info.name );
            Activator.getGLView().updateView();
        }
    }
}

The interesting part of this first line, where we call the Activator to the list of the ClassInfo we obtained at startup. We then set this as the input to the simple LabelProvider we create. The LabelProvider just fetches the label from the current ClassInfo object to display in the dialog. Then when the user selects an item and presses OK, we get back a reference to that ClassInfo object. We call back to the Activator to set the classname for that object as the current scene and tell the view to update itself. It then destroys the current view and creates a new one:

public void updateView()
{
    if (!sceneName.equals(Activator.getSceneName()))
    {
        sceneName = Activator.getSceneName();
        this.scene.dispose();
        this.scene = Activator.constructScene(sceneName, composite, glFrame);
        // we need to explicitly request the focus or we never get it
        this.glFrame.requestFocus();
    }

    this.scene.render();
}

We just call constructScene() again, passing the new scene-name, container and frame. Note the comment at the end - if you don't explicitly request that the frame be given the focus it never gets it.

Interactivity

Now that we have a functioning base class and a simple example, let's explore how we can interact with the examples. First off, we have a 3D scene so it would be great if we could see it from any angle we choose, We achieve this with the "Scene Grip" (written originally by Bo Majewsky and ported to JOGL, modified somewhat and extended by me).

The Scene Grip

The scene grip basically handles the mouse movements and keyboard entries and translates those to changes to the translation and rotation of the scene. The mouse events are captured by implementing the MouseListener and MouseMovementListeners of AWT. We have to use AWT because that is the frame that hosts the GLCanvas.

With the left mouse button pressed, the movements are used to adjust the X and Y offsets. If the right mouse button is pressed while dragging, the movements control the X and Y rotation of the scene. There is presently no way to control the Z offset or rotation with the mouse.

IMPORTANT: JOGL is effectively single-threaded so the GLCanvas can only be updated from its own thread - not the UI (keyboard and mouse) thread. So once inputs are captured, updates to the view can only be triggered by calling the render() method on the GLCanvas, which in turn calls the display method from within the GLCanvas thread.

Handling Mouse and Keyboard Events

The scene grip class is also used to trap the key events as well.This is done by implementing the KeyListener interface. The keys allow one to control the geometry via the leyboard. The mappings are as follows:

Key Unmodified Ctrl Pressed
Right Arrow
+X Shift
+X rotation
Left Arrow
-X Shift
-X rotation
Up Arrow
+Y shift
+Y rotation
Down Arrow
-Y Shift
-Y rotation
Page Up
+Z Shift
+Z rotation
Page Down
-Z shift
-Z rotation

Note that before actually handling a key event, the keyboard handler calls a method on the GLScene class, handleKeyEvent(). By default, this does nothing and returns false. But any of the example classes derived from GLScene can override this and handle any and all key events. If the handleKeyEvent method returns true, the keyhandler in SceneGrip will just force an update of the scene and return.

Lighting and Blending Control

The base class, GLScene, also has methods (and member variables) to toggle lighting and blending enablement. By pressing the L or B keys, lighting or blending (respectively) are toggled, which also triggers and automatic update of the scene.

As an aside, if you look at my examples which have lighting you will note that the lighting is enabled at the beginning of the display method. When I first ported them, I was puzzled for a little while why my lights wouldn't work. Turns out that most of the examples on NeHe and elsewhere enable the lights at the end of the display method (or equivalent). This has no effect since the scene has already been rendered. It only has an effect the next time the scene is rendered. But most people have their scenes animated - whether they need it or not. I don't like wasting CPU cycles so I never draw anything more than once unless I have to. In any case, you need to enable lights and blending and filtering BEFORE you draw the scene.

The base class also implements a toggle for the filter. But the default implementation does nothing. Derived classes need to implement it if they need it.

Animation

The GLScene class also instantiates an animator, using the scene's canvas. The scene instantiates the animator and immediately turns it off. The user can toggle the animator on and off with the 'A' key. It is up to the example class to actually do something for each animation step.

Textures and Resources

One of the many cool features of OpenGL is the support for texture mapping. This is of course supported by JOGL. In the C-language implementation, using textures requires a dozen calls of various types to load the texture, set up the mapping and so on. JOGL is pretty much the same. However, the group at Sun has added some utilities that make it much simpler.

First, you have to load the texture. I struggled with this a bit at the beginning because Eclipse's management of resources is a bit arcane (and poorly documented). However, I finally figured out the right way to do it.

Resources need to be stored in a folder that is a child of the project's base resource folder, typically named 'src'. The resources can also be stored as a child of the output folder, typically named 'bin' but this is less convenient since the bin folder is not normally reflected in the source-code control system (e.g. CVS or subversion, etc.).

So just put your image files (or any other resources you need) in that folder, which I imaginatively name 'resource') . Then, be sure to refresh your project - otherwise Eclipse won't know about the folder or its contents. The to load your file, get the classloader to do the work for you:

GLScene.class.getClassLoader().getResource(filename)

This returns a URL, which you can pass directly to the Texture creation utility. The GLScene class implements a method, createTexture that makes this all simple.

protected Texture createTexture( String filename, int filterParm, boolean mipmap )
{
    Texture t = null;
    try
    {
        t = TextureIO.newTexture(GLScene.class.getClassLoader().getResource(filename), mipmap, null);
        t.setTexParameteri(GL.GL_TEXTURE_MIN_FILTER, filterParm);
        t.setTexParameteri(GL.GL_TEXTURE_MAG_FILTER, filterParm);
    }
    catch (IOException e)
    {
        System.err.println("Error loading " + filename);
    }
    catch (Exception e)
    {
        System.err.println("Exception while loading " + filename);
        e.printStackTrace();
    }
    return t;
}

The filterParm is the value normally passed to glTexParameteri(), e.g. GL_NEAREST, GL_LINEAR or GL_LINEAR_MIPMAP_NEAREST. If mipmap is true, then the texture is mipmapped. Note that Texture supports GIF, JPEG, PNG and TIFF (and some of the funky old SGI formats) but not Microsoft's BMP. But Texture does have a TextureProvider subclass which you can implement easily enough if you don't want to convert your images themselves.

Using the texture is pretty straightforward. To map the texture onto a cube face, for example:

TextureCoords tc = texture.getImageTexCoords();
float tx1 = tc.left();
float ty1 = tc.top();
float tx2 = tc.right();
float ty2 = tc.bottom();

int imgw = texture.getImageWidth();
int imgh = texture.getImageHeight();

float h = 1.0f;
float w = 1.0f;
if (imgw > imgh)
h = ((float) imgh) / imgw;
else
w = ((float) imgw) / imgh;

texture.enable();
texture.bind();

gl.glBegin(GL.GL_QUADS);

gl.glTexCoord2f(tx1, ty1);
gl.glVertex3f(-w, h, 0f);

gl.glTexCoord2f(tx2, ty1);
gl.glVertex3f(w, h, 0f);

gl.glTexCoord2f(tx2, ty2);
gl.glVertex3f(w, -h, 0f);

gl.glTexCoord2f(tx1, ty2);
gl.glVertex3f(-w, -h, 0f);

gl.glEnd();

Note that this example even takes care of cases where the texture-image is not square. Using non-square images is a performance hit, but sometimes useful. Another important aspect to note is that the texture needs to be enabled and bound (the equivalent of the same calls in C). Finally, if you look closely at the TexCoord calls, you'll note that the X coords look normal, but the Y coords seem to be inverted - the top of the texture is mapped to the bottom of the face and the bottom is mapped to the upper right of the face. This is because the texture is mapped in the Java2D space which, like a lot of 2D geometries, is mapped like a CRT with the origin at the upper left and Y values increasing as one goes down the screen (For you kids, this is because that is how the old cathode ray tubes worked, starting the sweep at the top and sweeping down :-). So by default, your textures will be inverted unless you correct for that.

Rendering Text

Last but not least is text. As I mentioned in the first tutorial, good text rendering was one of my major requirements. JOGL does a pretty good job of providing text capabilities by basically mapping the Java2D text rendering into the OpenGL space. As a result of this approach, the text is two-dimensional (i.e. there is no thickness) but is fine for most purposes - spinning extruded text may look cool but isn't that useful.

Using the renderer is pretty straightforward. To intialize (from the JOGL demos TextCube):

renderer = new TextRenderer(new Font("Times New Roman", Font.TRUETYPE_FONT, 72), true, true);

// Compute the scale factor of the largest string which will make
// them all fit on the faces of the cube
Rectangle2D bounds = renderer.getBounds("Bottom");
float w = (float) bounds.getWidth();
float h = (float) bounds.getHeight();
textScaleFactor = 1.0f / (w * 1.1f);

A couple of comments. The text renderer takes a normal AWT font, so it can be either a so-called "logical" font, which is platform specific, or it can be an actual facename (e.g. Times New Roman or Palatino). Secondly, the two flags at the end of the call are for anti-aliasing and fractional metrics, respectively. Anti-aliasing is a very good idea in almost all cases except where performance is paramount (though I suspect that anti-aliasing probably gets lost in the noise performace-wise). The fractional metrics flag controls whether characters are always spaced an incremental number of pixels apart or are placed as closely as possible to the location where they would be if the screen had infinite resolution. In general, fractional metrics make sense for high-resolution devices, but not if you are targeting low resolution devices like phones or PDAs.

Once you have initialized the renderer, you're ready to draw.

renderer.begin3DRendering();

Rectangle2D bounds = renderer.getBounds(text);
float w = (float) bounds.getWidth();
float h = (float) bounds.getHeight();

renderer.draw3D(text, w / -2.0f * textScaleFactor, h / -2.0f * textScaleFactor, halfFaceSize, textScaleFactor);

renderer.end3DRendering();

Note that the code is getting the bounding box so it can offset the text by half the height and width so it centers the text on the face of the cube. This is needed because the draw3D code places the baseline of the leftmost character at position (x, y, z) in the current coordinate system.

One aspect to bear in mind with this text-rendering approach is that what it is doing under the covers is rendering the glyphs (characters) in an offscreen buffer and creating a texture with them. The texture is then mapped into your scene. The gotcha with this is that the resulting text is effectively a fixed size texture so the glyphs are effectively bitmapped. Turning on anti-aliasing will help, but if you are going to be zooming in on the text, it's going to pixelate at some point.

(I am working on some true vector text support leveraging the the TextRenderer object and will report on that when it is baked)

Next Steps

So that's pretty much a complete coverage of the GLScene class and its usage. I could dig into further, but you have the source code, so you can explore on your own. Feel free to send me questions or suggestions at rkwright@geofx.com.

The next part of this series is an OpenGL-enabled Eclipse perspective. For my purposes, it allows me to write visualization models easily with both OpenGL and 2D SWT windows for displaying data. But that's a work in progress - this isn't my day job, so it gets worked on when I can stay up late enough!



About Us | Contact Us | ©2017 Geo-F/X