Java OpenGL Game Development

A detailed look at implementing graphically demanding games in OpenGL with Java, using the JOGL library

Wednesday, February 11, 2009

Quick hint - Texture matrix

Just a quick note stemming from a week long bug hunt. The GL_TEXTURE matrix is per texture unit, so if you are using multiple texture units, be sure to get the right one active before pushing, popping or loading a new matrix! e.g.


gl.glMatrixMode(GL.GL_TEXTURE);
gl.glActiveTexture(GL.GL_TEXTURE0);
gl.glPushMatrix();
gl.glLoadIdentity();

...

gl.glPopMatrix();


Misunderstanding this feature can also easily lead to stack under flow if you change the active texture in between push and pop pairs.
Of course in retrospect, it's obvious that each texture unit needs its own matrix, but it's just one of those little details that's not well described in the man pages.

Text rendering

I can't think of many (or any!) games that don't have any need for text. From displaying your vital stats to pointing the player in the right direction to providing subtitles for those with hearing difficulties, it's hard to escape at least 5000 years of written tradition.

Rendering text in OpenGL poses some issues. There's no built in text rendering facility, though there are several utility libraries that give you varying degrees of control. But to go old-school and implement your own basic text functions will give you the ultimate control and help you earn serious respect for font designers.

There are two approaches to rendering text - vector and bitmap. Modern computing has sidelined the bitmap font in favor of structured vector fonts, but bitmaps have several advantages in OpenGL - they can be conveniently represented in a texture, and they're fast. And, you can easily customise them, adding colour, embossing, or even pixel shader effects such as normal bump mapping that literally make your text stand up off the page.

I've chosen a small-caps font. This means I have to draw fewer glyphs and kern fewer character pairs (more later on kerning). Here's my font texture, fresh out of photoshop:
Notice the classy embossed faux-gold effect ;o)

The characters are in an 8 by 8 grid, giving me 64 characters to play with. Now its a simple matter to load this texture, and render texture mapped quads for each character in a string:

private void drawChar(GL gl, int index, float x, float y, float size) {
float tx, ty;
tx = (index % 8) / 8.0f;
ty = (index / 8) / 8.0f;
gl.glBegin(GL.GL_QUADS);
{
gl.glTexCoord2f(tx, ty);
gl.glVertex2f(x, y + 0.8f * size);

gl.glTexCoord2f(tx, ty + 1.0f / 8.0f);
gl.glVertex2f(x, y - 0.2f * size);

gl.glTexCoord2f(tx + 1.0f / 8.0f, ty + 1.0f / 8.0f);
gl.glVertex2f(x + size, y - 0.2f * size);

gl.glTexCoord2f(tx + 1.0f / 8.0f, ty);
gl.glVertex2f(x + size, y + 0.8f * size);
}
gl.glEnd();
}

But hang on a second, we can do better than that. Used naively, we'd get ugly monospaced text, where consecutive i's look stranded and the W's are almost overlapping. We need kerning.

To get the kerning right is pretty hard. It's a subjective matter that requires an artful eye, but I thought I'd give it a go anyway. I wrote a quick app that displays grids of letter pairs, and allows the user to adjust the kerning by left and right clicking:


So - nearly 2000 kerning pairs later, we have a variable width font that can display whatever strings, in whatever size is needed.

