JOGL (Java OpenGL) Tutorial

@for Developers
@author Kai Ruhl
@since 2009-04

Introduction

JOGL is the OpenGL binding for Java (JSR-231), mapping functions as closely as possible from C. OpenGL is a standardized API used to draw 3D graphics to the screen (similar to DirectX / Direct3D). The primary difference to 2D is that you need to think about z coordinates and the position of the viewer ("camera", including viewing angle and such).

I am writing this JOGL tutorial because I did not find another good one. The JOGL homepage is poor in information, the few tutorials are mostly outdated (not using newer functions, e.g. for textures or fonts), and the famous NeHe OpenGL tutorials are for pure OpenGL under C (Java port available though). The best API doc I found is the Python OpenGL API (better than the JOGL javadoc).

My plan for this tutorial (which is based on my experiences while writing Breed Orbiter) is:

  1. Open a blank OpenGL window in Java.
  2. Set the camera position and direction, looking at a triangle.
  3. Create a planet sphere.
  4. Add lighting from the sun.
  5. Add an earth texture to the planet.
  6. Add an orbit with a primitive satellite.

If you like, follow me on this way...

How does JOGL/OpenGL work?

First, a few words on how OpenGL works: Procedural. You start a thread that repeatedly calls a display() method at your fps (frames per second) rate; in this method, you clear the screen and rebuild the entire scene. You do this by issuing primitive commands: Begin vertex, this is the 1st point, the 2nd point, the 3rd, end vertex. And so on.

Of course, it gets more complicated for textures, lighting and the like, but the bottom line is that there is one GL object which has like 1000 glWhatever() methods that you will call over and over again. Yes it is primitive but near to the hardware, and this means fast and flexible.

To integrate with Java, there have to be some concessions: Namely, the AWT component (a GLCanvas extending Component) is integrated into the AWT event queue, and you need to implement a listener (GLEventListener) for lifecycle callbacks (not so procedural here, aint it).

As a little prepping up for the 3D world, the figures below show coordinate system and camera.

3D coords Camera
3D coordinate system Camera looking at 3D scene

A positive z coordinate is coming out of the screen, a negative into the screen. The camera position is determined by one point to be in, another point to look at, and a vector defining where your head is pointing into the sky (well, and a viewing angle, but more on that later). With that, lets start doing a bit 3D!

Step 1: A blank OpenGL window in Java

Before we start programming, you have to setup a bit. Other sites have described that, so I will be brief:

And then we make a start class: Extend GLCanvas and implement GLEventListener; and in the main method, put the canvas into a JFrame. You will notice a constructor and 4 event methods:

public class MyJoglCanvas extends GLCanvas implements GLEventListener {
    public MyJoglCanvas(int width, int height,
GLCapabilities capabilities) {}
    public void init(GLAutoDrawable drawable) {}
    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {}
    public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {}
    public void display(GLAutoDrawable drawable) {}
}

One by one, they are good for the following things:

For your convenience, the important classes so far are summarised in the following figure:

JOGL starter classes
JOGL starter classes (many methods omitted)

And now, in detail. Lets start with the constructor.

    public MyJoglCanvas(int width, int height, GLCapabilities capabilities) {
        super(capabilities);
        setSize(width, height);
    }

Seems easy enough, but where to the capabilities come from? You need to specify them in your main method. Fortunately, this is quite easy:

    GLCapabilities capabilities = new GLCapabilities();
    capabilities.setRedBits(8);
    capabilities.setBlueBits(8);
    capabilities.setGreenBits(8);
    capabilities.setAlphaBits(8);

