From 41cd6c47b23975098cd155517790e018670785e7 Mon Sep 17 00:00:00 2001
From: Kenneth Russel <kbrussel@alum.mit.edu>
Date: Mon, 15 Jun 2009 23:12:27 +0000
Subject: Copied JOGL_2_SANDBOX r350 on to trunk; JOGL_2_SANDBOX branch is now
 closed

git-svn-id: file:///usr/local/projects/SUN/JOGL/git-svn/../svn-server-sync/jogl-demos/trunk@352 3298f667-5e0e-4b4a-8ed4-a3559d26a5f4
---
 src/demos/util/Bunny.java             | 141 +++++++
 src/demos/util/Cubemap.java           |  83 ++++
 src/demos/util/DurationTimer.java     |  68 ++++
 src/demos/util/DxTex.java             | 376 ++++++++++++++++++
 src/demos/util/FPSCounter.java        | 228 +++++++++++
 src/demos/util/FileUtils.java         |  68 ++++
 src/demos/util/FloatList.java         | 100 +++++
 src/demos/util/IntList.java           | 100 +++++
 src/demos/util/MD2.java               | 702 ++++++++++++++++++++++++++++++++++
 src/demos/util/ObjReader.java         | 368 ++++++++++++++++++
 src/demos/util/ScreenResSelector.java | 241 ++++++++++++
 src/demos/util/SystemTime.java        | 110 ++++++
 src/demos/util/Time.java              |  52 +++
 src/demos/util/Triceratops.java       | 144 +++++++
 14 files changed, 2781 insertions(+)
 create mode 100644 src/demos/util/Bunny.java
 create mode 100755 src/demos/util/Cubemap.java
 create mode 100644 src/demos/util/DurationTimer.java
 create mode 100644 src/demos/util/DxTex.java
 create mode 100755 src/demos/util/FPSCounter.java
 create mode 100755 src/demos/util/FileUtils.java
 create mode 100644 src/demos/util/FloatList.java
 create mode 100644 src/demos/util/IntList.java
 create mode 100644 src/demos/util/MD2.java
 create mode 100644 src/demos/util/ObjReader.java
 create mode 100755 src/demos/util/ScreenResSelector.java
 create mode 100644 src/demos/util/SystemTime.java
 create mode 100644 src/demos/util/Time.java
 create mode 100644 src/demos/util/Triceratops.java

(limited to 'src/demos/util')