The GLText class can be found here in the JavaPop project (though it is pretty standalone if you want to re-use it; you'll just have to pick somewhere in your project to keep the kerning2.dat file) and the kerning tool is in the JavaPopTools project. Let me know if you have any similar bitmap fonts that need kerning and maybe I'll see what I can do.

Saturday, January 24, 2009

JavaPop update

It's been quiet here but I've been busy working away on JavaPop. Several new features have come together recently, so I've released a new .jar so you can try earthquakes, lightning, volcanoes and so on for yourself. Give it a whirl and tell me which area I should work on next.

While there are buttons for all the effects from Populous II: Trials of the Olympian Gods, only the raise/lower land, move papal magnet, swamp, earthquake, batholith, volcano and basalt do anything at the moment. And there are no victory conditions, although there is a very dumb AI to play against. He will try to expand his settlement, but will not attack you yet.

Saturday, January 3, 2009

Models

It can get pretty difficult to generate all your geometry with calls to glVertex(). Wouldn't it be nice to use a 3d modeling app to design and texture your models, and then be able to render those models from Java?

Well - OK then, you've twisted my arm. Example source is at Tutorial 5. The fundamentals here are creating a file that contains four things - vertex positions, normals, texture coordinates, and triangle indices - and then importing it in Java. I used Blender, but you should be able to use 3D Studio, Lightwave or your favourite modeler. I'm not going to go into detail about using the modeler but I will point you to these fine Blender tutorials:
  1. Introduction to Character Animation
  2. UV Mapping Basics
Once you have your texture mapped, normal applied model, we need to save it in a format that contains the above mentioned four sets of data in a reasonable format. Even though we're using OpenGL, the DirectX .x format is a reasonable choice. It contains all of the above in a nice plain text format that's easy to read in. A hint though: in Blender, go back to Object mode and select the object you wish to export before using the .x exporter. Using the exporter in Edit mode ignores the normals and provides different texture coords, at least for me.

If you want to skip the model creation bit, you can use the very simple model I created. Just don't laugh too much.

To load the model, I've rolled the input and rendering into a class called XModel. It *very* lazily parses the .x file, and I can't guarantee that it will work for anything but the simplest models produced by the blender exporter. The key points to watch are the parse*() methods, which each take a portion of the file and fill an ArrayList with data. Then these are cut down to size by deduplicate(), which merges identical vertices, and finally passed to fillbuffer():
private void fillBuffer() {
b = BufferUtil.newFloatBuffer(triangleCount * 3 * vertexstride);
int index;
Vector8 v;
for (int i = 0; i < indices.size(); i++) {
index = indices.get(i);
v = vertices.get(index);
b.put(v.x);
b.put(v.y);
b.put(v.z);
b.put(v.nx);
b.put(v.ny);
b.put(v.nz);
b.put(v.tx);
b.put(v.ty);
}
}


This just gives us a plain old FloatBuffer that we can pass to glDrawArrays. After loading and enabling the texture (using TextureIO which I suppose is sort of cheating ;o) we get:



I said don't laugh! By the way, I'm open to assistance with the graphics and models for JavaPop. Any aspiring artists out there who want to draw 5 or so frames of walking and a dozen or so houses please leave a comment!

Tuesday, December 23, 2008

MultiTexturing and shaders

Textures are nice and all, but one at a time is a bit boring. There are lots of situations that call for multiple textures on a triangle, or just having multiple textures available on separate units to reduce the amount of swapping.
There are many multitexturing effects you can achieve with the fixed function pipeline, but things start getting really cool when you add shaders. These are programs that take over the tasks of transforming vertices and calculating pixel colours in arbitrary ways, allowing techniques such as bump mapping, environment mapping, blurs, bloom and more. Shaders and multitexturing go hand in hand so I'll tackle them both in one post. Source is available as Tutorial 4

Following on from the simple texturing tutorial, we'll make some minor changes. Starting at the beginning:
public void init(GL gl) {
GLHelper glh = new GLHelper();
try {
shader = glh.LoadShaderProgram(gl, "/tutorial4/vertex.shader", "/tutorial4/fragment.shader");
} catch (IOException ex) {
Logger.getLogger(TutorialObject.class.getName()).log(Level.SEVERE, null, ex);
} catch (GLHelperException ex) {
Logger.getLogger(TutorialObject.class.getName()).log(Level.SEVERE, null, ex);
}
gl.glGenTextures(2, textures, 0);
gl.glBindTexture(GL.GL_TEXTURE_2D, textures[0]);
ByteBuffer b = genTexture(32);
gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA8, 32, 32, 0, GL.GL_RGB, GL.GL_BYTE, b);

gl.glBindTexture(GL.GL_TEXTURE_2D, textures[1]);
b = genTexture(32);
gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA8, 32, 32, 0, GL.GL_RGB, GL.GL_BYTE, b);
}
GLHelper is a utility class to hide some of the complexity of loading a shader. Then we ask for 2 texture handles. We then bind them in turn and fill them with data. This all goes on in texture unit zero, as we don't need to render them in parallel yet.