The capabilities simply define what you want from the underlying OpenGL implementation. If the machine cannot do this, an exception will be thrown and you know what will work and what wont. Now going to the init() method:

    public void init(GLAutoDrawable drawable) {
        GL gl = drawable.getGL();
        drawable.setGL(new DebugGL(gl));

        // Global settings.
        gl.glEnable(GL.GL_DEPTH_TEST);
        gl.glDepthFunc(GL.GL_LEQUAL);
        gl.glShadeModel(GL.GL_SMOOTH);
        gl.glHint(GL.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
        gl.glClearColor(0f, 0f, 0f, 1f);

        // Start animator (which should be a field).
        animator = new FPSAnimator(this, 60);
        animator.start();
    }

A little more lines here. At first, you can notice the GL object: This is the one that we will issue most commands on. As a nice feature of JOGL, we can decorate it with a DebugGL, which will supply us with stacktraces on exceptions (as opposed to simply crashing as it does in C).

Next, some global settings (here: my standard settings). The first two enable z-buffers (always needed for 3D drawing - you could also use OpenGL to do 2D only) with last-one-wins settings (draw a rect, and a line on it, the line will still be visible). A shade model of GL_SMOOTH looks nice, as does the GL_NICEST correction hint. The clear color is the CLS (ClrScr) color: Black with no translucency (alpha is 1), specified as RGBA in floats.

Finally, the fps thread I spoke about earlier: The animator thread (which should be a field in our class) expects a GLCanvas and the frame rate setting (here: 60fps). As we are in the init() method, we can start it without fearing something is not ready in AWT. And now, someone could resize the window:

    public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
        GL gl = drawable.getGL();
        gl.glViewport(0, 0, width, height);
    }

Dont ask me what exactly a view port is; it represents the part of the screen that you can see, and we'll leave it at that (for more, wikipedia is your friend). Ignoring the displayChanged() method, we come to the last and most important one:

    public void display(GLAutoDrawable drawable) {
        GL gl = drawable.getGL();
        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
    }

And now, you can start the whole thing and we can see... a black screen (this is what glClear() does at every tick). We have no objects yet and no camera direction, but this is something we can change now.

For a source code view of what we have covered so far, consider looking at the MyJoglCanvasStep1.java file.

Step 2: Camera position and first Triangle

Kinda complicated just for a black screen so far, huh? I certainly think so (however I am willing to accept that this is the price of being near to the hardware). Now I want to see something! For that, we need to define our camera position in the 3D space and then actually draw something.

Consider the display() method: We will spend all our time here. Before any drawing occurs, the camera position should be set. This is because OpenGL is just a simple pipeline: It renders something and promptly forgets about it, so if we change the camera position in between, it will be confused.

For a start, we set the camera position at (0, 0, 100), which is 100 units just in front of the screen (where you are probably sitting). For this, we need a GLU object (an OpenGL helper library with standard geometric functions), and a setCamera() method within display():

    public void init(GLAutoDrawable drawable) {
        glu = new GLU();
    }

    public void display(GLAutoDrawable drawable) {
        setCamera(gl, glu, 100);
    }

And of course, the magic of setting the camera.

    private void setCamera(GL gl, GLU glu, float distance) {
        // Change to projection matrix.
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glLoadIdentity();

        // Perspective.
        float widthHeightRatio = (float) getWidth() / (float) getHeight();
        glu.gluPerspective(45, widthHeightRatio, 1, 1000);
        glu.gluLookAt(0, 0, distance, 0, 0, 0, 0, 1, 0);

        // Change back to model view matrix.
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glLoadIdentity();
    }

This looks complicated, and inside, it surely is -- but this is no concern to us. The first thing to know is that OpenGL calculates coordinates using matrixes. For the camera, we will switch on the PROJECTION matrix, and for everything else the MODELVIEW matrix. The glLoadIdentity() method resets all previous matrix settings one may have made.

As for the perspective itself, that is something that GLU takes care of. We specify a viewing angle (e.g. 45) with respect to the current width/height ratio of the window (required by OpenGL to calculate the actual viewing angle on screen), and a minimum (1) and a maximum (1000) distance where we will see things (infinity would be impossible to compute by your poor GPU).

