Using Timer

Documentation
Contents

This is how to write LightGoAround.java, a program that adds surfaces and lighting to the cubes described in the previous section. The light source is automatically rotated in the scene, using Java's java.util.Timer.
Move screen

Below is the step by step guide to this source code.

Modifications and additions

The following elements are modified and added to the program described in the previous section.

Other elements, starting method, camera, text display and key operation have the code identical to that in the previous section.

Cube Class: Draw an object's surfaces

Look at the Cube class.

class Cube extends Material {
    public Cube() {
        super(0, 0, 0);
    }
    
    private double width = 2d;
    
    private double[][] vertexList = {
        { -1, -1, -1},
        { +1, -1, -1},
        { +1, +1, -1},
        { -1, +1, -1},
        { -1, -1, +1},
        { +1, -1, +1},
        { +1, +1, +1},
        { -1, +1, +1},
    };
    
    private int[][] faceList = {
        {0, 3, 2, 1},
        {1, 2, 6, 5},
        {5, 6, 7, 4},
        {4, 7, 3, 0},
        {4, 0, 1, 5},
        {3, 7, 6, 2},
    };
    
    private double[][] normalList = {
        {0.0, 0.0, -1.0},
        {1.0, 0.0, 0.0},
        {0.0, 0.0, 1.0},
        {-1.0, 0.0, 0.0},
        {0.0, -1.0, 0.0},
        {0.0, 1.0, 0.0},
    };
    
    float[] color = { 1.0f, 0.1f, 0.1f, 1.0f };
    float[] diffuse = { 0.8f, 0.8f, 0.8f, 1.0f };
    float[] specular = { 0.0f, 0.0f, 0.0f, 1.0f };
    float[] ambient = { 0.2f, 0.2f, 0.2f, 1.0f };
    float[] emission = { 0.0f, 0.0f, 0.0f, 1.0f };
    float shininess = 0.0f;
    
    public void update(GLGraphics g) {
        g.glTranslated(getX(), getY(), getZ());
        g.glScaled(width, width, width);
        g.glMaterialfv(GLConstants.GL_FRONT, GLConstants.GL_DIFFUSE, color);
        g.glMaterialfv(GLConstants.GL_FRONT, GLConstants.GL_AMBIENT, color);
        g.glMaterialfv(GLConstants.GL_FRONT, GLConstants.GL_SPECULAR, specular);
        g.glMaterialfv(GLConstants.GL_FRONT, GLConstants.GL_EMISSION, emission);
        g.glMaterialf(GLConstants.GL_FRONT, GLConstants.GL_SHININESS, shininess);
        
        g.glBegin(GLConstants.GL_QUADS);
        for (int i = 0; i < faceList.length; i++) {
            int[] face = faceList[i];
            g.glNormal3dv(normalList[i]);
            for (int j = 0; j < face.length; j++) {
                int vertexIndex = face[j];
                g.glVertex3dv(vertexList[vertexIndex]);
            }
        }
        g.glEnd();
    }
    
    public String toString() {
        return "Cube at: (" + getX() + "," + getY() + "," + getZ() + ") width: " + width;
    }
}

Coordinate Transformation

In the previous section, the vertex coordinates were directly calculated and specified by glVertex3dv to draw a cube. Here, the vertices are not directly calculated. If you change the origin and scaling in advance, drawing a size-fixed cube at the origin enables you to get the same cube as in the previous section.

First, translate the origin to the specified position by glTranslated, and then scale up the size into each axis-direction by glScaled (this must be done in this order). And, by simply drawing a cube with vertexList, you can get the cube of the same size at the same position as in the previous section.

Reflection Setting

The next step is to define the cube's material property by glMaterial, which specifies how much of light is reflected, or in which color it is displayed. Unlike characters or lines, glColor is not used here.

The color of opaque red is set to GL_DIFFUSE (diffuse reflection) and GL_AMBIENT (ambient reflection), which will make the cube displayed in red with a white lighting.

The other glMaterial settings including GL_SPECULAR (specular reflection), GL_EMISSION (emission), and GL_SHINENESS (reflection intensity) represent OpenGL's default values and simply specify its default values (that is, they are not necessary). Fore details, see the OpenGL manual.

Draw Cubes

The next step is to draw flat surfaces instead of lines to form a cube. GL_QUADS creates a square surface. Normal vectors perpendicular to each surface should also be specified manually by glNormal3dv. There are two OpenGL face types, front and back. By default, polygons whose vertices appear in counterclockwise order on the screen are the front face whereas those in clockwise order are the back face. The normal vectors specify in advance the normal lines of the surface corresponding to faceList using normalList.

Light Class: Set up a light source

Even if the cube's material property has been specified, no color is given without lighting. So, the Light class that is a light source should be created. Also, the Light class should have the rotate method to rotate with the timer.

class Light extends Material {
    public Light() {
        super(10, 10, 0);
    }

