Triangle Scene

<WORK IN PROGRESS>
This program will render several triangles that are clustered inside a cube. The user's view will rotate around this cube with speed proportional to the framerate, giving an idea of how fast the scene is being rendered; the faster the cube is rotating, the faster everything is being drawn.


Triangle Class

Since we are drawing a bunch of triangles, we'll start by making a data structure that stores information about a single triangle. It might be a good idea store all the data for every triangle in a single array; it's not always wise to have an object for such a simple shape, because objects can take up a lot of memory. This is such a trivial program that it's better to have the simplicity of giving each triangle its own object, but it's important to keep memory in mind on larger programs.

package rendering;

public class Triangle {
   public final float[][] vertices;
   public final float[][] colors;

   public Triangle() {
      vertices = new float[3][3];
      vertices[0] = createRandomPoint(new float[] { 0, 0, 0 }, 15);
      vertices[1] = createRandomPoint(vertices[0], 2);
      vertices[2] = createRandomPoint(vertices[0], 2);

      colors = new float[3][3];
      for (int i = 0; i < 3; i++)
         for (int j = 0; j < 3; j++)
            colors[i][j] = (float) Math.random();
   }

   private static float[] createRandomPoint(float[] p, float dist) {
      return new float[] { 
         (float) ((Math.random() - 0.5) * dist + p[0]),
         (float) ((Math.random() - 0.5) * dist + p[1]),
         (float) ((Math.random() - 0.5) * dist + p[2]) };
   }
}

Each triangle consists of three vertices and some color for each vertex. A vertex is described by its position (x,y,z). Each color is described by three components (red, green ,blue). The vertex and color data is stored in two separate 2D arrays. The first vertex position, for example, is vertices[0]. The y-component of this position is vertices[0][1]. The colors array matches the vertices array, so colors[0] is the {r,g,b} components for the first vertex; colors[1] is for the second vertex, and so on.

The constructor for the triangle class randomly generates three vertex positions and some random colors for each vertex. To keep the triangle within a cube area, the createRandomPoint method is used to create an initial vertex position. This method simply creates three random numbers each within dist / 2 units from a provided point. The first vertex position is created with dist=15 around the point (0,0,0), so the x, y, and z components are in [-7.5,7.5]. The other two vertex positions are then kept within 1 unit of the first vertex position. This method ensures triangles are mostly the same size (no really huge triangles), but created at different parts of the cube.

TriangleRenderer Interface

Instead of hard-coding the rendering of all the triangles inside the program's render method, the drawing will be delegated to a "triangle renderer" object. We will create different implementations of a triangle renderer for the different rendering methods: immediate mode, vertex arrays, display lists, and vertex buffer objects (VBOs).

package rendering;

import javax.media.opengl.GL2;

public interface TriangleRenderer {
    public void init(GL2 gl, Triangle[] triangles);
    public void render(GL2 gl);
    public void dispose(GL2 gl);
}

Each renderer implementation is given an opportunity to initialize itself and store a reference to the triangle data (or move it into a different data structure) with the init method. The render method will draw the triangles. The dispose method allows an implementation to release any resources it may or may not have used on the graphics card.

TriangleScene Class

This is the main class that will create and store the triangles, initialize a window, and listen for rendering events.

package rendering;

import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import javax.media.opengl.*;
import javax.swing.Timer;
import support.GLInfo;
import windows.AWTWindowProgram;

public class TriangleScene extends AWTWindowProgram implements KeyListener {

    private List<TriangleRenderer> renderers;
    private Triangle[]             triangles;
    private double                 cameraTheta      = 0;
    private int                    framesRendered   = 0;
    private int                    selectedRenderer = 0;

    public TriangleScene(String title, int w, int h, GLCapabilities caps,
            int numTriangles) {
        super(title, w, h, caps);

        // create triangle blob
        triangles = new Triangle[numTriangles];
        for (int i = 0; i < numTriangles; i++)
            triangles[i] = new Triangle();
        
        getCanvas().addKeyListener(this);
    }

    public void update(GL gl) {}

    public void dispose(GLAutoDrawable drawable) {
        for (TriangleRenderer tr : renderers)
            tr.dispose(drawable.getGL().getGL2());
    }

    public void init(GLAutoDrawable drawable) {
        GL2 gl = drawable.getGL().getGL2();

        // get OpenGL support / extension information
        GLInfo info = new GLInfo(drawable.getGL());
        info.print();

        renderers = new ArrayList<TriangleRenderer>();
        renderers.add(new ImmediateRenderer());
        renderers.add(new VertexArrayRenderer());
        renderers.add(new DisplayListRenderer());
        if (info.extSupported("GL_ARB_vertex_buffer_object"))
            renderers.add(new VBORenderer());
        for (TriangleRenderer renderer : renderers)
            renderer.init(gl, triangles);

        gl.glEnable(GL.GL_DEPTH_TEST);
        gl.glEnable(GL.GL_CULL_FACE);
    }