And then, the gluLookAt() method, which takes three 3D coordinates:

And now we are out of the screen, looking in. And for what to see? Let us go back into the display() method and add some code at the very end:

    public void display(GLAutoDrawable drawable) {
        // Write triangle.
        gl.glColor3f(0.9f, 0.5f, 0.2f);
        gl.glBegin(GL.GL_TRIANGLE_FAN);
        gl.glVertex3f(-20, -20, 0);
        gl.glVertex3f(+20, -20, 0);
        gl.glVertex3f(0, 20, 0);
        gl.glEnd();
    }

Here is it that the standard JOGL / OpenGL drawing procedure begins. You specify a color (RGB), you say what you want (GL_TRIANGLE_FAN), and then begin churning out coordinates. When you are finished, you glEnd() what you started with glBegin(). And voila, our first visual result:

Our first triangle
Our first triangle

You will notice that although the 3D coordinates are evenly spaced, the triangle on screen is not. This is an effect of the viewing angle: If you stand in front of a tree, you wont see its "real" height either.

And in keeping step, you might now consider looking at the MyJoglCanvasStep2.java source file.

Step 3: Planet Sphere

Having a triangle is nice and all (and this is actually the basis of almost all games out there), but how about some more sophisticated shapes? The GLU helper library can help us again, this time with a sphere. Again, go to the display() method and replace the triangle section at the end:

    public void display(GLAutoDrawable drawable) {
        // Set camera.
        setCamera(gl, glu, 30);

        // Draw sphere (possible styles: FILL, LINE, POINT).
        gl.glColor3f(0.3f, 0.5f, 1f);
        GLUquadric earth = glu.gluNewQuadric();
        glu.gluQuadricDrawStyle(earth, GLU.GLU_FILL);
        glu.gluQuadricNormals(earth, GLU.GLU_FLAT);
        glu.gluQuadricOrientation(earth, GLU.GLU_OUTSIDE);
        final float radius = 6.378f;
        final int slices = 16;
        final int stacks = 16;
        glu.gluSphere(earth, radius, slices, stacks);
        glu.gluDeleteQuadric(earth);
    }

We set the camera to distance 30 to better coincide with the 6.378 km earth radius, then set a nice blue color (RGB = 0.3, 0.5, 1.0) and command GLU to paint a sphere. Now, GLU uses a concept called "quadrics", which is really just an identifier for a lot of behind-the-scenes work: A sphere is actually drawn using a lot of quads ("rectangle floating in space").

Everytime we want to use a GLU geometry, we have to create one with gluNewQuadric() and, at the end, delete it with gluDeleteQuadric(). Everything in between depends on the geometry.

Above, you first see that we specify the draw style. FILL means to fill the quads that make up the sphere's shell; it is not the volume of the sphere. Try LINE and POINT to see what they mean.

Next, we specify the normals. Normals are vectors that are perpendicular to a quad, specifying where "up" is when standing on the rectangle. This is important to OpenGL when calculating light effects (see below). Next, the orientation of the quads is either INSIDE or OUTSIDE; you would use the former if you wanted to paint some kind of cave to stand in, using a sphere.

And then, the sphere itself: We call gluSphere() with the quadric identifier, radius, and slices and stacks (think "longitude and latitude detail along z-axis"). The result looks like the following:

Our first sphere
Our first sphere

The sphere looks kind of flat, but that is ok: After all, we specified only a blue color, and no lights or shadows, so where should the "3D effect" come from? Since this is not nice, we are going to change this in the next section.

For this section though, you could have a look at the MyJoglCanvasStep3.java source file.

Step 4: Lighting

Lighting is where the whole color model is exchanged: That's right, throw glColor3f() away. Instead, we specify how a rectangle surface reacts to light in its RGB spectrum (like in the real world), and we add light sources with their own RGB spectrum (also like in the real world).