Then when we draw -

public void display(GL gl, float time) {
int[] i = new int[1];
gl.glGetIntegerv(GL.GL_CURRENT_PROGRAM, i, 0);
if (i[0] != shader) {
gl.glUseProgram(shader);
}

gl.glEnable(GL.GL_TEXTURE_2D);
gl.glActiveTexture(GL.GL_TEXTURE0);
gl.glBindTexture(GL.GL_TEXTURE_2D, textures[0]);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
gl.glActiveTexture(GL.GL_TEXTURE1);
gl.glBindTexture(GL.GL_TEXTURE_2D, textures[1]);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);

gl.glUniform2f(gl.glGetUniformLocation(shader, "offset"), time, time);
gl.glUniform1i(gl.glGetUniformLocation(shader, "tex1"), 0);
gl.glUniform1i(gl.glGetUniformLocation(shader, "tex2"), 1);
gl.glUniform1f(gl.glGetUniformLocation(shader, "r"), time);

gl.glBegin(GL.GL_TRIANGLES);
{
gl.glVertex3f(0, 1, -3);
gl.glVertex3f(-1, -1, -3);
gl.glVertex3f(1, -1, -3);
}
gl.glEnd();
}
The first block checks to see if the shader program is active, and if not, activates it.

The second block enables texturing and binds the textures to the first two units. It also sets up bilinear filtering.

The third block sets some variables that are used by the shaders themselves. These are all uniform variables - they cannot be changed between a glBegin/glEnd pair, but there are also attribute variables that can be applied per vertex. Lighthouse 3d has some good background info.

The actual drawing is actually simpler than the last tutorial - just three vertices. That's possible because the vertex shader calculates the texture coordinates from the vertex coordinates, so they don't have to be specified. Obviously this won't work for every situation, but it's a good example of how the whole fixed function pipeline has been replaced. Those shaders in full:

Vertex:
uniform float r;

void main(void)
{
vec4 v = gl_Vertex;
v.y += 0.3*cos(r+v.x);
gl_Position = gl_ModelViewProjectionMatrix * v;
gl_TexCoord[0].xy = gl_Vertex.xy;
gl_TexCoord[1].xy = gl_Vertex.xy;
}
Fragment:
uniform sampler2D tex1;
uniform sampler2D tex2;
uniform vec2 offset;
void main(void)
{
vec4 c;
c = texture2D(tex1, gl_TexCoord[0].st);
c+= texture2D(tex2, offset+gl_TexCoord[1].st);
gl_FragColor = c / 2.0 ;
}
The vertex shader applies a cosine wave to the y coordinate of each vertex - and the fragment shader blends two textures and applies an offset. The overall effect is a wavy, shimmering triangle. You'll have to run this one to see it in motion, I think!

Texture mapping

Sooner or later everyone gets bored of plain coloured triangles. Texturing in OpenGL is easy enough once you've mastered a few concepts:
  • Texture names
  • Filtering
This post (and the accompanying source code) also builds slightly on the previous tutorial - the rendering we're interested in is now in TutorialObject.java. The Canvas object holds a list of all objects and calls each one to render itself.

Texture names are actually numbers. The numbers are generated by glGenTextures and returned into an array like so:
int[] textures = new int[1];
gl.glGenTextures(1, textures, 0);
The numbers returned in textures[] are effectively handles to textures. At the moment, they are not attached to any data - they are not even declared as 1 or 2 dimensional. To attach actual data, we must bind the texture:
gl.glBindTexture(GL.GL_TEXTURE_2D, textures[0]);
then we can load our image data in with
gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA8, 32, 32, 0, GL.GL_RGB, GL.GL_BYTE, b);
Hang on a minute, where do those parameters come from? The full details are at the OpenGL man pages, but the key ones are the 32s and b. This means we are defining a 32 square texture, and the data defining it is in the buffer b. In this example, b is just full of random data, so the texture looks like disco lights.