    private int index;
    private double radius = 10;
    private double angle = 0;
    private float[] ambient = {0.0f, 0.0f, 0.0f, 1.0f};
    private float[] diffuse = {1.0f, 1.0f, 1.0f, 1.0f};
    private float[] specular = {1.0f, 1.0f, 1.0f, 1.0f};
    
    public void update(GLGraphics g) {
        g.glLightModeli(GLConstants.GL_LIGHT_MODEL_TWO_SIDE, 1);
        int light = GLConstants.GL_LIGHT0;
        g.glLightfv(light, GLConstants.GL_AMBIENT, ambient);
        g.glLightfv(light, GLConstants.GL_DIFFUSE, diffuse);
        g.glLightfv(light, GLConstants.GL_SPECULAR, specular);
        g.glLightfv(light, GLConstants.GL_POSITION, new float[]{getX(), getY(), getZ(), 1f});
        g.glEnable(light);
    }
    
    public void rotate() {
        angle += getSpeed();
        setX((int) (radius * Math.cos(Math.toRadians(angle))));
        setZ((int) (radius * Math.sin(Math.toRadians(angle))));
    }
    
}

Light Setting

The light can be positioned at GL_LIGHT0`GL_LIGHT7. GL_LIGHT0 is specified here. By default, the light comes from only one side, so use glLightModeli(GL_LIGHT_MODEL_TWOSIDE, 1) to have the light come from both sides. Set GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR for the light as well as the object's surfaces. This setting is for a white light.

The 3D position of the light is specified with GL_POSITION. Lastly, GL_LIGHT0 is enabled with glEnable.

Actually the Light object is called like this. Below is a part of the update method of the main Application:

            g.glClear(GLConstants.GL_COLOR_BUFFER_BIT | GLConstants.GL_DEPTH_BUFFER_BIT);
            
            g.glPushMatrix();
            int[] enables = {
                GLConstants.GL_DEPTH_TEST,
                GLConstants.GL_LIGHTING,
                GLConstants.GL_CULL_FACE,
            };
            for (int i = 0; i < enables.length; i++) {
                g.glEnable(enables[i]);
            }
            g.glMatrixMode(GLConstants.GL_PROJECTION);
            g.glLoadIdentity();
            camera.update(g);
            g.glMatrixMode(GLConstants.GL_MODELVIEW);
            g.glLoadIdentity();
            g.glPushMatrix();
            light.update(g);
            g.glPopMatrix();
            for (int i = 0; i < materials.length; i++) {
                g.glPushMatrix();
                materials[i].update(g);
                g.glPopMatrix();
            }
            for (int i = enables.length - 1; i >= 0; i--) {
                g.glDisable(enables[i]);
            }
            g.glPopMatrix();

First, GL_DEPTH_BUFFER_BIT is also cleared by glClear. To enable 3D display with lighting, GL_DEPTH_TEST, GL_LIGHTING, GL_CULL_FACE and so on are enabled. And then, the following setting sequence allows drawing.

  1. Camera setting
  2. Light setting
  3. Drawing the object

rotate Method for Rotation

The rotate method simply resets the position after rotating the light in orbit. It does not perform any drawing-related process here.

Methods that are called by multi-threads simply update the calculation or data, but do not call any drawing-related process. Drawing should be updated in the update method, in which the updated data are used.

Use java.util.Timer: Update at regular intervals

java.util.Timer can call the run method of the java.util.TimerTask object after a given period or keep calling repeatedly. Using this method, the process to rotate the Light object in orbit is called.

Constructor of the implementation class Application:

    private Timer timer;
    private Object monitor = new Object();
    
    public LightGoAround() {
        font = new SystemFont(new Font("System", Font.PLAIN, fontSize));
        camera = new Camera(width / (double) height, 10, cube1);
        light = new Light();
        
        timer = new Timer(true);
        timer.schedule(new TimerTask() {
            public void run() {
                synchronized (monitor) {
                    light.rotate();
                }
            }
        }, 0, 10);
        
        materials = new Material[] {
            cube1,
            cube2,
        };
    }

The constructor of the Timer class takes arguments to specify the thread to run as a daemon. There are several schedule methods and the method used here schedules a task so that "this TimerTask is repeatedly called every 0~10 milliseconds". The run method of TimerTask simply calls the rotate method of Light object.

Synchronization

In TimerTask, the task is enclosed in the synchronized block of monitor object. In the update method, it is also enclosed by the synchronized block of monitor object as follows.

    public void update(GLGraphics g) {
        synchronized (monitor) {
            g.glClear(GLConstants.GL_COLOR_BUFFER_BIT | GLConstants.GL_DEPTH_BUFFER_BIT);
            ...
        }
    }

This should be done so that while one method is running, the other will not be called because the Timer and update methods are called from different threads.

Life Game

Actually, above described is Life.java, the source code of Life Game that uses the timer.

CtQ[

Copyright © 2001-2002 CyberStep, Inc. All Rights Reserved.

Oni Software