Before we begin, let me brief you on the kind of lights. We will be using AMBIENT and SPECULAR light, and our surface "material" will react on this light. Ambient light is just everywhere, with no particular source (no "3D effect" with this). We will typically use it at strength 0.2 (of 0..1) so that we can see places dimly where no light spot is shining. Specular light is the one coming from a spot, and reflecting from surfaces; this gives the typical "3D effect". I normally use it at strength 0.8 across the spectrum.

In standard OpenGL, you can have eight light sources, but we will be only using one. You have to specify them before drawing (remember that OpenGL is just a stupid pipeline...), otherwise light will not be used correctly. Once again, we go into the display() method and set everything:

    public void display(GLAutoDrawable drawable) {
        // Prepare light parameters.
        float SHINE_ALL_DIRECTIONS = 1;
        float[] lightPos = {-30, 0, 0, SHINE_ALL_DIRECTIONS};
        float[] lightColorAmbient = {0.2f, 0.2f, 0.2f, 1f};
        float[] lightColorSpecular = {0.8f, 0.8f, 0.8f, 1f};

        // Set light parameters.
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, lightPos, 0);
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_AMBIENT, lightColorAmbient, 0);
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_SPECULAR, lightColorSpecular, 0);

        // Enable lighting in GL.
        gl.glEnable(GL.GL_LIGHT1);
        gl.glEnable(GL.GL_LIGHTING);

        // Set material properties.
        float[] rgba = {0.3f, 0.5f, 1f};
        gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, rgba, 0);
        gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, rgba, 0);
        gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, 0.5f);

        // Draw sphere (possible styles: FILL, LINE, POINT).
        GLUquadric earth = glu.gluNewQuadric();
        ...
    }

You see that a lot is going on. First, we define the light parameters: The position of our only light source: A 3D coordinate (-30, 0, 0) slightly to the left, with a fourth element set to 1. This is necessary to shine in all directions, but if you want to know why, ask another tutorial. Next, you state the RGBA value of AMBIENT and SPECULAR light (here: white light, evenly distributed). The alpha value is always 1, meaning full opacity ("no glass effect at all").

In the next three lines, we bind these parameters to a specific light, namely LIGHT1, by specifying the parameter to set, the appropriate array, and an index where to start in this array (this could be different from 0 if you use one giant array for performance reasons). Finally, we enable LIGHT1 and, most importantly, LIGHTING at all! From now an, everything will be lighted.

Please note that if you do not change the lighting between fps frames, you could put everything so far into the init() method instead of display(). This would bring a bit of performance.

Now that we have finished writing the light sources, it is time to specify how the material reacts to the light across the RGB spectrum: This is what used to be "the color". We take the same blue color as before and add an alpha of 1 (full opacity); then specify it both as reaction to AMBIENT and to SPECULAR light (this is not necessarily so! Try experimenting if you like).

The last thing before we start drawing our sphere is the SHININESS, which specifies how strong the reflection on the surface is (0..100). We set it to 0.5 here, so it has not much effect. And then, we draw the same sphere as before -- the result:

A lighted sphere
A lighted sphere

Much better, I would say! The light source from the left illuminates the left side of the sphere with specular 0.8 light, while the darker right side is purely made visible by the ambient 0.2 light.

As always, please follow the code by inspecting the MyJoglCanvasStep4.java source file.

Step 5: Earth Texture

We can do even better: A texture is what brings a form truly alive, by adding a pattern that our human eyes like so much. In case of planet textures, you can get a good one from Planet Pixel Emporium; the 1000x500 texture is freeware. So now, how do we apply it to our sphere?

Two steps need to be taken: First, we need to load the texture from a PNG file and assemble it into a form JOGL can understand. Second, just before drawing the sphere, we have to tell JOGL that the next thing coming should use the texture ("bind the texture to the GL context").