If we tried to render a triangle now, it still wouldn't be textured. We need to enable texturing and choose a filtering mode. So at the beginning of our object rendering code, we make these calls:
gl.glEnable(GL.GL_TEXTURE_2D);
gl.glBindTexture(GL.GL_TEXTURE_2D, textures[0]);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
Note that I re-bind the texture. This is because in a realistic program, different objects will need different textures and you won't know which texture is active at the beginning of an object's display method.
Now we can render vertices and expect to see our texture.

gl.glBegin(GL.GL_TRIANGLES);
{
gl.glTexCoord2f(0.0f,0.0f);
gl.glVertex3f(0.0f, 1.0f, -3.0f);

gl.glTexCoord2f(1.0f,0.0f);
gl.glVertex3f(-1.0f, -1.0f, -3.0f);

gl.glTexCoord2f(0.0f,1.0f);
gl.glVertex3f(1.0f, -1.0f, -3.0f);
}
gl.glEnd();
We need to pass the texture coordinates before the vertex coordinates - when a vertex is defined it picks up the last specified texture coordinates, color, normal etc.

Friday, December 19, 2008

Basic OpenGL set up

So you have a skeleton application compiling and running, you've got a subversion repository to commit your code to, and you're ready to start rendering some 3D graphics to see what your game could look like. Great! Let's start at the beginning.

The init method

The first opportunity to execute any OpenGL calls is your canvas's init method (technical note - the method is actually an implementation of the GLEventListener interface, not the GLCanvas class). This is called when the underlying OpenGL context is created (and could conceivably be called again if the native context was lost and re-created, but it's generally OK to consider it a one-time deal). You can use this method to set up anything that will stay constant over the lifetime of the application. Tutorial 2 calls gl.setSwapInterval(1) which makes GL wait for the vertical blank interval before swapping the front and back buffers. This reduces shearing and allows for silky-smooth animation, and also prevents your game from trying to render frames more frequently than the monitor can physically display them.
init is also a good place to load textures, set up display lists and vertex arrays, and set global lighting parameters like the ambient light level.

The reshape method

The second opportunity to execute GL calls is in reshape. This is called whenever the viewport changes, and once before the first call to display. This is where you set up the viewport and the GL_PERSPECTIVE matrix, and it's as good a place as any to set the default GL_MODELVIEW matrix. Combined, these three settings are like defining the camera in your scene, but it can do things that no physical camera can.

The viewport is the 2d window that OpenGL will draw to. display is called with height and width parameters, and in most cases I can think of these are just passed straight through to glViewport. The only exception I can think of to that is if you are managing multiple views in the same context, and you wish to render to several different viewports. However I would look into multiple GLCanvas instances first.

On to the matrices. Despite the name, GL_PERSPECTIVE doesn't have to be a perspective transformation at all. Though a perspective view makes sense in a lot of games, if you're planning an RTS or other top-down view, or even a totally 2D game (OpenGL turns out to be great for 2d too -), then an orthogonal transform is what you want. There are two helper functions to create matrices, as otherwise the math can get tricky:
  • glFrustum. A Frustum is a shape like an Incan pyramid, and defines the viewing volume for a perspective view, i.e. things further away are smaller. Suitable for first person shooters, many third person games, and anything where realism is key. 
  • glOrtho. Ortho is short for Orthogonal, meaning that vectors that were at right angles before the tranformation will still be at right angles afterwards. Take a moment to think why perspective transforms are not orthogonal. This function sets up the camera for a parallel projection, i.e. things further do not get smaller. This is handy for 2d games, or top down god games, RTSs and surprisingly more gameplay styles.
Both functions produce a matrix that defines the viewing volume. This is the volume of space that is visible to the camera. Note that it has a front and back - like a real camera, if something is too close to the lens you can't see it very well, and unlike a real camera there is a maximum distance beyond which OpenGL will ignore objects. There is a problem with setting the near plane to 0.00001 and the far plane to 9999999 though - depth buffer precision. Depth buffering is a fantastically useful technique we'll cover in detail later, but for now let's just note that we don't want the ratio of near:far to get too large, or you'll see rendering artifacts.