diff --git a/src/demos/util/Bunny.java b/src/demos/util/Bunny.java
new file mode 100644
index 0000000..c4afd0c
--- /dev/null
+++ b/src/demos/util/Bunny.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import java.io.*;
+
+import javax.media.opengl.*;
+
+/** Renders a bunny.
+
+   <P> This file was produced by 3D Exploration Plugin: CPP Export filter.
+
+   <P> 3D Exploration
+
+   <P> Copyright (c) 1999-2000 X Dimension Software
+
+   <P>WWW         http://www.xdsoft.com/explorer/ <BR>
+   eMail       info@xdsoft.com
+*/
+public class Bunny {
+
+  /** Generates and returns a display list for the bunny model. */
+  public static int gen3DObjectList(GL2 gl) throws IOException {
+    StreamTokenizer tok = new StreamTokenizer(new BufferedReader(new InputStreamReader(
+      Bunny.class.getClassLoader().getResourceAsStream("demos/data/models/bunny.txt"))));
+    // Reset tokenizer's syntax so numbers are not parsed
+    tok.resetSyntax();
+    tok.wordChars('a', 'z');
+    tok.wordChars('A', 'Z');
+    tok.wordChars('0', '9');
+    tok.wordChars('-', '-');
+    tok.wordChars('.', '.');
+    tok.wordChars(128 + 32, 255);
+    tok.whitespaceChars(0, ' ');
+    tok.whitespaceChars(',', ',');
+    tok.whitespaceChars('{', '{');
+    tok.whitespaceChars('}', '}');
+    tok.commentChar('/');
+    tok.quoteChar('"');
+    tok.quoteChar('\'');
+    tok.slashSlashComments(true);
+    tok.slashStarComments(true);
+
+    // Read in file
+    int numFaceIndices = nextInt(tok, "number of face indices");
+    short[] faceIndices = new short[numFaceIndices * 6];
+    for (int i = 0; i < numFaceIndices * 6; i++) {
+      faceIndices[i] = (short) nextInt(tok, "face index");
+    }
+    int numVertices = nextInt(tok, "number of vertices");
+    float[] vertices = new float[numVertices * 3];
+    for (int i = 0; i < numVertices * 3; i++) {
+      vertices[i] = nextFloat(tok, "vertex");
+    }
+    int numNormals = nextInt(tok, "number of normals");
+    float[] normals = new float[numNormals * 3];
+    for (int i = 0; i < numNormals * 3; i++) {
+      normals[i] = nextFloat(tok, "normal");
+    }
+
+    int lid = gl.glGenLists(1);
+    gl.glNewList(lid, GL2.GL_COMPILE);
+
+    gl.glBegin(GL.GL_TRIANGLES);
+    for (int i = 0; i < faceIndices.length; i += 6) {
+      for (int j = 0; j < 3; j++) {
+        int vi = faceIndices[i + j];
+        int ni = faceIndices[i + j + 3];
+        gl.glNormal3f(normals[3 * ni],
+                      normals[3 * ni + 1],
+                      normals[3 * ni + 2]);
+        gl.glVertex3f(vertices[3 * vi],
+                      vertices[3 * vi + 1],
+                      vertices[3 * vi + 2]);
+      }
+    }
+    gl.glEnd();
+
+    gl.glEndList();
+    return lid;
+  }
+
+  private static int nextInt(StreamTokenizer tok, String error) throws IOException {
+    if (tok.nextToken() != StreamTokenizer.TT_WORD) {
+      throw new IOException("Parse error reading " + error + " at line " + tok.lineno());
+    }
+    try {
+      return Integer.parseInt(tok.sval);
+    } catch (NumberFormatException e) {
+      throw new IOException("Parse error reading " + error + " at line " + tok.lineno());
+    }
+  }
+
+  private static float nextFloat(StreamTokenizer tok, String error) throws IOException {
+    if (tok.nextToken() != StreamTokenizer.TT_WORD) {
+      throw new IOException("Parse error reading " + error + " at line " + tok.lineno());
+    }
+    try {
+      return Float.parseFloat(tok.sval);
+    } catch (NumberFormatException e) {
+      throw new IOException("Parse error reading " + error + " at line " + tok.lineno());
+    }
+  }
+}
diff --git a/src/demos/util/Cubemap.java b/src/demos/util/Cubemap.java
new file mode 100755
index 0000000..405fc38
--- /dev/null
+++ b/src/demos/util/Cubemap.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2003-2005 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import com.sun.opengl.util.FileUtil;
+import com.sun.opengl.util.texture.Texture;
+import com.sun.opengl.util.texture.TextureData;
+import com.sun.opengl.util.texture.TextureIO;
+import java.io.IOException;
+import javax.media.opengl.GL;
+import javax.media.opengl.GLException;
+
+
+
+/** Helper class for loading cubemaps from a set of textures. */
+
+public class Cubemap {
+    
+  private static final String[] suffixes = { "posx", "negx", "posy", "negy", "posz", "negz" };
+  private static final int[] targets = { GL.GL_TEXTURE_CUBE_MAP_POSITIVE_X,
+                                         GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
+                                         GL.GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
+                                         GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
+                                         GL.GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
+                                         GL.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z };
+
+  public static Texture loadFromStreams(ClassLoader scope,
+                                        String basename,
+                                        String suffix,
+                                        boolean mipmapped) throws IOException, GLException {
+    Texture cubemap = TextureIO.newTexture(GL.GL_TEXTURE_CUBE_MAP);
+
+    for (int i = 0; i < suffixes.length; i++) {
+      String resourceName = basename + suffixes[i] + "." + suffix;
+      TextureData data = TextureIO.newTextureData(scope.getResourceAsStream(resourceName),
+                                                  mipmapped,
+                                                  FileUtil.getFileSuffix(resourceName));
+      if (data == null) {
+        throw new IOException("Unable to load texture " + resourceName);
+      }
+      cubemap.updateImage(data, targets[i]);
+    }
+
+    return cubemap;
+  }
+}
diff --git a/src/demos/util/DurationTimer.java b/src/demos/util/DurationTimer.java
new file mode 100644
index 0000000..c0f88e7
--- /dev/null
+++ b/src/demos/util/DurationTimer.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+/** Simple class for helping measure frames-per-second. */
+
+public class DurationTimer {
+  private long startTime;
+  private long accumulatedTime;
+
+  public void reset() {
+    accumulatedTime = 0;
+  }
+
+  public void start() {
+    startTime = System.currentTimeMillis();
+  }
+
+  public void stop() {
+    long curTime = System.currentTimeMillis();
+    accumulatedTime += (curTime - startTime);
+  }
+
+  public long getDuration() {
+    return accumulatedTime;
+  }
+
+  public float getDurationAsSeconds() {
+    return (float) accumulatedTime / 1000.0f;
+  }
+}
diff --git a/src/demos/util/DxTex.java b/src/demos/util/DxTex.java
new file mode 100644
index 0000000..fbe1963
--- /dev/null
+++ b/src/demos/util/DxTex.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import java.io.*;
+import java.nio.*;
+import java.awt.image.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.filechooser.*;
+
+import com.sun.opengl.util.texture.spi.*;
+
+/** Simplified clone of DxTex tool from the DirectX SDK, written in
+    Java using the DDSImage; tests fetching of texture data */
+
+public class DxTex {
+  private InternalFrameListener frameListener;
+  private File defaultDirectory;
+  private JDesktopPane desktop;
+  private static String endl = System.getProperty("line.separator");
+  private JMenu mipMapMenu;
+
+  public static void main(String[] args) {
+    new DxTex().run(args);
+  }
+
+  private void run(String[] args) {
+    defaultDirectory = new File(System.getProperty("user.dir"));
+    JFrame frame = new JFrame("DirectX Texture Tool");
+    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+    JMenuBar menuBar = new JMenuBar();
+    JMenu menu = createMenu("File", 'F', 0);
+    JMenuItem item =
+      createMenuItem("Open...",
+                     new ActionListener() {
+                         public void actionPerformed(ActionEvent e) {
+                           openFile();
+                         }
+                       },
+                     KeyEvent.VK_O, InputEvent.CTRL_MASK,
+                     'O', 0);
+    menu.add(item);
+    item =
+      createMenuItem("Exit",
+                     new ActionListener() {
+                         public void actionPerformed(ActionEvent e) {
+                           System.exit(0);
+                         }
+                       },
+                     KeyEvent.VK_Q, InputEvent.CTRL_MASK,
+                     'x', 1);
+    menu.add(item);
+    menuBar.add(menu);
+
+    menu = createMenu("MipMap", 'M', 0);
+    menu.setEnabled(false);
+    mipMapMenu = menu;
+    menuBar.add(menu);
+
+    frame.setJMenuBar(menuBar);
+
+    desktop = new JDesktopPane();
+    frame.getContentPane().add(desktop);
+    frame.setSize(640, 480);
+    frame.setVisible(true);
+
+    frameListener = new InternalFrameAdapter() {
+        public void internalFrameActivated(InternalFrameEvent e) {
+          JInternalFrame ifr = e.getInternalFrame();
+          if (ifr instanceof ImageFrame) {
+            // Recompute entries in mip map menu
+            final ImageFrame frame = (ImageFrame) ifr;
+            if (frame.getNumMipMaps() > 0) {
+              mipMapMenu.removeAll();
+              // Add entries
+              for (int i = 0; i < frame.getNumMipMaps(); i++) {
+                final int map = i;
+                JMenuItem item;
+                String title = "Level " + (i + 1);
+                ActionListener listener = new ActionListener() {
+                    public void actionPerformed(ActionEvent e) {
+                      SwingUtilities.invokeLater(new Runnable() {
+                          public void run() {
+                            frame.setMipMapLevel(map);
+                          }
+                        });
+                    }
+                  };
+                if (i < 9) {
+                  char c = (char) ('0' + i + 1);
+                  item = createMenuItem(title, listener, c, 6);
+                } else {
+                  item = createMenuItem(title, listener);
+                }
+                mipMapMenu.add(item);
+              }
+              mipMapMenu.setEnabled(true);
+            } else {
+              mipMapMenu.setEnabled(false);
+            }
+          } else {
+            mipMapMenu.setEnabled(false);
+          }
+        }
+
+        public void internalFrameClosing(InternalFrameEvent e) {
+          desktop.remove(e.getInternalFrame());
+          desktop.invalidate();
+          desktop.validate();
+          desktop.repaint();
+          // THIS SHOULD NOT BE NECESSARY
+          desktop.requestFocus();
+        }        
+
+        public void internalFrameClosed(InternalFrameEvent e) {
+          JInternalFrame ifr = e.getInternalFrame();
+          if (ifr instanceof ImageFrame) {
+            ((ImageFrame) ifr).close();
+          }
+        }
+      };
+
+    for (int i = 0; i < args.length; i++) {
+      final File file = new File(args[i]);
+      SwingUtilities.invokeLater(new Runnable() {
+          public void run() {
+            openFile(file);
+          }
+        });
+    }
+  }
+
+  //----------------------------------------------------------------------
+  // Actions
+  //
+
+  private void openFile() {
+    JFileChooser chooser = new JFileChooser(defaultDirectory);
+    chooser.setMultiSelectionEnabled(false);
+    chooser.addChoosableFileFilter(new javax.swing.filechooser.FileFilter() {
+        public boolean accept(File f) {
+          return (f.isDirectory() ||
+                  f.getName().endsWith(".dds"));
+        }
+        
+        public String getDescription() {
+          return "Texture files (*.dds)";
+        }
+      });
+
+    int res = chooser.showOpenDialog(null);
+    if (res == JFileChooser.APPROVE_OPTION) {
+      final File file = chooser.getSelectedFile();
+      defaultDirectory = file.getParentFile();
+      SwingUtilities.invokeLater(new Runnable() {
+          public void run() {
+            openFile(file);
+          }
+        });
+    }
+  }
+
+  private void openFile(File file) {
+    try {
+      DDSImage image = DDSImage.read(file);
+      showImage(file.getName(), image, 0);
+    } catch (IOException e) {
+      showMessageDialog("Error while opening file:" + endl +
+                        exceptionToString(e),
+                        "Error opening file",
+                        JOptionPane.WARNING_MESSAGE);
+    }
+  }
+
+  //----------------------------------------------------------------------
+  // Image display
+  //
+
+  private void showImage(String filename, DDSImage image, int mipMapLevel) {
+    try {
+      ImageFrame fr = new ImageFrame(filename, image, mipMapLevel);
+      desktop.add(fr);
+      fr.setVisible(true);
+    } catch (Exception e) {
+      showMessageDialog("Error while loading file:" + endl +
+                        exceptionToString(e),
+                        "Error loading file",
+                        JOptionPane.WARNING_MESSAGE);
+    }
+  }
+
+  class ImageFrame extends JInternalFrame {
+    private String filename;
+    private DDSImage image;
+    private int mipMapLevel;
+    private int curWidth;
+    private int curHeight;
+    private JLabel label;
+
+    ImageFrame(String filename, DDSImage image, int mipMapLevel) {
+      super();
+      this.filename = filename;
+      this.image = image;
+
+      addInternalFrameListener(frameListener);
+      label = new JLabel();
+      JScrollPane scroller = new JScrollPane(label);
+      getContentPane().add(scroller);
+      setSize(400, 400);
+      setResizable(true);
+      setIconifiable(true);
+      setClosable(true);
+      setMipMapLevel(mipMapLevel);
+    }
+
+    int getNumMipMaps() {
+      return image.getNumMipMaps();
+    }
+
+    void setMipMapLevel(int level) {
+      mipMapLevel = level;
+      computeImage();
+      resetTitle();
+    }
+
+    void close() {
+      System.err.println("Closing files");
+      image.close();
+    }
+
+    private void computeImage() {
+      // Get image data
+      image.getNumMipMaps();
+      DDSImage.ImageInfo info = image.getMipMap(mipMapLevel);
+      int width = info.getWidth();
+      int height = info.getHeight();
+      curWidth = width;
+      curHeight = height;
+      ByteBuffer data = info.getData();
+
+      // Build ImageIcon out of image data
+      BufferedImage img = new BufferedImage(width, height,
+                                            BufferedImage.TYPE_3BYTE_BGR);
+      WritableRaster dst = img.getRaster();
+
+      int skipSize;
+      if (image.getPixelFormat() == DDSImage.D3DFMT_A8R8G8B8) {
+        skipSize = 4;
+      } else if (image.getPixelFormat() == DDSImage.D3DFMT_R8G8B8) {
+        skipSize = 3;
+      } else {
+        image.close();
+        throw new RuntimeException("Unsupported pixel format " + image.getPixelFormat());        
+      }
+
+      for (int y = 0; y < height; y++) {
+        for (int x = 0; x < width; x++) {
+          // NOTE: highly suspicious that A comes fourth in
+          // A8R8G8B8...not really ARGB, but RGBA (like OpenGL)
+          dst.setSample(x, y, 0, data.get(skipSize * (width * y + x) + 2) & 0xFF);
+          dst.setSample(x, y, 1, data.get(skipSize * (width * y + x) + 1) & 0xFF);
+          dst.setSample(x, y, 2, data.get(skipSize * (width * y + x) + 0) & 0xFF);
+        }
+      }
+
+      label.setIcon(new ImageIcon(img));
+    }
+
+    private void resetTitle() {
+      setTitle(filename + " (" + curWidth + "x" + curHeight +
+               ", mipmap " + (1 + mipMapLevel) + " of " +
+               image.getNumMipMaps() + ")");
+    }
+  }
+
+
+  //----------------------------------------------------------------------
+  // Menu and menu item creation
+  //
+
+  private static JMenu createMenu(String name, char mnemonic, int mnemonicPos) {
+    JMenu menu = new JMenu(name);
+    menu.setMnemonic(mnemonic);
+    menu.setDisplayedMnemonicIndex(mnemonicPos);
+    return menu;
+  }
+
+  private static JMenuItem createMenuItem(String name, ActionListener l) {
+    JMenuItem item = new JMenuItem(name);
+    item.addActionListener(l);
+    return item;
+  }
+
+  private static JMenuItem createMenuItemInternal(String name, ActionListener l, int accelerator, int modifiers) {
+    JMenuItem item = createMenuItem(name, l);
+    item.setAccelerator(KeyStroke.getKeyStroke(accelerator, modifiers));
+    return item;
+  }
+
+  private static JMenuItem createMenuItem(String name, ActionListener l, int accelerator) {
+    return createMenuItemInternal(name, l, accelerator, 0);
+  }
+
+  private static JMenuItem createMenuItem(String name, ActionListener l, char mnemonic, int mnemonicPos) {
+    JMenuItem item = createMenuItem(name, l);
+    item.setMnemonic(mnemonic);
+    item.setDisplayedMnemonicIndex(mnemonicPos);
+    return item;
+  }
+
+  private static JMenuItem createMenuItem(String name,
+                                          ActionListener l,
+                                          int accelerator,
+                                          int acceleratorMods,
+                                          char mnemonic,
+                                          int mnemonicPos) {
+    JMenuItem item = createMenuItemInternal(name, l, accelerator, acceleratorMods);
+    item.setMnemonic(mnemonic);
+    item.setDisplayedMnemonicIndex(mnemonicPos);
+    return item;
+  }
+
+  private void showMessageDialog(final String message, final String title, final int jOptionPaneKind) {
+    SwingUtilities.invokeLater(new Runnable() {
+        public void run() {
+          JOptionPane.showInternalMessageDialog(desktop, message, title, jOptionPaneKind);
+        }
+      });
+  }
+
+  private static String exceptionToString(Exception e) {
+    ByteArrayOutputStream bos = new ByteArrayOutputStream();
+    PrintStream s = new PrintStream(bos);
+    e.printStackTrace(s);
+    return bos.toString();
+  }
+}
diff --git a/src/demos/util/FPSCounter.java b/src/demos/util/FPSCounter.java
new file mode 100755
index 0000000..79ea38b
--- /dev/null
+++ b/src/demos/util/FPSCounter.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2007 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import com.sun.opengl.util.awt.TextRenderer;
+import com.sun.opengl.util.texture.Texture;
+import java.awt.Font;
+import java.awt.geom.Rectangle2D;
+import java.text.DecimalFormat;
+import javax.media.opengl.GLDrawable;
+import javax.media.opengl.GLException;
+
+
+
+/** A simple class which uses the TextRenderer to provide an FPS
+    counter overlaid on top of the scene. */
+
+public class FPSCounter {
+  // Placement constants
+  public static final int UPPER_LEFT  = 1;
+  public static final int UPPER_RIGHT = 2;
+  public static final int LOWER_LEFT  = 3;
+  public static final int LOWER_RIGHT = 4;
+
+  private int textLocation = LOWER_RIGHT;
+  private GLDrawable drawable;
+  private TextRenderer renderer;
+  private DecimalFormat format = new DecimalFormat("####.00");
+  private int frameCount;
+  private long startTime;
+  private String fpsText;
+  private int fpsMagnitude;
+  private int fpsWidth;
+  private int fpsHeight;
+  private int fpsOffset;
+  
+  /** Creates a new FPSCounter with the given font size. An OpenGL
+      context must be current at the time the constructor is called.
+
+      @param drawable the drawable to render the text to
+      @param textSize the point size of the font to use
+      @throws GLException if an OpenGL context is not current when the constructor is called
+  */
+  public FPSCounter(GLDrawable drawable, int textSize) throws GLException {
+    this(drawable, new Font("SansSerif", Font.BOLD, textSize));
+  }
+
+  /** Creates a new FPSCounter with the given font. An OpenGL context
+      must be current at the time the constructor is called.
+
+      @param drawable the drawable to render the text to
+      @param font the font to use
+      @throws GLException if an OpenGL context is not current when the constructor is called
+  */
+  public FPSCounter(GLDrawable drawable, Font font) throws GLException {
+    this(drawable, font, true, true);
+  }
+
+  /** Creates a new FPSCounter with the given font and rendering
+      attributes. An OpenGL context must be current at the time the
+      constructor is called.
+
+      @param drawable the drawable to render the text to
+      @param font the font to use
+      @param antialiased whether to use antialiased fonts
+      @param useFractionalMetrics whether to use fractional font
+      @throws GLException if an OpenGL context is not current when the constructor is called
+  */
+  public FPSCounter(GLDrawable drawable,
+                    Font font,
+                    boolean antialiased,
+                    boolean useFractionalMetrics) throws GLException {
+    this.drawable = drawable;
+    renderer = new TextRenderer(font, antialiased, useFractionalMetrics);
+  }
+
+  /** Gets the relative location where the text of this FPSCounter
+      will be drawn: one of UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, or
+      LOWER_RIGHT. Defaults to LOWER_RIGHT. */
+  public int getTextLocation() {
+    return textLocation;
+  }
+
+  /** Sets the relative location where the text of this FPSCounter
+      will be drawn: one of UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, or
+      LOWER_RIGHT. Defaults to LOWER_RIGHT. */
+  public void setTextLocation(int textLocation) {
+    if (textLocation < UPPER_LEFT || textLocation > LOWER_RIGHT) {
+      throw new IllegalArgumentException("textLocation");
+    }
+    this.textLocation = textLocation;
+  }
+
+  /** Changes the current color of this TextRenderer to the supplied
+      one, where each component ranges from 0.0f - 1.0f. The alpha
+      component, if used, does not need to be premultiplied into the
+      color channels as described in the documentation for {@link
+      Texture Texture}, although premultiplied colors are used
+      internally. The default color is opaque white.
+
+      @param r the red component of the new color
+      @param g the green component of the new color
+      @param b the blue component of the new color
+      @param alpha the alpha component of the new color, 0.0f =
+        completely transparent, 1.0f = completely opaque
+      @throws GLException If an OpenGL context is not current when this method is called
+  */
+  public void setColor(float r, float g, float b, float a) throws GLException {
+    renderer.setColor(r, g, b, a);
+  }
+
+  /** Updates the FPSCounter's internal timer and counter and draws
+      the computed FPS. It is assumed this method will be called only
+      once per frame.
+  */
+  public void draw() {
+    if (startTime == 0) {
+      startTime = System.currentTimeMillis();
+    }
+
+    if (++frameCount >= 100) {
+      long endTime = System.currentTimeMillis();
+      float fps = 100.0f / (float) (endTime - startTime) * 1000;
+      recomputeFPSSize(fps);
+      frameCount = 0;
+      startTime = System.currentTimeMillis();
+
+      fpsText = "FPS: " + format.format(fps);
+    }
+
+    if (fpsText != null) {
+      renderer.beginRendering(drawable.getWidth(), drawable.getHeight());
+      // Figure out the location at which to draw the text
+      int x = 0;
+      int y = 0;
+      switch (textLocation) {
+        case UPPER_LEFT:
+          x = fpsOffset;
+          y = drawable.getHeight() - fpsHeight - fpsOffset;
+          break;
+
+        case UPPER_RIGHT:
+          x = drawable.getWidth() - fpsWidth - fpsOffset;
+          y = drawable.getHeight() - fpsHeight - fpsOffset;
+          break;
+
+        case LOWER_LEFT:
+          x = fpsOffset;
+          y = fpsOffset;
+          break;
+
+        case LOWER_RIGHT:
+          x = drawable.getWidth() - fpsWidth - fpsOffset;
+          y = fpsOffset;
+          break;
+      }
+
+      renderer.draw(fpsText, x, y);
+      renderer.endRendering();
+    }
+  }
+
+  private void recomputeFPSSize(float fps) {
+    String fpsText;
+    int fpsMagnitude;
+    if (fps >= 10000) {
+      fpsText = "10000.00";
+      fpsMagnitude = 5;
+    } else if (fps >= 1000) {
+      fpsText = "1000.00";
+      fpsMagnitude = 4;
+    } else if (fps >= 100) {
+      fpsText = "100.00";
+      fpsMagnitude = 3;
+    } else if (fps >= 10) {
+      fpsText = "10.00";
+      fpsMagnitude = 2;
+    } else {
+      fpsText = "9.00";
+      fpsMagnitude = 1;
+    }
+
+    if (fpsMagnitude > this.fpsMagnitude) {
+      Rectangle2D bounds = renderer.getBounds("FPS: " + fpsText);
+      fpsWidth = (int) bounds.getWidth();
+      fpsHeight = (int) bounds.getHeight();
+      fpsOffset = (int) (fpsHeight * 0.5f);
+      this.fpsMagnitude = fpsMagnitude;
+    }
+  }
+}
diff --git a/src/demos/util/FileUtils.java b/src/demos/util/FileUtils.java
new file mode 100755
index 0000000..442485d
--- /dev/null
+++ b/src/demos/util/FileUtils.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import java.io.*;
+
+public class FileUtils {
+  public static String loadStreamIntoString(InputStream stream) throws IOException {
+    if (stream == null) {
+      throw new java.io.IOException("null stream");
+    }
+    stream = new java.io.BufferedInputStream(stream);
+    int avail = stream.available();
+    byte[] data = new byte[avail];
+    int numRead = 0;
+    int pos = 0;
+    do {
+      if (pos + avail > data.length) {
+        byte[] newData = new byte[pos + avail];
+        System.arraycopy(data, 0, newData, 0, pos);
+        data = newData;
+      }
+      numRead = stream.read(data, pos, avail);
+      if (numRead >= 0) {
+        pos += numRead;
+      }
+      avail = stream.available();
+    } while (avail > 0 && numRead >= 0);
+    return new String(data, 0, pos, "US-ASCII");
+  }
+}
diff --git a/src/demos/util/FloatList.java b/src/demos/util/FloatList.java
new file mode 100644
index 0000000..fe06e2e
--- /dev/null
+++ b/src/demos/util/FloatList.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+/** Growable array of floats. */
+
+public class FloatList {
+  private static final int DEFAULT_SIZE = 10;
+
+  private float[] data = new float[DEFAULT_SIZE];
+  private int numElements;
+
+  public void add(float f) {
+    if (numElements == data.length) {
+      resize(1 + numElements);
+    }
+    data[numElements++] = f;
+    assert numElements <= data.length;
+  }
+
+  public int size() {
+    return numElements;
+  }
+
+  public float get(int index) {
+    if (index >= numElements) {
+      throw new ArrayIndexOutOfBoundsException(index);
+    }
+    return data[index];
+  }
+
+  public void put(int index, float val) {
+    if (index >= numElements) {
+      throw new ArrayIndexOutOfBoundsException(index);
+    }
+    data[index] = val;
+  }
+
+  public void trim() {
+    if (data.length > numElements) {
+      float[] newData = new float[numElements];
+      System.arraycopy(data, 0, newData, 0, numElements);
+      data = newData;
+    }
+  }
+
+  public float[] getData() {
+    return data;
+  }
+
+  private void resize(int minCapacity) {
+    int newCapacity = 2 * data.length;
+    if (newCapacity == 0) {
+      newCapacity = DEFAULT_SIZE;
+    }
+    if (newCapacity < minCapacity) {
+      newCapacity = minCapacity;
+    }
+    float[] newData = new float[newCapacity];
+    System.arraycopy(data, 0, newData, 0, data.length);
+    data = newData;
+  }
+}
diff --git a/src/demos/util/IntList.java b/src/demos/util/IntList.java
new file mode 100644
index 0000000..54a4745
--- /dev/null
+++ b/src/demos/util/IntList.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+/** Growable array of ints. */
+
+public class IntList {
+  private static final int DEFAULT_SIZE = 10;
+
+  private int[] data = new int[DEFAULT_SIZE];
+  private int numElements;
+
+  public void add(int f) {
+    if (numElements == data.length) {
+      resize(1 + numElements);
+    }
+    data[numElements++] = f;
+    assert numElements <= data.length;
+  }
+
+  public int size() {
+    return numElements;
+  }
+
+  public int get(int index) {
+    if (index >= numElements) {
+      throw new ArrayIndexOutOfBoundsException(index);
+    }
+    return data[index];
+  }
+
+  public void put(int index, int val) {
+    if (index >= numElements) {
+      throw new ArrayIndexOutOfBoundsException(index);
+    }
+    data[index] = val;
+  }
+
+  public void trim() {
+    if (data.length > numElements) {
+      int[] newData = new int[numElements];
+      System.arraycopy(data, 0, newData, 0, numElements);
+      data = newData;
+    }
+  }
+
+  public int[] getData() {
+    return data;
+  }
+
+  private void resize(int minCapacity) {
+    int newCapacity = 2 * data.length;
+    if (newCapacity == 0) {
+      newCapacity = DEFAULT_SIZE;
+    }
+    if (newCapacity < minCapacity) {
+      newCapacity = minCapacity;
+    }
+    int[] newData = new int[newCapacity];
+    System.arraycopy(data, 0, newData, 0, data.length);
+    data = newData;
+  }
+}
diff --git a/src/demos/util/MD2.java b/src/demos/util/MD2.java
new file mode 100644
index 0000000..8e43c79
--- /dev/null
+++ b/src/demos/util/MD2.java
@@ -0,0 +1,702 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+
+/** Reader for MD2 models, used by Quake II. */
+
+public class MD2 {
+  public static Model loadMD2(String filename) throws IOException {
+    List/*<IFrame>*/ ifr = new ArrayList/*<IFrame>*/();
+    loadFrames(filename, ifr);
+    return computeModel(ifr);
+  }
+
+  public static Model loadMD2(InputStream in) throws IOException {
+    List/*<IFrame>*/ ifr = new ArrayList/*<IFrame>*/();
+    loadFrames(in, ifr);
+    return computeModel(ifr);
+  }
+
+  public static class FileHeader {
+    public int ident;
+    public int version;
+    public int skinwidth;
+    public int skinheight;
+    public int framesize;     // byte size of each frame
+    public int num_skins;
+    public int num_xyz;
+    public int num_st;        // greater than num_xyz for seams
+    public int num_tris;
+    public int num_glcmds;    // dwords in strip/fan command list
+    public int num_frames;
+    public int ofs_skins;     // each skin is a MAX_SKINNAME string
+    public int ofs_st;        // byte offset from start for stverts
+    public int ofs_tris;      // offset for dtriangles
+    public int ofs_frames;    // offset for first frame
+    public int ofs_glcmds;
+    public int ofs_end;       // end of file
+  };
+
+  public static class FileCompressedVertex {
+    public byte[] v = new byte[3]; // scaled byte to fit in frame mins/maxs
+    public byte lightnormalindex;
+  }
+  
+  public static class FileFrame {
+    public float[] scale     = new float[3];           // multiply byte verts by this
+    public float[] translate = new float[3];           // then add this
+    public String name;                                // frame name from grabbing
+    public FileCompressedVertex[] verts;               // variable sized
+  }
+  
+  public static class FileModel {
+    public int[] glcmds;
+    public FileFrame[] frames;
+  }
+
+  public static class PositionNormal implements Cloneable {
+    public float x, y, z;
+    public float nx, ny, nz;
+    public Object clone() {
+      try {
+        return super.clone();
+      } catch (CloneNotSupportedException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  public static class TexCoord {
+    public float s,t;
+  }
+
+  public static class Vertex {
+    public int pn_index;
+    public TexCoord tc = new TexCoord();
+  }
+
+  public static class Triangle {
+    public Vertex[] v = new Vertex[3];
+    public boolean kill;
+  }
+
+  public static class WingedEdge {
+    public int[] e = new int[2];  // vertex index
+    public int[] w = new int[2];  // triangle index: for "open" models, w[1] == -1 on open edges
+  }
+
+  public static class Plane implements Cloneable {
+    public float a,b,c,d;
+    public Object clone() {
+      try {
+        return super.clone();
+      } catch (CloneNotSupportedException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  public static class Frame implements Cloneable {
+    public PositionNormal[] pn;  // [pn_index]
+    public Plane[] triplane;     // [tri_num]
+
+    public Object clone() {
+      Frame res = new Frame();
+      res.pn = new PositionNormal[pn.length];
+      for (int i = 0; i < pn.length; i++) {
+        res.pn[i] = (PositionNormal) pn[i].clone();
+      }
+      res.triplane = new Plane[triplane.length];
+      for (int i = 0; i < triplane.length; i++) {
+        res.triplane[i] = (Plane) triplane[i].clone();
+      }
+      return res;
+    }
+  }
+
+  public static class Model {
+    public Frame[] f;
+    public Triangle[] tri;                   // [tri_num]
+    public WingedEdge[] edge;                // [edge_num]
+  }
+
+  public static void computePlane(PositionNormal a, PositionNormal b, PositionNormal c, Plane p) {
+    float[] v0 = new float[3];
+    v0[0] = b.x - a.x;
+    v0[1] = b.y - a.y;
+    v0[2] = b.z - a.z;
+    float[] v1 = new float[3];
+    v1[0] = c.x - a.x;
+    v1[1] = c.y - a.y;
+    v1[2] = c.z - a.z;
+    float[] cr = new float[3];
+    cr[0] = v0[1] * v1[2] - v0[2] * v1[1];
+    cr[1] = v0[2] * v1[0] - v0[0] * v1[2];
+    cr[2] = v0[0] * v1[1] - v0[1] * v1[0];
+    float l = (float) Math.sqrt(cr[0] * cr[0] + cr[1] * cr[1] + cr[2] * cr[2]);
+    if (l == 0) {
+      // degenerate triangle
+      p.a = p.b = p.c = p.d = 0;
+      return;
+    }
+    p.a = cr[0] / l;
+    p.b = cr[1] / l;
+    p.c = cr[2] / l;
+
+    p.d = -(p.a * a.x + p.b * a.y + p.c * a.z);  // signed distance of a point on the plane from the origin
+  }
+
+  //----------------------------------------------------------------------
+  // Internals only below this point
+  //
+
+  private static Model computeModel(List/*<IFrame>*/ ifr) throws IOException {
+    if (!compareFrames(ifr)) {
+      throw new IOException("unsuitable model -- frames aren't same");
+    }
+    Model m = new Model();
+    m.tri = ((IFrame) ifr.get(0)).tri;
+    m.f = new Frame[ifr.size()];
+    for (int i = 0; i < ifr.size(); i++) {
+      Frame f = new Frame();
+      m.f[i] = f;
+      IFrame it = (IFrame) ifr.get(i);
+      f.pn = it.pn;
+      computeFramePlanes(m.tri, f);
+    }
+    computeWingedEdges(m);
+    return m;
+  }
+
+  private static class IFrame {
+    PositionNormal[] pn;
+    Triangle[] tri;
+  }
+
+  // normal table lifted from Mark Kilgard's md2bump demo
+  private static final float[] normalTable = new float[] {
+    -0.525731f, 0.000000f, 0.850651f, 
+    -0.442863f, 0.238856f, 0.864188f, 
+    -0.295242f, 0.000000f, 0.955423f, 
+    -0.309017f, 0.500000f, 0.809017f, 
+    -0.162460f, 0.262866f, 0.951056f, 
+    0.000000f, 0.000000f, 1.000000f, 
+    0.000000f, 0.850651f, 0.525731f, 
+    -0.147621f, 0.716567f, 0.681718f, 
+    0.147621f, 0.716567f, 0.681718f, 
+    0.000000f, 0.525731f, 0.850651f, 
+    0.309017f, 0.500000f, 0.809017f, 
+    0.525731f, 0.000000f, 0.850651f, 
+    0.295242f, 0.000000f, 0.955423f, 
+    0.442863f, 0.238856f, 0.864188f, 
+    0.162460f, 0.262866f, 0.951056f, 
+    -0.681718f, 0.147621f, 0.716567f, 
+    -0.809017f, 0.309017f, 0.500000f, 
+    -0.587785f, 0.425325f, 0.688191f, 
+    -0.850651f, 0.525731f, 0.000000f, 
+    -0.864188f, 0.442863f, 0.238856f, 
+    -0.716567f, 0.681718f, 0.147621f, 
+    -0.688191f, 0.587785f, 0.425325f, 
+    -0.500000f, 0.809017f, 0.309017f, 
+    -0.238856f, 0.864188f, 0.442863f, 
+    -0.425325f, 0.688191f, 0.587785f, 
+    -0.716567f, 0.681718f, -0.147621f, 
+    -0.500000f, 0.809017f, -0.309017f, 
+    -0.525731f, 0.850651f, 0.000000f, 
+    0.000000f, 0.850651f, -0.525731f, 
+    -0.238856f, 0.864188f, -0.442863f, 
+    0.000000f, 0.955423f, -0.295242f, 
+    -0.262866f, 0.951056f, -0.162460f, 
+    0.000000f, 1.000000f, 0.000000f, 
+    0.000000f, 0.955423f, 0.295242f, 
+    -0.262866f, 0.951056f, 0.162460f, 
+    0.238856f, 0.864188f, 0.442863f, 
+    0.262866f, 0.951056f, 0.162460f, 
+    0.500000f, 0.809017f, 0.309017f, 
+    0.238856f, 0.864188f, -0.442863f, 
+    0.262866f, 0.951056f, -0.162460f, 
+    0.500000f, 0.809017f, -0.309017f, 
+    0.850651f, 0.525731f, 0.000000f, 
+    0.716567f, 0.681718f, 0.147621f, 
+    0.716567f, 0.681718f, -0.147621f, 
+    0.525731f, 0.850651f, 0.000000f, 
+    0.425325f, 0.688191f, 0.587785f, 
+    0.864188f, 0.442863f, 0.238856f, 
+    0.688191f, 0.587785f, 0.425325f, 
+    0.809017f, 0.309017f, 0.500000f, 
+    0.681718f, 0.147621f, 0.716567f, 
+    0.587785f, 0.425325f, 0.688191f, 
+    0.955423f, 0.295242f, 0.000000f, 
+    1.000000f, 0.000000f, 0.000000f, 
+    0.951056f, 0.162460f, 0.262866f, 
+    0.850651f, -0.525731f, 0.000000f, 
+    0.955423f, -0.295242f, 0.000000f, 
+    0.864188f, -0.442863f, 0.238856f, 
+    0.951056f, -0.162460f, 0.262866f, 
+    0.809017f, -0.309017f, 0.500000f, 
+    0.681718f, -0.147621f, 0.716567f, 
+    0.850651f, 0.000000f, 0.525731f, 
+    0.864188f, 0.442863f, -0.238856f, 
+    0.809017f, 0.309017f, -0.500000f, 
+    0.951056f, 0.162460f, -0.262866f, 
+    0.525731f, 0.000000f, -0.850651f, 
+    0.681718f, 0.147621f, -0.716567f, 
+    0.681718f, -0.147621f, -0.716567f, 
+    0.850651f, 0.000000f, -0.525731f, 
+    0.809017f, -0.309017f, -0.500000f, 
+    0.864188f, -0.442863f, -0.238856f, 
+    0.951056f, -0.162460f, -0.262866f, 
+    0.147621f, 0.716567f, -0.681718f, 
+    0.309017f, 0.500000f, -0.809017f, 
+    0.425325f, 0.688191f, -0.587785f, 
+    0.442863f, 0.238856f, -0.864188f, 
+    0.587785f, 0.425325f, -0.688191f, 
+    0.688191f, 0.587785f, -0.425325f, 
+    -0.147621f, 0.716567f, -0.681718f, 
+    -0.309017f, 0.500000f, -0.809017f, 
+    0.000000f, 0.525731f, -0.850651f, 
+    -0.525731f, 0.000000f, -0.850651f, 
+    -0.442863f, 0.238856f, -0.864188f, 
+    -0.295242f, 0.000000f, -0.955423f, 
+    -0.162460f, 0.262866f, -0.951056f, 
+    0.000000f, 0.000000f, -1.000000f, 
+    0.295242f, 0.000000f, -0.955423f, 
+    0.162460f, 0.262866f, -0.951056f, 
+    -0.442863f, -0.238856f, -0.864188f, 
+    -0.309017f, -0.500000f, -0.809017f, 
+    -0.162460f, -0.262866f, -0.951056f, 
+    0.000000f, -0.850651f, -0.525731f, 
+    -0.147621f, -0.716567f, -0.681718f, 
+    0.147621f, -0.716567f, -0.681718f, 
+    0.000000f, -0.525731f, -0.850651f, 
+    0.309017f, -0.500000f, -0.809017f, 
+    0.442863f, -0.238856f, -0.864188f, 
+    0.162460f, -0.262866f, -0.951056f, 
+    0.238856f, -0.864188f, -0.442863f, 
+    0.500000f, -0.809017f, -0.309017f, 
+    0.425325f, -0.688191f, -0.587785f, 
+    0.716567f, -0.681718f, -0.147621f, 
+    0.688191f, -0.587785f, -0.425325f, 
+    0.587785f, -0.425325f, -0.688191f, 
+    0.000000f, -0.955423f, -0.295242f, 
+    0.000000f, -1.000000f, 0.000000f, 
+    0.262866f, -0.951056f, -0.162460f, 
+    0.000000f, -0.850651f, 0.525731f, 
+    0.000000f, -0.955423f, 0.295242f, 
+    0.238856f, -0.864188f, 0.442863f, 
+    0.262866f, -0.951056f, 0.162460f, 
+    0.500000f, -0.809017f, 0.309017f, 
+    0.716567f, -0.681718f, 0.147621f, 
+    0.525731f, -0.850651f, 0.000000f, 
+    -0.238856f, -0.864188f, -0.442863f, 
+    -0.500000f, -0.809017f, -0.309017f, 
+    -0.262866f, -0.951056f, -0.162460f, 
+    -0.850651f, -0.525731f, 0.000000f, 
+    -0.716567f, -0.681718f, -0.147621f, 
+    -0.716567f, -0.681718f, 0.147621f, 
+    -0.525731f, -0.850651f, 0.000000f, 
+    -0.500000f, -0.809017f, 0.309017f, 
+    -0.238856f, -0.864188f, 0.442863f, 
+    -0.262866f, -0.951056f, 0.162460f, 
+    -0.864188f, -0.442863f, 0.238856f, 
+    -0.809017f, -0.309017f, 0.500000f, 
+    -0.688191f, -0.587785f, 0.425325f, 
+    -0.681718f, -0.147621f, 0.716567f, 
+    -0.442863f, -0.238856f, 0.864188f, 
+    -0.587785f, -0.425325f, 0.688191f, 
+    -0.309017f, -0.500000f, 0.809017f, 
+    -0.147621f, -0.716567f, 0.681718f, 
+    -0.425325f, -0.688191f, 0.587785f, 
+    -0.162460f, -0.262866f, 0.951056f, 
+    0.442863f, -0.238856f, 0.864188f, 
+    0.162460f, -0.262866f, 0.951056f, 
+    0.309017f, -0.500000f, 0.809017f, 
+    0.147621f, -0.716567f, 0.681718f, 
+    0.000000f, -0.525731f, 0.850651f, 
+    0.425325f, -0.688191f, 0.587785f, 
+    0.587785f, -0.425325f, 0.688191f, 
+    0.688191f, -0.587785f, 0.425325f, 
+    -0.955423f, 0.295242f, 0.000000f, 
+    -0.951056f, 0.162460f, 0.262866f, 
+    -1.000000f, 0.000000f, 0.000000f, 
+    -0.850651f, 0.000000f, 0.525731f, 
+    -0.955423f, -0.295242f, 0.000000f, 
+    -0.951056f, -0.162460f, 0.262866f, 
+    -0.864188f, 0.442863f, -0.238856f, 
+    -0.951056f, 0.162460f, -0.262866f, 
+    -0.809017f, 0.309017f, -0.500000f, 
+    -0.864188f, -0.442863f, -0.238856f, 
+    -0.951056f, -0.162460f, -0.262866f, 
+    -0.809017f, -0.309017f, -0.500000f, 
+    -0.681718f, 0.147621f, -0.716567f, 
+    -0.681718f, -0.147621f, -0.716567f, 
+    -0.850651f, 0.000000f, -0.525731f, 
+    -0.688191f, 0.587785f, -0.425325f, 
+    -0.587785f, 0.425325f, -0.688191f, 
+    -0.425325f, 0.688191f, -0.587785f, 
+    -0.425325f, -0.688191f, -0.587785f, 
+    -0.587785f, -0.425325f, -0.688191f, 
+    -0.688191f, -0.587785f, -0.425325f
+  };
+  
+  private static void loadFrames(String filename, List/*<IFrame>*/ md2p) throws IOException {
+    FileModel mf = loadMD2File(filename);
+    computeFrames(mf, md2p);
+  }
+
+  private static void loadFrames(InputStream in, List/*<IFrame>*/ md2p) throws IOException {
+    FileModel mf = loadMD2File(in);
+    computeFrames(mf, md2p);
+  }
+
+  private static void computeFrames(FileModel mf, List/*<IFrame>*/ md2p) throws IOException {
+    for (int i = 0; i < mf.frames.length; i++) {
+      IFrame f = new IFrame();
+      md2p.add(f);
+      FileFrame curframe = mf.frames[i];
+      f.pn = new PositionNormal[curframe.verts.length];
+      for (int j = 0; j < curframe.verts.length; j++) {
+        PositionNormal pn = new PositionNormal();
+        pn.x = (((curframe.verts[j].v[0] & 0xFF) * curframe.scale[0]) + curframe.translate[0]) * .025f;
+        pn.y = (((curframe.verts[j].v[1] & 0xFF) * curframe.scale[1]) + curframe.translate[1]) * .025f;
+        pn.z = (((curframe.verts[j].v[2] & 0xFF) * curframe.scale[2]) + curframe.translate[2]) * .025f;
+        int normal_index = curframe.verts[j].lightnormalindex & 0xFF;
+        pn.nx = normalTable[3 * normal_index + 0];
+        pn.ny = normalTable[3 * normal_index + 1];
+        pn.nz = normalTable[3 * normal_index + 2];
+        f.pn[j] = pn;
+      }
+
+      List/*<Triangle>*/ tris = new ArrayList();
+      int[] idx = new int[1];
+      while (mf.glcmds[idx[0]] != 0) {
+        int vertnum;
+        boolean is_strip;
+        if (mf.glcmds[idx[0]] > 0) {
+          vertnum =  mf.glcmds[idx[0]++]; is_strip = true;  // triangle strip
+        } else {
+          vertnum = -mf.glcmds[idx[0]++]; is_strip = false; // triangle fan
+        }
+
+        if (is_strip) {
+          Vertex[] prev = new Vertex[2];
+          prev[0] = extractVertex(mf.glcmds, idx);
+          prev[1] = extractVertex(mf.glcmds, idx);
+          for (int j = 2; j < vertnum; j++) {
+            Triangle tri = new Triangle();
+            if ((j % 2) == 0) {
+              tri.v[0] = prev[0];
+              tri.v[1] = prev[1];
+              tri.v[2] = extractVertex(mf.glcmds, idx);
+              prev[0] = tri.v[2];
+            } else {
+              tri.v[0] = prev[1];
+              tri.v[1] = extractVertex(mf.glcmds, idx);
+              tri.v[2] = prev[0];
+              prev[1] = tri.v[1];
+            }
+            // swap v[1] and v[2] to fix triangle winding
+            Vertex hold = tri.v[1];
+            tri.v[1] = tri.v[2];
+            tri.v[2] = hold;
+            tris.add(tri);
+          }
+        } else {
+          // is fan
+          Vertex ctr = extractVertex(mf.glcmds, idx);
+          Vertex prev = extractVertex(mf.glcmds, idx);
+          for (int j = 2; j < vertnum; j++) {
+            Triangle tri = new Triangle();
+            tri.v[0] = ctr;
+            tri.v[1] = prev;
+            tri.v[2] = extractVertex(mf.glcmds, idx);
+            prev = tri.v[2];
+            // swap v[1] and v[2] to fix triangle winding
+            Vertex hold = tri.v[1];
+            tri.v[1] = tri.v[2];
+            tri.v[2] = hold;
+            tris.add(tri);
+          }
+        }
+      }
+      f.tri = (Triangle[]) tris.toArray(new Triangle[0]);
+    }
+  }
+
+  private static FileModel loadMD2File(ByteBuffer buf) throws IOException {
+    buf.order(ByteOrder.LITTLE_ENDIAN);
+    FileModel md2p = new FileModel();
+    FileHeader header = readHeader(buf);
+    buf.position(header.ofs_frames);
+    readFrames(buf, header, md2p);
+    buf.position(header.ofs_glcmds);
+    readGLCommands(buf, header, md2p);
+    return md2p;
+  }
+
+  private static FileModel loadMD2File(InputStream in) throws IOException {
+    in = new BufferedInputStream(in);
+    int avail = in.available();
+    byte[] data = new byte[avail];
+    int numRead = 0;
+    int pos = 0;
+    do {
+      if (pos + avail > data.length) {
+        byte[] newData = new byte[pos + avail];
+        System.arraycopy(data, 0, newData, 0, pos);
+        data = newData;
+      }
+      numRead = in.read(data, pos, avail);
+      if (numRead >= 0) {
+        pos += numRead;
+      }
+      avail = in.available();
+    } while (avail > 0 && numRead >= 0);
+    ByteBuffer buf = ByteBuffer.allocateDirect(pos);
+    buf.put(data, 0, pos);
+    buf.rewind();
+    return loadMD2File(buf);
+  }
+
+  private static FileModel loadMD2File(String filename) throws IOException {
+    FileInputStream fis = new FileInputStream(filename);
+    FileChannel chan = fis.getChannel();
+    ByteBuffer buf = chan.map(FileChannel.MapMode.READ_ONLY, 0, fis.available());
+    FileModel md2p = loadMD2File(buf);
+    chan.close();
+    fis.close();
+    return md2p;
+  }
+
+  private static FileHeader readHeader(ByteBuffer buf) {
+    FileHeader header = new FileHeader();
+    header.ident      = buf.getInt();
+    header.version    = buf.getInt();
+    header.skinwidth  = buf.getInt();
+    header.skinheight = buf.getInt();
+    header.framesize  = buf.getInt();
+    header.num_skins  = buf.getInt();
+    header.num_xyz    = buf.getInt();
+    header.num_st     = buf.getInt();
+    header.num_tris   = buf.getInt();
+    header.num_glcmds = buf.getInt();
+    header.num_frames = buf.getInt();
+    header.ofs_skins  = buf.getInt();
+    header.ofs_st     = buf.getInt();
+    header.ofs_tris   = buf.getInt();
+    header.ofs_frames = buf.getInt();
+    header.ofs_glcmds = buf.getInt();
+    header.ofs_end    = buf.getInt();
+    return header;
+  }
+
+  private static int numVerts(int framesize) {
+    return (framesize >> 2) - 10;
+  }
+
+  private static void readFrames(ByteBuffer buf, FileHeader header, FileModel md2p) throws IOException {
+    int numframes = header.num_frames;
+    int framesize = header.framesize;
+    int numVerts = numVerts(framesize);
+    FileFrame[] frames = new FileFrame[numframes];
+    byte[] name = new byte[16];
+    for (int i = 0; i < numframes; i++) {
+      FileFrame frame = new FileFrame();
+      frame.scale[0] = buf.getFloat();
+      frame.scale[1] = buf.getFloat();
+      frame.scale[2] = buf.getFloat();
+      frame.translate[0] = buf.getFloat();
+      frame.translate[1] = buf.getFloat();
+      frame.translate[2] = buf.getFloat();
+      buf.get(name);
+      try {
+        frame.name = new String(name, "US-ASCII");
+      } catch (UnsupportedEncodingException e) {
+        throw new IOException(e.toString());
+      }
+      frame.verts = new FileCompressedVertex[numVerts];
+      for (int j = 0; j < numVerts; j++) {
+        FileCompressedVertex vert = new FileCompressedVertex();
+        buf.get(vert.v);
+        vert.lightnormalindex = buf.get();
+        frame.verts[j] = vert;
+      }
+      frames[i] = frame;
+    }
+    md2p.frames = frames;
+  }
+
+  private static void readGLCommands(ByteBuffer buf, FileHeader header, FileModel md2p) {
+    int num_glcmds = header.num_glcmds;
+    int[] glcmds = new int[num_glcmds];
+    for (int i = 0; i < num_glcmds; i++) {
+      glcmds[i] = buf.getInt();
+    }
+    md2p.glcmds = glcmds;
+  }
+
+  private static Vertex extractVertex(int[] glcmds, int[] idx) {
+    Vertex v = new Vertex();
+    v.tc.s = Float.intBitsToFloat(glcmds[idx[0]++]);
+    v.tc.t = Float.intBitsToFloat(glcmds[idx[0]++]);
+    v.pn_index = glcmds[idx[0]++];
+    return v;
+  }
+
+  private static boolean compareFrames(List/*<IFrame>*/ m) {
+    IFrame f0 = (IFrame) m.get(0);
+    boolean same_topology  = true;
+    boolean same_texcoords = true;
+		
+    for (int i = 1; i < m.size(); i++) {
+      IFrame f = (IFrame) m.get(i);
+      if (f.pn.length != f0.pn.length) {
+        System.err.println("pn size different for iframe " + i + " :  " + f0.pn.length + " != " + f.pn.length);
+        same_topology = false;
+      }
+      if (f.tri.length != f0.tri.length) {
+        System.err.println("tri size different for iframe " + i + " :  " + f0.tri.length + " != " + f.tri.length);
+        same_topology = false;
+      }
+      if (same_topology) {
+        for (int j = 0; j < f.tri.length; j++) {
+          Triangle t0 = f0.tri[j];
+          Triangle t  = f.tri[j];
+          for (int k = 0; k < 3; k++) {
+            if (t0.v[k].pn_index != t.v[k].pn_index) {
+              System.err.println("tri " + j + " triangle pn_index " + k + " different!");
+              same_topology = false;
+            }
+            if (t0.v[k].tc.s != t.v[k].tc.s || t0.v[k].tc.t != t.v[k].tc.t) {
+              System.err.println("tri " + j + " triangle tc " + k + " different!");
+              same_texcoords = false;
+            }
+          }
+        }
+      }
+    }
+
+    return same_topology && same_texcoords;
+  }
+
+  /**
+     Computes the plane equations for each polygon of a frame.
+  */
+  private static void computeFramePlanes(Triangle[] tri, Frame f) {
+    f.triplane = new Plane[tri.length];
+    for (int i = 0; i < tri.length; i++) {
+      Triangle t = tri[i];
+      int ia = t.v[0].pn_index;
+      int ib = t.v[1].pn_index;
+      int ic = t.v[2].pn_index;
+      Plane p = new Plane();
+      computePlane(f.pn[ia], f.pn[ib], f.pn[ic], p);
+      f.triplane[i] = p;
+    }
+  }
+
+  private static int computeWingedEdges(Model m) {
+    Triangle[] tri = m.tri;
+    List/*<WingedEdge>*/ edge = new ArrayList/*<WingedEdge>*/();
+
+    // for each triangle, try to add each edge to the winged_edge vector,
+    // but check first to see if it's already there
+    int tsize = tri.length;
+    for (int i = 0; i < tsize; i++) {
+      Triangle t = tri[i];
+      for (int j = 0; j < 3; j++) {
+        WingedEdge we = new WingedEdge();
+        we.e[0] = t.v[   j   ].pn_index;
+        we.e[1] = t.v[(j+1)%3].pn_index;
+        we.w[0] = i;
+        we.w[1] = -1;  // subsequent attempt to add this edge will replace w[1] 
+        addEdge(edge, we);
+      }
+    }
+    int open_edge = 0;
+    for (int i = 0; i < edge.size(); i++) {
+      if (((WingedEdge) edge.get(i)).w[1] == -1)
+        open_edge++;
+    }
+    //fprintf(stderr, "out of % edges, there were %d open edges\n", edge.size(), open_edge);
+    m.edge = (WingedEdge[]) edge.toArray(new WingedEdge[0]);
+    return open_edge;
+  }
+
+  /**
+     add_edge will look to see if the current edge is already in the list.
+     If it is not, it will add it. If it is, it will replace the w[1] in
+     the existing table with w[0] from the edge being added.
+  */
+  private static void addEdge(List/*<WingedEdge>*/ edge, WingedEdge we) {
+    int esize = edge.size();
+    for (int i=0; i < esize; i++) {
+      WingedEdge we0 = (WingedEdge) edge.get(i);
+      if (we0.e[0] == we.e[0] && we0.e[1] == we.e[1]) {
+        System.err.println("facingness different between polys on edge!");
+      }
+      if(we0.e[0] == we.e[1]  && we0.e[1] == we.e[0]) {
+        if(we0.w[1] != -1) {
+          System.err.println("triple edge! bad...");
+        }
+        we0.w[1] = we.w[0]; // pair the edge and return
+        return;
+      }
+    }
+    edge.add(we);  // otherwise, add the new edge
+  }
+
+  public static void main(String[] args) {
+    for (int i = 0; i < args.length; i++) {
+      try {
+        MD2.Model model = loadMD2(args[i]);
+        System.err.println("Successfully parsed " + args[i]);
+      } catch (IOException e) {
+        System.err.println("Error parsing " + args[i] + ":");
+        e.printStackTrace();
+      }
+    }
+  }
+}
diff --git a/src/demos/util/ObjReader.java b/src/demos/util/ObjReader.java
new file mode 100644
index 0000000..75a2d08
--- /dev/null
+++ b/src/demos/util/ObjReader.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+import com.sun.opengl.util.BufferUtil;
+
+
+
+/** Simple parser for Wavefront .OBJ files. Does not support all file
+    options -- currently requires vertices and normals (only) to be
+    present. */
+
+public class ObjReader {
+  private int verticesPerFace = -1;
+  private FloatBuffer vertices;
+  private FloatBuffer normals;
+  private float[] aabbMin = new float[3];
+  private float[] aabbMax = new float[3];
+  private float[] center = new float[3];
+  private float radius;
+  // If we wanted this to be really general we'd have an array of
+  // FloatLists for the various kinds of vertices as well
+  private FloatList tmpVertices;
+  private FloatList tmpVertexNormals;
+  private IntList   faceIndices;
+  private IntList[] tmpFaceIndices;
+
+  public ObjReader(String filename) throws IOException {
+    this(new File(filename));
+  }
+
+  public ObjReader(InputStream in) throws IOException {
+    this(new InputStreamReader(in));
+  }
+
+  public ObjReader(File file) throws IOException {
+    this (new FileReader(file));
+  }
+
+  public ObjReader(Reader r) throws IOException {
+    BufferedReader reader = new BufferedReader(r);
+    String line = null;
+    int lineNo = 0;
+    float[] floatTmp = new float[3];
+
+    while ((line = reader.readLine()) != null) {
+      ++lineNo;
+      if (line.length() > 0) {
+        char c = line.charAt(0);
+        // FIXME: support continuation of lines with trailing '\'
+        switch (c) {
+          case '#':
+            break;
+
+          case 'v':
+            if (Character.isWhitespace(line.charAt(1))) {
+              addVertex(parseFloats(line, 3, floatTmp, lineNo));
+            } else if (line.startsWith("vn")) {
+              addVertexNormal(parseFloats(line, 3, floatTmp, lineNo));
+            } else {
+              throw new IOException("Unsupported vertex command on line " + lineNo);
+            }
+            break;
+            
+          case 'f':
+            parseIndices(line, lineNo);
+
+          default:
+            // For now we ignore all other lines
+        }
+      }
+    }
+
+    // Now have all vertex information.
+    // Make it possible to use same indices for both vertices and normals
+    condenseIndices();
+
+    // Compute axis-aligned bounding box and radius
+    computeBoundingBox();
+  }
+
+  public void rescale(float amount) {
+    for (int i = 0; i < vertices.capacity(); i++) {
+      vertices.put(i, vertices.get(i) * amount);
+    }
+  }
+
+  public FloatBuffer getVertices() {
+    return vertices;
+  }
+
+  public FloatBuffer getVertexNormals() {
+    return normals;
+  }
+
+  public int[] getFaceIndices() {
+    return faceIndices.getData();
+  }
+  
+  public int getVerticesPerFace() {
+    return verticesPerFace;
+  }
+
+  public float[] getAABBMin() {
+    return aabbMin;
+  }
+
+  public float[] getAABBMax() {
+    return aabbMax;
+  }
+
+  public float[] getCenter() {
+    return center;
+  }
+
+  public float getRadius() {
+    return radius;
+  }
+
+  //----------------------------------------------------------------------
+  // Internals only below this point
+  //
+
+  private void addVertex(float[] tmp) {
+    if (tmpVertices == null) {
+      tmpVertices = new FloatList();
+    }
+    for (int i = 0; i < 3; i++) {
+      tmpVertices.add(tmp[i]);
+    }
+  }
+
+  private void addVertexNormal(float[] tmp) {
+    if (tmpVertexNormals == null) {
+      tmpVertexNormals = new FloatList();
+    }
+    for (int i = 0; i < 3; i++) {
+      tmpVertexNormals.add(tmp[i]);
+    }
+  }
+
+  private float[] parseFloats(String line, int num, float[] tmp, int lineNo) throws IOException {
+    StringTokenizer tok = new StringTokenizer(line);
+    tok.nextToken(); // skip command
+    int idx = 0;
+    while (tok.hasMoreTokens()) {
+      if (idx >= tmp.length) {
+        throw new IOException("Too many floating-point values on line " + lineNo);
+      }
+      tmp[idx++] = Float.parseFloat(tok.nextToken());
+    }
+    return tmp;
+  }
+
+  private void parseIndices(String line, int lineNo) throws IOException {
+    StringTokenizer tok = new StringTokenizer(line);
+    tok.nextToken(); // skip command
+    List tokens = new ArrayList();
+    while (tok.hasMoreTokens()) {
+      tokens.add(tok.nextToken());
+    }
+    // This is the number of vertices in this face.
+    // If we seem to have already found this, it had better match the
+    // previously read value (for now - don't want to add the
+    // complexity of supporting some faces with a certain number of
+    // vertices and some with a different number)
+    if (verticesPerFace < 0) {
+      verticesPerFace = tokens.size();
+    } else {
+      if (verticesPerFace != tokens.size()) {
+        throw new IOException("Face on line " + lineNo + " had " + tokens.size() +
+                              " vertices, but had already previously set the number of vertices per face to " +
+                              verticesPerFace);
+      }
+    }
+    // Now read the individual indices out of each token
+    for (Iterator iter = tokens.iterator(); iter.hasNext(); ) {
+      String indices = (String) iter.next();
+      if (tmpFaceIndices == null) {
+        StringTokenizer tmpTok = new StringTokenizer(indices, "/");
+        int numIndicesPerVertex = 0;
+        while (tmpTok.hasMoreTokens()) {
+          tmpTok.nextToken();
+          ++numIndicesPerVertex;
+        }
+        tmpFaceIndices = new IntList[numIndicesPerVertex];
+        for (int i = 0; i < numIndicesPerVertex; i++) {
+          tmpFaceIndices[i] = new IntList();
+        }
+      }
+
+      StringTokenizer tok2 = new StringTokenizer(indices, "/");
+      int which = 0;
+      while (tok2.hasMoreTokens()) {
+        if (which >= tmpFaceIndices.length) {
+          throw new IOException("Expected all vertices to have " + tmpFaceIndices.length +
+                                " indices based on earlier input, but saw vertex with more on line " + lineNo);
+        }
+        String token = tok2.nextToken();
+        int index = Integer.parseInt(token);
+        tmpFaceIndices[which].add(index);
+        ++which;
+      }
+    }
+  }
+
+  // Don't know the hashing rules for arrays off the top of my head
+  static class Indices {
+    int[] data;
+    Indices(int[] data) {
+      this.data = data;
+    }
+
+    public boolean equals(Object obj) {
+      if ((obj == null) || (!(obj instanceof Indices))) {
+        return false;
+      }
+
+      Indices other = (Indices) obj;
+
+      if (data.length != other.data.length) {
+        return false;
+      }
+
+      for (int i = 0; i < data.length; i++) {
+        if (data[i] != other.data[i]) {
+          return false;
+        }
+      }
+      
+      return true;
+    }
+
+    public int hashCode() {
+      int hash = 0;
+      for (int i = 0; i < data.length; i++) {
+        hash ^= data[i];
+      }
+      return hash;
+    }
+  }
+
+  private void condenseIndices() {
+    FloatList newVertices = new FloatList();
+    FloatList newVertexNormals = new FloatList();
+    IntList   newIndices = new IntList();
+    int nextIndex = 0;
+    HashMap condensingMap = new HashMap();
+    for (int i = 0; i < tmpFaceIndices[0].size(); i++) {
+      Indices indices = getIndices(i);
+      Integer newIndex = (Integer) condensingMap.get(indices);
+      if (newIndex == null) {
+        // Fabricate new vertex and normal index for this one
+        // FIXME: generalize this by putting vertices and vertex
+        // normals in FloatList[] as well
+        condensingMap.put(indices, new Integer(nextIndex));
+        int vtxIdx    = 3 * (indices.data[0] - 1);
+        int vtxNrmIdx = 3 * (indices.data[1] - 1);
+        newVertices.add(tmpVertices.get(vtxIdx + 0));
+        newVertices.add(tmpVertices.get(vtxIdx + 1));
+        newVertices.add(tmpVertices.get(vtxIdx + 2));
+        newVertexNormals.add(tmpVertexNormals.get(vtxNrmIdx + 0));
+        newVertexNormals.add(tmpVertexNormals.get(vtxNrmIdx + 1));
+        newVertexNormals.add(tmpVertexNormals.get(vtxNrmIdx + 2));
+        newIndices.add(nextIndex);
+        ++nextIndex;
+      } else {
+        newIndices.add(newIndex.intValue());
+      }
+    }
+    newVertices.trim();
+    newVertexNormals.trim();
+    newIndices.trim();
+    vertices = BufferUtil.newFloatBuffer(newVertices.size());
+    vertices.put(newVertices.getData());
+    vertices.rewind();
+    normals = BufferUtil.newFloatBuffer(newVertexNormals.size());
+    normals.put(newVertexNormals.getData());
+    normals.rewind();
+    faceIndices = newIndices;
+    tmpVertices = null;
+    tmpVertexNormals = null;
+  }
+
+  private void computeBoundingBox() {
+    for (int i = 0; i < vertices.capacity(); i += 3) {
+      if (i == 0) {
+        aabbMin[0] = vertices.get(i + 0);
+        aabbMin[1] = vertices.get(i + 1);
+        aabbMin[2] = vertices.get(i + 2);
+        aabbMax[0] = vertices.get(i + 0);
+        aabbMax[1] = vertices.get(i + 1);
+        aabbMax[2] = vertices.get(i + 2);
+      } else {
+        aabbMin[0] = Math.min(aabbMin[0], vertices.get(i + 0));
+        aabbMin[1] = Math.min(aabbMin[1], vertices.get(i + 1));
+        aabbMin[2] = Math.min(aabbMin[2], vertices.get(i + 2));
+        aabbMax[0] = Math.max(aabbMax[0], vertices.get(i + 0));
+        aabbMax[1] = Math.max(aabbMax[1], vertices.get(i + 1));
+        aabbMax[2] = Math.max(aabbMax[2], vertices.get(i + 2));
+      }
+    }
+    center[0] = 0.5f * (aabbMin[0] + aabbMax[0]);
+    center[1] = 0.5f * (aabbMin[1] + aabbMax[1]);
+    center[2] = 0.5f * (aabbMin[2] + aabbMax[2]);
+    radius = (float) Math.sqrt((aabbMax[0] - center[0]) * (aabbMax[0] - center[0]) +
+                               (aabbMax[1] - center[1]) * (aabbMax[1] - center[1]) +
+                               (aabbMax[2] - center[2]) * (aabbMax[2] - center[2]));
+  }
+
+  private Indices getIndices(int index) {
+    int[] indices = new int[tmpFaceIndices.length];
+    for (int i = 0; i < tmpFaceIndices.length; i++) {
+      indices[i] = tmpFaceIndices[i].get(index);
+    }
+    return new Indices(indices);
+  }
+}
diff --git a/src/demos/util/ScreenResSelector.java b/src/demos/util/ScreenResSelector.java
new file mode 100755
index 0000000..5b085de
--- /dev/null
+++ b/src/demos/util/ScreenResSelector.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import javax.swing.*;
+
+public class ScreenResSelector {
+  static interface DisplayModeFilter {
+    public boolean filter(DisplayMode mode);
+  }
+
+  public static java.util.List getAvailableDisplayModes() {
+    java.util.List modes = getDisplayModes();
+    final DisplayMode curMode = getCurrentDisplayMode();
+    // Filter everything which is higher frequency than the current
+    // display mode
+    modes = filterDisplayModes(modes, new DisplayModeFilter() {
+        public boolean filter(DisplayMode mode) {
+          return (mode.getRefreshRate() <= curMode.getRefreshRate());
+        }
+      });
+    // Filter everything that is not at least 24-bit
+    modes = filterDisplayModes(modes, new DisplayModeFilter() {
+        public boolean filter(DisplayMode mode) {
+          // Bit depth < 0 means "multi-depth" -- can't reason about it
+          return (mode.getBitDepth() < 0 || mode.getBitDepth() >= 24);
+        }
+      });
+    // Filter everything less than 640x480
+    modes = filterDisplayModes(modes, new DisplayModeFilter() {
+        public boolean filter(DisplayMode mode) {
+          return (mode.getWidth() >= 640 && mode.getHeight() >= 480);
+        }
+      });
+    if (modes.size() == 0) {
+      throw new RuntimeException("Couldn't find any valid display modes");
+    }
+    return modes;
+  }
+
+  /** Shows a modal dialog containing the available screen resolutions
+      and requests selection of one of them. Returns the selected one. */
+  public static DisplayMode showSelectionDialog() {
+    SelectionDialog dialog = new SelectionDialog();
+    dialog.setVisible(true);
+    dialog.waitFor();
+    return dialog.selected();
+  }
+
+  public static void main(String[] args) {
+    DisplayMode mode = showSelectionDialog();
+    if (mode != null) {
+      System.err.println("Selected display mode:");
+      System.err.println(modeToString(mode));
+    } else {
+      System.err.println("No display mode selected.");
+    }
+    System.exit(0);
+  }
+
+  //----------------------------------------------------------------------
+  // Internals only below this point
+  //
+
+  private static DisplayMode getCurrentDisplayMode() {
+    GraphicsDevice dev = getDefaultScreenDevice();
+    return dev.getDisplayMode();
+  }
+
+  private static java.util.List/*<DisplayMode>*/ getDisplayModes() {
+    GraphicsDevice dev = getDefaultScreenDevice();
+    DisplayMode[] modes = dev.getDisplayModes();
+    return toList(modes);
+  }
+
+  private static GraphicsDevice getDefaultScreenDevice() {
+    return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
+  }
+
+  private static java.util.List/*<DisplayMode>*/ toList(DisplayMode[] modes) {
+    java.util.List res = new ArrayList();
+    for (int i = 0; i < modes.length; i++) {
+      res.add(modes[i]);
+    }
+    return res;
+  }
+
+  private static String modeToString(DisplayMode mode) {
+    return (((mode.getBitDepth() > 0) ? (mode.getBitDepth() + " bits, ") : "") +
+            mode.getWidth() + "x" + mode.getHeight() + ", " +
+            mode.getRefreshRate() + " Hz");
+  }
+
+  private static String[] modesToString(java.util.List/*<DisplayMode>*/ modes) {
+    String[] res = new String[modes.size()];
+    int i = 0;
+    for (Iterator iter = modes.iterator(); iter.hasNext(); ) {
+      res[i++] = modeToString((DisplayMode) iter.next());
+    }
+    return res;
+  }
+
+  private static java.util.List/*<DisplayMode>*/ filterDisplayModes(java.util.List/*<DisplayMode>*/ modes,
+                                                                    DisplayModeFilter filter) {
+    java.util.List res = new ArrayList();
+    for (Iterator iter = modes.iterator(); iter.hasNext(); ) {
+      DisplayMode mode = (DisplayMode) iter.next();
+      if (filter.filter(mode)) {
+        res.add(mode);
+      }
+    }
+    return res;
+  }
+
+  static class SelectionDialog extends JFrame {
+    private Object monitor = new Object();
+    private java.util.List modes;
+    private volatile boolean done = false;
+    private volatile int selectedIndex;
+
+    public SelectionDialog() {
+      super();
+
+      setTitle("Display Modes");
+      modes = getAvailableDisplayModes();
+      String[] strings = modesToString(modes);
+      final JList modeList = new JList(strings);
+      modeList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+      modeList.setSelectedIndex(0);
+      JScrollPane scroller = new JScrollPane(modeList);
+      getContentPane().setLayout(new BorderLayout());
+      JPanel panel = new JPanel();
+      panel.setLayout(new BorderLayout());
+      panel.add(new JLabel("Select full-screen display mode,"), BorderLayout.NORTH);
+      panel.add(new JLabel("or Cancel for windowed mode:"),     BorderLayout.CENTER);
+      getContentPane().add(BorderLayout.NORTH, panel);
+      getContentPane().add(BorderLayout.CENTER, scroller);
+      JPanel buttonPanel = new JPanel();
+      buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
+      buttonPanel.add(Box.createGlue());
+      JButton button = new JButton("OK");
+      button.addActionListener(new ActionListener() {
+          public void actionPerformed(ActionEvent e) {
+            selectedIndex = modeList.getSelectedIndex();
+            done = true;
+            synchronized(monitor) {
+              monitor.notify();
+            }
+            setVisible(false);
+            dispose();
+          }
+        });
+      buttonPanel.add(button);
+      buttonPanel.add(Box.createHorizontalStrut(10));
+      button = new JButton("Cancel");
+      button.addActionListener(new ActionListener() {
+          public void actionPerformed(ActionEvent e) {
+            selectedIndex = -1;
+            done = true;
+            synchronized(monitor) {
+              monitor.notify();
+            }
+            setVisible(false);
+            dispose();
+          }
+        });
+      buttonPanel.add(button);
+      buttonPanel.add(Box.createGlue());
+      getContentPane().add(BorderLayout.SOUTH, buttonPanel);
+      setSize(300, 200);
+      center(this, Toolkit.getDefaultToolkit().getScreenSize());
+    }
+
+    public void waitFor() {
+      synchronized(monitor) {
+        while (!done) {
+          try {
+            monitor.wait();
+          } catch (InterruptedException e) {
+          }
+        }
+      }
+    }
+
+    public DisplayMode selected() {
+      if (selectedIndex < 0) {
+        return null;
+      } else {
+        return (DisplayMode) modes.get(selectedIndex);
+      }
+    }
+  }
+
+  private static void center(Component component,
+                             Dimension containerDimension) {
+    Dimension sz = component.getSize();
+    int x = ((containerDimension.width - sz.width) / 2);
+    int y = ((containerDimension.height - sz.height) / 2);
+    component.setLocation(x, y);
+  }
+}
diff --git a/src/demos/util/SystemTime.java b/src/demos/util/SystemTime.java
new file mode 100644
index 0000000..b6a154b
--- /dev/null
+++ b/src/demos/util/SystemTime.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+/** Implementation of {@link demos.util.Time} interface based
+    on {@link java.lang.System.currentTimeMillis}. Performs smoothing
+    internally to avoid effects of poor granularity of
+    currentTimeMillis on Windows platform in particular. */
+
+public class SystemTime implements Time {
+  private static final int DEFAULT_NUM_SMOOTHING_SAMPLES = 10;
+  private long[] samples = new long[DEFAULT_NUM_SMOOTHING_SAMPLES];
+  private int   numSmoothingSamples;
+  private int   curSmoothingSample; // Index of current sample to be replaced
+  private long baseTime = System.currentTimeMillis();
+  private boolean hasCurTime;
+  private double curTime;
+  private double deltaT;
+
+  /** Sets number of smoothing samples. Defaults to 10. Note that
+      there may be a discontinuity in the reported time after a call
+      to this method. */
+  public void setNumSmoothingSamples(int num) {
+    samples = new long[num];
+    numSmoothingSamples = 0;
+    curSmoothingSample = 0;
+    hasCurTime = false;
+  }
+
+  /** Returns number of smoothing samples; default is 10. */
+  public int getNumSmoothingSamples() {
+    return samples.length;
+  }
+
+  /** Rebases this timer. After very long periods of time the
+      resolution of this timer may decrease; the application can call
+      this to restore higher resolution. Note that there may be a
+      discontinuity in the reported time after a call to this
+      method. */
+  public void rebase() {
+    baseTime = System.currentTimeMillis();
+    setNumSmoothingSamples(samples.length);
+  }
+
+  public void update() {
+    long tmpTime = System.currentTimeMillis();
+    long diffSinceBase = tmpTime - baseTime;
+    samples[curSmoothingSample] = diffSinceBase;
+    curSmoothingSample = (curSmoothingSample + 1) % samples.length;
+    numSmoothingSamples = Math.min(1 + numSmoothingSamples, samples.length);
+    // Average of samples is current time
+    double newCurTime = 0.0;
+    for (int i = 0; i < numSmoothingSamples; i++) {
+      newCurTime += samples[i];
+    }
+    newCurTime /= (1000.0f * numSmoothingSamples);
+    double lastTime = curTime;
+    if (!hasCurTime) {
+      lastTime = newCurTime;
+      hasCurTime = true;
+    }
+    deltaT = newCurTime - lastTime;
+    curTime = newCurTime;
+  }
+
+  public double time() {
+    return curTime;
+  }
+
+  public double deltaT() {
+    return deltaT;
+  }
+}
diff --git a/src/demos/util/Time.java b/src/demos/util/Time.java
new file mode 100644
index 0000000..f3ede1f
--- /dev/null
+++ b/src/demos/util/Time.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+/** Interface abstracting concept of time from applications. */
+
+public interface Time {
+  /** Updates this Time object. Call update() each frame before
+      calling the accessor routines. */
+  public void  update();
+  /** Time in seconds since beginning of application. */
+  public double time();
+  /** Time in seconds since last update. */
+  public double deltaT();
+}
diff --git a/src/demos/util/Triceratops.java b/src/demos/util/Triceratops.java
new file mode 100644
index 0000000..ca0221d
--- /dev/null
+++ b/src/demos/util/Triceratops.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * 
+ * - Redistribution of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * 
+ * - Redistribution in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * 
+ * Neither the name of Sun Microsystems, Inc. or the names of
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ * 
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
+ * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
+ * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
+ * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
+ * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
+ * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
+ * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
+ * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ * 
+ * You acknowledge that this software is not designed or intended for use
+ * in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ * 
+ * Sun gratefully acknowledges that this software was originally authored
+ * and developed by Kenneth Bradley Russell and Christopher John Kline.
+ */
+
+package demos.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StreamTokenizer;
+import javax.media.opengl.GL;
+import javax.media.opengl.GL2ES1;
+import javax.media.opengl.GL2;
+
+
+
+/** Renders a triceratops. <P>
+
+	Copyright by Thomas Baier (thomas.baier@stmuc.com)<br>
+	Created by OpenGL Export Plugin 1.0 at Fri Oct 27 22:04:55 2000<br>
+	OpenGL-Structure <br><p>
+
+        Ported to Java by Kenneth Russell
+*/
+public class Triceratops {
+
+  /** Draws the triceratops object. Callers should capture the result
+      in a display list. */
+  public static void drawObject(GL2 gl) throws IOException {
+    Reader reader = new BufferedReader(new InputStreamReader(
+      Triceratops.class.getClassLoader().getResourceAsStream("demos/data/models/triceratops.txt")));
+    StreamTokenizer tok = new StreamTokenizer(reader);
+    // Reset tokenizer's syntax so numbers are not parsed
+    tok.resetSyntax();
+    tok.wordChars('a', 'z');
+    tok.wordChars('A', 'Z');
+    tok.wordChars('0', '9');
+    tok.wordChars('-', '-');
+    tok.wordChars('.', '.');
+    tok.wordChars(128 + 32, 255);
+    tok.whitespaceChars(0, ' ');
+    tok.whitespaceChars(',', ',');
+    tok.whitespaceChars('{', '{');
+    tok.whitespaceChars('}', '}');
+    tok.commentChar('/');
+    tok.quoteChar('"');
+    tok.quoteChar('\'');
+    tok.slashSlashComments(true);
+    tok.slashStarComments(true);
+
+    // Read in file
+    int numVertices = nextInt(tok, "number of vertices");
+    float[] vertices = new float[numVertices * 3];
+    for (int i = 0; i < numVertices * 3; i++) {
+      vertices[i] = nextFloat(tok, "vertex");
+    }
+    int numNormals = nextInt(tok, "number of normals");
+    float[] normals = new float[numNormals * 3];
+    for (int i = 0; i < numNormals * 3; i++) {
+      normals[i] = nextFloat(tok, "normal");
+    }
+    int numFaceIndices = nextInt(tok, "number of face indices");
+    short[] faceIndices = new short[numFaceIndices * 9];
+    for (int i = 0; i < numFaceIndices * 9; i++) {
+      faceIndices[i] = (short) nextInt(tok, "face index");
+    }
+
+    reader.close();
+
+    float sf = 0.1f;
+    gl.glBegin(GL2.GL_TRIANGLES);
+    for (int i = 0; i < faceIndices.length; i += 9) {
+      for (int j = 0; j < 3; j++) {
+        int vi = faceIndices[i + j    ] & 0xFFFF;
+        int ni = faceIndices[i + j + 3] & 0xFFFF;
+        gl.glNormal3f(normals[3 * ni],
+                      normals[3 * ni + 1],
+                      normals[3 * ni + 2]);
+        gl.glVertex3f(sf * vertices[3 * vi],
+                      sf * vertices[3 * vi + 1],
+                      sf * vertices[3 * vi + 2]);
+      }
+    }
+    gl.glEnd();
+  }
+
+  private static int nextInt(StreamTokenizer tok, String error) throws IOException {
+    if (tok.nextToken() != StreamTokenizer.TT_WORD) {
+      throw new IOException("Parse error reading " + error + " at line " + tok.lineno());
+    }
+    try {
+      return Integer.parseInt(tok.sval);
+    } catch (NumberFormatException e) {
+      throw new IOException("Parse error reading " + error + " at line " + tok.lineno());
+    }
+  }
+
+  private static float nextFloat(StreamTokenizer tok, String error) throws IOException {
+    if (tok.nextToken() != StreamTokenizer.TT_WORD) {
+      throw new IOException("Parse error reading " + error + " at line " + tok.lineno());
+    }
+    try {
+      return Float.parseFloat(tok.sval);
+    } catch (NumberFormatException e) {
+      throw new IOException("Parse error reading " + error + " at line " + tok.lineno());
+    }
+  }
+}
-- 
cgit v1.2.3