The first step used to be difficult with OpenGL (see excerpt); in JOGL, we have helper classes such as the TextureRenderer that do most of the things for us. In the init() method,we load the texture:

    public void init(GLAutoDrawable drawable) {
        // Load earth texture.
        try {
            InputStream stream = getClass().getResourceAsStream("earth.png");
            TextureData data = TextureIO.newTextureData(stream, false, "png");
            earthTexture = TextureIO.newTexture(data);
        }
        catch (IOException exc) {
            exc.printStackTrace();
            System.exit(1);
        }
    }

For this purpose, we retrieve an input stream (standard Java IO: image must be in same package as class) and command TextureIO to read the texture data; apart from the stream, we specify false to supress generation of mipmaps ("up/downscaled versions of the texture for different viewing distances"), and supply the file ending. Then, we generate an OpenGL earthTexture from the data, again using TextureIO.

In the display() method, we put this earthTexture into use, by binding the texture to the GL context before proceeding with drawing the sphere.

    public void display(GLAutoDrawable drawable) {
        // Set material properties.
        float[] rgba = {1f, 1f, 1f};
        gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, rgba, 0);
        gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, rgba, 0);
        gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, 0.5f);

        // Apply texture.
        earthTexture.enable();
        earthTexture.bind();

        // Draw sphere.
        GLUquadric earth = glu.gluNewQuadric();
        glu.gluQuadricTexture(earth, true);
    }

Compared to the previous version, you can see three changes: First, we change the material color to white (1f, 1f, 1f), because now the texture shall define the color of each spot on the surface. Then, we enable() textures globally (switch this off again if you want an untextured surface come next), and then bind() the specific texture for whatever comes next.

And next comes the GLU sphere! Here, we need to make only one change: Announce that we indeed want to use the current texture for the next form. This is kind of redundant, and GLU specific: If you draw triangles (or quads), you wont have to do this.

The rest plays out as before, resulting in the following:

A textured sphere
A textured sphere

And this is the earth! Seen from the south pole, and that is because GLU always paints at 0, 0, 0 and along the z-axis; and we are looking along the z-axis into the monitor. No worry, we can change the zero position ("translation"), and also the alignment of the body ("rotation") -- stuff for the next chapter!

For a review of what we have done so far, have a look at the MyJoglCanvasStep5.java source file.

Step 6: Satellite on Orbit

So now we have the basics of triangles, bodies, light and material, and texture. In our toolkit, there is only one basic thing missing: How to arrange the bodies we draw in the 3D space. You only need to memorize two methods names here: glTranslate() and glRotate().

What I want to do with you is to paint a satellite on a circular orbit around earth. This satellite will be a cube with two solar panels. Clearly, it is unfeasible to manually calculate all those coordinates for each point within the orbit circle. Therefore, we will simply shift what is considered to be the zero point (0, 0, 0).

Before any drawing begins, we load a secondary texture for the solar panel in the init() method. This also demonstrates nicely how to use multiple textures. I used GIMP and a scanned foto to produce a solar panel texture with font on it.

    public void init(GLAutoDrawable drawable) {
        // Load the solar panel texture.
        try {
            InputStream stream = getClass().getResourceAsStream("solar_panel.png");
            TextureData data = TextureIO.newTextureData(stream, false, "png");
            solarPanelTexture = TextureIO.newTexture(data);
        }
        catch (IOException exc) {
            exc.printStackTrace();
            System.exit(2);
        }
    }