glFrustum takes six parameters. The first five define the position of the front face of the viewing volume - left, right, bottom, top and near. The sixth sets the far distance. The shape of the front face should be the same as the shape of your viewport - if the user drags out a tall, skinny window, then to prevent distorting your graphics, the view volume should be tall and skinny too, like in the following code:
float aspect = (1.0f) * height / width;
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glLoadIdentity();
gl.glFrustum(.5f, -.5f, -.5f * aspect, .5f * aspect, 1.f, 500.f);
This sets up a pretty standard perspective view, where a square of side 1 would fill the screen if it was centered one unit in front of the camera - as long as the screen is square.

glOrtho takes six parameters too - in fact, the same ones. But the near face of this viewing volume will be the same size as the far face - so it probably needs to be much larger to be able the view the same objects - e.g:
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrtho(-width / 64.0f, width / 64.0f, -height / 64.0f, height / 64.0f, 1, 100);
This view would show each unit square as a square 64 pixels across, no matter how large the screen is. Resizing the window would show more of the world, rather than scaling up the same view. It's the camera matrix that JavaPop uses.

Now as for GL_MODELVIEW, all we want to do is set it to the identity matrix, to make sure it's initialised:
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glLoadIdentity();
Now the OpenGL context is set up and ready for you to draw your geometry in the display method. We'll get to that very soon.

Image editor

Most games need some graphics. Unless you plan to draw absolutely every element in your game as flat shaded 3d primitives, you'll need textures, custom mouse cursors, and who knows what else.

The best paint package I've ever used was Cloanto's Personal Paint. But that was on the Amiga more than a decade ago, so unless you have one of those lying around (or UAE) there are a few more modern options.
  1. Photoshop. The grand daddy of image editors, but expensive and burdened with what I find to be too many features. But then again maybe I just sucjk at photoshop...
  2. The Gimp. Apart from the scary name that got me a few funny looks from my wife when she saw it on my desktop, a pretty good editor for programmers.
  3. Online editors. These are getting better and better. No installation, so if you find yourself at the in-laws over thanksgiving or Christmas you can still knock out a few icons or textures. I like pixlr and splashup
I would recommend spending only as much time drawing your graphics as you find fun. It's really easy to go back and improve them - and it's also really easy to change the design or display method of your game so that it needs different graphics, so don't commit too much time up front. For this reason I recommend starting with pixlr or splashup just to creat some stand in graphics, then revisiting then later with the Gimp. If a guy in a suit with a briefcase full of cash turns up and wants to publish your opus, then it's time to splash out on photoshop.

In any case, your goal will be to produce, in most cases, a 24-bit .png file with an 8-bit alpha channel. This is a really flexible format, well supported by both image editors and java. That's why PNG stands for Portable Network Graphics!

Wednesday, December 17, 2008

Version control

So, you've got Netbeans set up and you're ready to get coding. Hold those horses! Consider setting up some version control first.

Version control means saving your code (and images, models, and everything else) in a database. If you drop your hard drive or your PC gets hit by lightning and you're using remote version control, then no drama - just get a new PC, check out the latest version of your code and carry on. Or if you delete a big chunk of code you don't need any more, then remember that little bit in the middle that took hours to get right - no problem, just step back to an older revision and look at last months code.

Version control used to be a bit tricky, requiring a local install of cvs and some serious unix skills to keep it running, but these days several companies offer free, simple subversion services. Chief among these are Sourceforge and Google Code - both are open source. which means when you save your work there, the rest of the world can see, learn from and use that code. If you're uneasy about that, spend some time here then come back.

I'm going to go through the steps required to get your project hosted on Google Code. Not only is this really useful for development, but it can also act as a community hub and download site for your game when it's released!

Go to http://code.google.com/hosting/ and click on the Create a new project link. Type in your projects name (it will have to be unique across the google code site) and a brief description and click Create Project - and guess what - you're done. I said it was simple. Click on the Settings link in the top right and make a note of your googlecode password. This isn't your normal google password, it's your passport to the subversion repository.