    public void update(GL gl, double elapsedMS) {
        cameraTheta += 0.001;
    }

    public void render(GL glObj) {
        GL2 gl = glObj.getGL2();

        // perspective projection
        gl.glMatrixMode(GL2.GL_PROJECTION);
        gl.glLoadIdentity();
        glu.gluPerspective(45, getWindowDimensions().aspect, 0.1, 100);
        
        // rotate camera position around origin
        gl.glMatrixMode(GL2.GL_MODELVIEW);
        gl.glLoadIdentity();
        glu.gluLookAt(Math.sin(cameraTheta) * 30, 10,
                Math.cos(cameraTheta) * 30, 0, 0, 0, 0, 1, 0);

        gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);

        renderers.get(selectedRenderer).render(gl);

        framesRendered++;
    }

    /** Updates window title to display renderer name and last FPS */
    public void updateFPS() {
        String renderer = renderers.get(selectedRenderer).toString();
        getFrame().setTitle(renderer + " | FPS: " + framesRendered);
        framesRendered = 0;
    }

    public void keyTyped(KeyEvent e) {
        if (e.getKeyChar() == KeyEvent.VK_SPACE) {
            selectedRenderer = (selectedRenderer + 1) % renderers.size();
            updateFPS();
        }
    }

    public void keyPressed(KeyEvent e) {
    }

    public void keyReleased(KeyEvent e) {
    }

    public static void main(String[] args) {
        
        // if user enters a number in command-line args, use it for number of
        // triangles; otherwise, use 10,000 triangles
        int numTriangles = args.length > 0 ? Integer.parseInt(args[0]) : 10000;
        
        GLProfile glp = GLProfile.getDefault();
        GLCapabilities caps = new GLCapabilities(glp);
        final TriangleScene prog = new TriangleScene("Triangle Blob",
                600, 600, caps, numTriangles);
        
        // timer thread that gives a rough estimate of FPS
        Timer fpsTimer = new Timer(1000, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                prog.updateFPS();
            }
        });
        fpsTimer.setRepeats(true);
        fpsTimer.start();
    }
}

The class extends from an AWTWindowProgram to avoid some of the boilerplate code involved in getting an AWT frame and animator thread started. It also implements the AWT KeyListener interface, so when the user presses the space key it iterates through available triangle renderers.

There are five fields in the class:
 
private List<TriangleRenderer> renderers;
private Triangle[]             triangles;
private double                 cameraTheta      = 0;
private int                    framesRendered   = 0;
private int                    selectedRenderer = 0;

The first, renderers, is the list of available triangle renderers. The selectedRenderer variable refers to the index of the renderer currently being used. The camera's rotation angle (around y-axis) is stored in cameraTheta, which is updated constantly in the update method. To get an idea of framerate, framesRendered keeps track of the number of times the render method has been called and is displayed / reset every second by the fpsTimer thread started in the main method.

The init method first checks the client's OpenGL support information with the GLInfo object, a utility class you can download at the bottom of this page. This is used to determine if the client can support the VBO triangle renderer. All supported renderers are created and initialized. The render method updates the user's view with the new rotation angle, then asks the selected triangle renderer to render the triangles.

Č
ċ
ď
AWTWindowProgram.java
(4k)
Justin Stoecker,
Feb 22, 2011, 8:45 AM
ċ
ď
DisplayListRenderer.java
(1k)
Justin Stoecker,
Feb 22, 2011, 8:44 AM
ċ
ď
GLInfo.java
(2k)
Justin Stoecker,
Feb 22, 2011, 8:44 AM
ċ
ď
ImmediateRenderer.java
(1k)
Justin Stoecker,
Feb 22, 2011, 8:44 AM
ċ
ď
Triangle.java
(1k)
Justin Stoecker,
Feb 22, 2011, 8:43 AM
ċ
ď
TriangleRenderer.java
(0k)
Justin Stoecker,
Feb 22, 2011, 8:44 AM
ċ
ď
TriangleScene.java
(4k)
Justin Stoecker,
Mar 23, 2011, 9:47 PM
ċ
ď
VBORenderer.java
(2k)
Justin Stoecker,
Feb 22, 2011, 8:44 AM
ċ
ď
VertexArrayRenderer.java
(1k)
Justin Stoecker,
Feb 22, 2011, 8:44 AM
ċ
ď
Viewport.java
(1k)
Justin Stoecker,
Feb 22, 2011, 8:45 AM
ċ
ď
WindowProgram.java
(1k)
Justin Stoecker,
Feb 22, 2011, 8:45 AM
Comments