Having that, once again we go into the display() method, this time computing the orbit position and drawing a satellite (silver cylinder plus textured panel). For a start, we will cover the cylinder at the right position.

    public void display(GLAutoDrawable drawable) {
        // Save old state.
        gl.glPushMatrix();

        // Compute satellite position.
        satelliteAngle = (satelliteAngle + 1f) % 360f;
        final float distance = 10.000f;
        final float x = (float) Math.sin(Math.toRadians(satelliteAngle)) * distance;
        final float y = (float) Math.cos(Math.toRadians(satelliteAngle)) * distance;
        final float z = 0;
        gl.glTranslatef(x, y, z);
        gl.glRotatef(satelliteAngle, 0, 0, -1);
        gl.glRotatef(45f, 0, 1, 0);

        // Set silver color, and disable texturing.
        gl.glDisable(GL.GL_TEXTURE_2D);
        float[] ambiColor = {0.3f, 0.3f, 0.3f, 1f};
        float[] specColor = {0.8f, 0.8f, 0.8f, 1f};
        gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, ambiColor, 0);
        gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, specColor, 0);
        gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, 90f);

        // Draw satellite body.
        final float cylinderRadius = 1f;
        final float cylinderHeight = 2f;
        GLUquadric body = glu.gluNewQuadric();
        glu.gluQuadricTexture(body, false);
        glu.gluQuadricDrawStyle(body, GLU.GLU_FILL);
        glu.gluQuadricNormals(body, GLU.GLU_FLAT);
        glu.gluQuadricOrientation(body, GLU.GLU_OUTSIDE);
        gl.glTranslatef(0, 0, -cylinderHeight / 2);
        glu.gluDisk(body, 0, cylinderRadius, cylinderSlices, 2);
        glu.gluCylinder(body, cylinderRadius, cylinderRadius, cylinderHeight, slices, stacks);
        gl.glTranslatef(0, 0, cylinderHeight);
        glu.gluDisk(body, 0, cylinderRadius, cylinderSlices, 2);
        glu.gluDeleteQuadric(body);
        gl.glTranslatef(0, 0, -cylinderHeight / 2);
    }

The first thing you see is glPushMatrix(). Since we will be tinkering with the default position, a backup is useful; after we are done drawing, we will restore it via glPopMatrix(). Next, we compute the satellite position: A circle around the z axis, computed with sin() und cos() from the standard Java libraries. To track the current position, we will simply store a satelliteAngle (0..359) which advances 1 degree with each display() call.

And then, the interesting thing happens: Using glTranslatef(), we shift the default position to where the cylinder should be; so for the next GL or GLU commands, their "local" coordinate (0, 0, 0) is at our "global" (x, y, z). You also see another position manipulation: With glRotatef(), the local coordinate system can be rotated: First around the z axis according to the satellite angle, then 45 degrees around the y axis (so we have a nice isometric view on the cylinder and the panels).

Setting the material properties for the cylinder begins with a glDisable(GL_TEXTURE_2D). This is because the earth texture we used earlier is still in effect: If we would draw the cylinder now, it would be earth textured. The rest of the material properties is simple: Gray color in ambient and specular spectrum, and a high shininess rating (90) to let the silver reflect light properly.

The cylinder itself should look somewhat familiar: A new quadric is created, and here it is used 3 times: 2 disks ("top and bottom of the pipe") and a GLU cylinder (which is a pipe, not a can). Since GLU always works from  its local (0, 0, 0), we have to glTranslatef() along the z axis while drawing disk, cylinder, disk. Afterwards, the last translation restores the previous state.