Now to import your work into the new repository. There's a decision to be made here, unfortunately. By convention, svn repositories are laid out a certain way, but Netbeans rides roughshod over that and just plunks the whole project directory into the root. I recommend a middle ground, laying out the repository like so:
  • /svn (root)
    • branches
    • tags
    • trunk
      • Project1
        • src, test, etc
      • Project2
        • src, test, etc
This will work just fine and let you store multiple projects in one google code project. Have a look at the code for this blog at http://code.google.com/p/javagamedev/source/browse/ to get the idea. I've put a project in there called Tutorial 0 which is simply the default Netbeans project, in the right place. Follow the checkout instructions at the end of this post to get it - though you'll only be able to use http, not https.

Go back to NetBeans, right click on your project in the project tree, and choose Versioning->Import into Subversion Repository...
In the space for the repository URL, type "https://myprojectname.googlecode.com/svn", type your google account email address in the username field and in the password field, enter the googlecode password on your settings page from earlier.
On the next page, change the directory from "myproject" to "trunk/myproject" and enter a simple message - something like "New project" is plenty. Now hit finish and the project will be imported into subversion.

If you want to work on the project on a different computer, install netbeans there too and use the Versioning->Subversion->Checkout command. Enter the repository, user name and password info as above (remembering httpS - or you won't be able to write back your changes) and hit next. On the next screen, use the browse button to select the project in trunk/ or just type trunk/projectname, and select your net beans project dir as the local directory.
Leave the skip... tickbox unticked, and the scan... tickbox checked, and hit finish. Netbeans will check out the project and ask if you want to open the project it found - answer yes and hey presto, we're there.

Now we can go coding off into the sunset without worrying about losing our data.

Fullscreen or windowed?

Quick answer - both! I think that Java provides the most transparent way of handling fullscreen graphics that I've come across. Whether we're fullscreen or windowed, we need to set up an application window (JFrame) and put an OpenGL canvas (GLCanvas) in it. To go fullscreen, we use the GraphicsDevice class, which can be used with OpenGL or without (just in case you want a fullscreen accounting package?)

There are a few steps to turn your app into a fullscreen one:
  1. Remove decorations from your window. If you don't do this, you'll still see a title bar, and users will be able to move the window about on a black background. Not very pro.
  2. Set the full screen window to your JFrame.
  3. Choose a display mode - ideally you'll provide a way for the user to choose this, but to start with there's nothing wrong with hard-coding a resolution you know your machine can handle. Note - you can't change the display mode before setting the full screen window.
  4. Provide a way to get of of fullscreen mode! This should really be zero on the list. As a last resort, you should be able to Alt-F4 or ⌘-Q out of it, but debugging in fullscreen mode can be a pain unless you have a second computer. Fortunately you can debug in windowed mode, and just switch to fullscreen for serious playing.
Lets implement it. The full code can be found at
svn checkout "http://javagamedev.googlecode.com/svn/trunk/Tutorial 2"
and we'll end up with this: More exciting than a grey screen, no? Lets look at the important details. There are two classes this time - TutorialFrame and TutorialCanvas. The Frame class is the top level window. It controls whether the application runs full screen or not -
private void init(boolean bFullScreen) {
fullscreen = bFullScreen;
setUndecorated(bFullScreen);
setSize(800, 600);
setVisible(true);
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow(bFullScreen ? this : null);
canvas.requestFocus();
}
By default the frame starts up windowed, but a quick call to
public void setFullscreen(boolean bFullscreen) {
if (fullscreen != bFullscreen) {
this.dispose();
init(bFullscreen);
}
}

switches between windowed or fullscreen. Note the call to dispose() - without this, the frame is "displayable", and any call to setUndecorated() will fail.

The Canvas class actually performs the rendering, and apart from reacting to a reshape, carries on blissfully ignorant of whether it's full screen or windowed. The Canvas implements KeyListener so that it can call the Frame's setFullscreen method:
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_F:
tFrame.setFullscreen(!tFrame.fullscreen);
break;
case KeyEvent.VK_ESCAPE:
tFrame.setFullscreen(false);
}
}
Nice and easy. Practise checking it out, and try to change the spinning shape being rendered - maybe a cube? Hint: you might need a depth buffer!