Since this is the last chapter, I will now load even more stuff on your brain: We will do the solar panel texturing the "classic way". If you ever want to texture quads or triangles by yourself (not using GLU), you will need this. To make things simpler, we will draw only one large solar panel, which goes straight through the cylinder.

    public void display(GLAutoDrawable drawable) {
        // Set white color, and enable texturing.
        gl.glEnable(GL.GL_TEXTURE_2D);
        gl.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT, rgba, 0);
        gl.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, rgba, 0);
        gl.glMaterialf(GL.GL_FRONT, GL.GL_SHININESS, 0f);

        // Draw solar panels.
        gl.glScalef(6f, 0.7f, 0.1f);
        solarPanelTexture.bind();
        gl.glBegin(GL.GL_QUADS);
        final float[] frontUL = {-1.0f, -1.0f, 1.0f};
        final float[] frontUR = {1.0f, -1.0f, 1.0f};
        final float[] frontLR = {1.0f, 1.0f, 1.0f};
        final float[] frontLL = {-1.0f, 1.0f, 1.0f};
        final float[] backUL = {-1.0f, -1.0f, -1.0f};
        final float[] backLL = {-1.0f, 1.0f, -1.0f};
        final float[] backLR = {1.0f, 1.0f, -1.0f};
        final float[] backUR = {1.0f, -1.0f, -1.0f};
        // Front Face.
        gl.glNormal3f(0.0f, 0.0f, 1.0f);
        gl.glTexCoord2f(0.0f, 0.0f);
        gl.glVertex3fv(frontUR, 0);
        gl.glTexCoord2f(1.0f, 0.0f);
        gl.glVertex3fv(frontUL, 0);
        gl.glTexCoord2f(1.0f, 1.0f);
        gl.glVertex3fv(frontLL, 0);
        gl.glTexCoord2f(0.0f, 1.0f);
        gl.glVertex3fv(frontLR, 0);
        // Back Face.
        gl.glNormal3f(0.0f, 0.0f, -1.0f);
        gl.glTexCoord2f(0.0f, 0.0f);
        gl.glVertex3fv(backUL, 0);
        gl.glTexCoord2f(1.0f, 0.0f);
        gl.glVertex3fv(backUR, 0);
        gl.glTexCoord2f(1.0f, 1.0f);
        gl.glVertex3fv(backLR, 0);
        gl.glTexCoord2f(0.0f, 1.0f);
        gl.glVertex3fv(backLL, 0);
        gl.glEnd();

        // Restore old state.
        gl.glPopMatrix();
    }

First, we enable textures again with glEnable(GL.GL_TEXTURE_2D). Note that we could also have used satelliteTexture.enable(), as we did before with the earthTexture. Material properties are set to white, with no shininess.

The plan for the next section is to draw front and back face of a cube, both textured. Since we want a flat, long block rather than a die, we have to distort the dimensions: And we do it with glScalef(). In the float array section, you see the definition of the cube's corners: Upper left, upper right, lower right and lower left for both front and back face.

We draw GL_QUADS (rectangles). Note that you have to specify glBegin() and glEnd() only once: Every four vertices, OpenGL assumes automatically that a new quad begins. But we do not only specify the vertices ("corners"): For each corner of the rectangle, we map a corner of the texture, whereby (0, 0) is the upper left, and (1, 1) the lower right corner of the texture.

Additionally, we specify the normal vector ("arrow pointing into the sky") for each rectangle, using glNormal3f(). This is required for the lighting, so OpenGL knows where and how to reflect light. This is also the reason why the corners and their texture corner mappings may seem counterintuitive sometimes: Depending on the normal vector, we have to switch left/right on the texture.

And that is it! This last step was kind of long, but we are rewarded by this sight:

A satellite in orbit
A satellite in orbit

You will note that half of the time, the solar panel is dark: This is when the sunlight strikes the back of the panel, which is reflected not into your eye, but into the screen, away from you.

And as the final source code of this tutorial, please have a look at the MyJoglCanvasStep6.java source file.

Conclusion

In this tutorial, we have walked through quite some JOGL functionality. We covered basic structures like triangles, quads and spheres in three dimensional space; directional lighting and material properties; and textures, which we all like so much. Finally, we worked some movement into our scene. Personally, I have worked further on the orbiter, which currently looks like this:

Orbiter (breed) 0.2
Orbiter (breed) 0.2

And now, there is very much more to discover for you! (e.g. diffuse lighting, translucency, or the great JOGL FontRenderer). I hope you have been able to learn something from this tutorial; for improvements, I appreciate all comments (preferably via mail to kain at the above domain). Thanks for leaving a part of your attention span here, and have a good time!

EOF (Apr:2009)