Final Rendering Pipeline

Now you must extend the simple 3D pipeline used for Simple3DTest1 and add back-face removal, clipping, and scan converting:

  1. Check if facing camera.
  2. Apply transform.
  3. Clip.
  4. Project onto view window.
  5. Scan-convert.
  6. Draw.

To draw using this new 3D pipeline, you'll create the abstract PolygonRenderer, shown in .

Listing 7.9 PolygonRenderer.java

package com.brackeen.javagamebook.graphics3D;
import java.awt.Graphics2D;
import java.awt.Color;
import com.brackeen.javagamebook.math3D.*;
/**
 The PolygonRenderer class is an abstract class that transforms
 and draws polygons onto the screen.
*/
public abstract class PolygonRenderer {
 protected ScanConverter scanConverter;
 protected Transform3D camera;
 protected ViewWindow viewWindow;
 protected boolean clearViewEveryFrame;
 protected Polygon3D sourcePolygon;
 protected Polygon3D destPolygon;
 /**
 Creates a new PolygonRenderer with the specified
 Transform3D (camera) and ViewWindow. The view is cleared
 when startFrame() is called.
 */
 public PolygonRenderer(Transform3D camera,
 ViewWindow viewWindow)
 {
 this(camera, viewWindow, true);
 }
 /**
 Creates a new PolygonRenderer with the specified
 Transform3D (camera) and ViewWindow. If
 clearViewEveryFrame is true, the view is cleared when
 startFrame() is called.
 */
 public PolygonRenderer(Transform3D camera,
 ViewWindow viewWindow, boolean clearViewEveryFrame)
 {
 this.camera = camera;
 this.viewWindow = viewWindow;
 this.clearViewEveryFrame = clearViewEveryFrame;
 init();
 }
 /**
 Create the scan converter and dest polygon.
 */
 protected void init() {
 destPolygon = new Polygon3D();
 scanConverter = new ScanConverter(viewWindow);
 }
 /**
 Gets the camera used for this PolygonRenderer.
 */
 public Transform3D getCamera() {
 return camera;
 }
 /**
 Indicates the start of rendering of a frame. This method
 should be called every frame before any polygons are drawn.
 */
 public void startFrame(Graphics2D g) {
 if (clearViewEveryFrame) {
 g.setColor(Color.black);
 g.fillRect(viewWindow.getLeftOffset(),
 viewWindow.getTopOffset(),
 viewWindow.getWidth(), viewWindow.getHeight());
 }
 }
 /**
 Indicates the end of rendering of a frame. This method
 should be called every frame after all polygons are drawn.
 */
 public void endFrame(Graphics2D g) {
 // do nothing, for now.
 }
 /**
 Transforms and draws a polygon.
 */
 public boolean draw(Graphics2D g, Polygon3D poly) {
 if (poly.isFacing(camera.getLocation())) {
 sourcePolygon = poly;
 destPolygon.setTo(poly);
 destPolygon.subtract(camera);
 boolean visible = destPolygon.clip(-1);
 if (visible) {
 destPolygon.project(viewWindow);
 visible = scanConverter.convert(destPolygon);
 if (visible) {
 drawCurrentPolygon(g);
 return true;
 }
 }
 }
 return false;
 }
 /**
 Draws the current polygon. At this point, the current
 polygon is transformed, clipped, projected,
 scan-converted, and visible.
 */
 protected abstract void drawCurrentPolygon(Graphics2D g);
}

The PolygonRenderer class is an abstract class that provides a framework to render polygons. Its main purpose is to implement the rendering pipeline for a polygon in the draw() method. Any subclasses need to implement the drawCurrentPolygon() method to actually draw the final, scan-converted polygon. Also, the renderer can optionally clear the view before drawing every frame, provided the startFrame() method is called. You'll want to clear the view because the examples don't have polygons that fill the entire screen. For 3D scenes in which every pixel is covered by a polygon, there's no reason to clear the view. You'll create a few different PolygonRenderer subclasses for different types of polygons in the next chapter, "." Currently, you are dealing only with solid-colored polygons, so you'll create a SolidPolygonRenderer subclass, whose drawCurrentPolygon() method is shown in .

Listing 7.10 SolidPolygonRenderer.drawCurrentPolygon

protected void drawCurrentPolygon(Graphics2D g) {
 // set the color
 if (sourcePolygon instanceof SolidPolygon3D) {
 g.setColor(((SolidPolygon3D)sourcePolygon).getColor());
 }
 else {
 g.setColor(Color.GREEN);
 }
 // draw the scans
 int y = scanConverter.getTopBoundary();
 while (y<=scanConverter.getBottomBoundary()) {
 ScanConverter.Scan scan = scanConverter.getScan(y);
 if (scan.isValid()) {
 g.drawLine(scan.left, y, scan.right, y);
 }
 y++;
 }
}

The SolidPolygonRenderer class implements the drawCurrentPolygon() method, which draws a polygon from the scan list in the ScanConverter. Note that this method is called only from the draw() method, which means that when it is called, the current destPolygon is transformed, clipped, projected, scan-converted, and visible in the view window. The source polygon is also available to get any necessary data out of it. If the source polygon isn't an instance of the SolidPolygon3D class, (in other words, if the polygon has no color associated with it), the polygon is filled with green. Now you'll create another test to try out the new renderer. This new test, called Simple3DTest2, creates a 3D convex polyhedron that looks strangely like a house. It has a couple front-facing polygons that imitate a door and a window, as shown in .

Screenshot Screenshot of Simple3DTest2, which includes clipping, back-face removal, a custom polygon renderer, and free camera movement.

Java graphics 07fig24

Simple3DTest2 enables the user to wander freely around the house, looking up, down, left, and right, and moving anywhere, even within the house. Note that if you actually go in the house, you won't see anything because every polygon is facing away from the camera. The entire Simple3DTest2 code is too long to list here (a bulk of if is just creating polygons), but we'll talk about the relevant parts here. Simple3DTest2 uses the SolidPolygonRenderer to draw every polygon in the list of polygons:

public void draw(Graphics2D g) {
 // draw polygons
 polygonRenderer.startFrame(g);
 for (int i=0; i<polygons.size(); i++) {
 polygonRenderer.draw(g, (Polygon3D)polygons.get(i));
 }
 polygonRenderer.endFrame(g);
 drawText(g);
}

Simple3DTest2 also draws some onscreen instructions and can optionally display the frame rate of the renderer:

private boolean drawFrameRate = false;
private boolean drawInstructions = true;
private int numFrames;
private long startTime;
private float frameRate;
...
public void drawText(Graphics g) {
 // draw text
 g.setColor(Color.WHITE);
 if (drawInstructions) {
 g.drawString("Use the mouse/arrow keys to move. " +
 "Press Esc to exit.", 5, fontSize);
 }
 // (you may have to turn off the BufferStrategy in
 // ScreenManager for more accurate tests)
 if (drawFrameRate) {
 calcFrameRate();
 g.drawString(frameRate + " frames/sec", 5,
 screen.getHeight() - 5);
 }
}
public void calcFrameRate() {
 numFrames++;
 long currTime = System.currentTimeMillis();
 // calculate the frame rate every 500 milliseconds
 if (currTime > startTime + 500) {
 frameRate = (float)numFrames * 1000 /
 (currTime - startTime);
 startTime = currTime;
 numFrames = 0;
 }
}

The frame rate is recalculated every 500ms and just involves dividing the number of frames drawn by the amount of time that has passed. An alternative way to calculate the frame rate is to just keep a running average, but this doesn't give a good look at the current frame rate because the frame rate can vary from time to time based on how much the renderer has to draw. The camera's transform is updated in the update() method of Simple3DTest2:

public void update(long elapsedTime) {
 if (exit.isPressed()) {
 stop();
 return;
 }
 // check options
 if (largerView.isPressed()) {
 setViewBounds(viewWindow.getWidth() + 64,
 viewWindow.getHeight() + 48);
 }
 else if (smallerView.isPressed()) {
 setViewBounds(viewWindow.getWidth() - 64,
 viewWindow.getHeight() - 48);
 }
 if (frameRateToggle.isPressed()) {
 drawFrameRate = !drawFrameRate;
 }
 // cap elapsedTime
 elapsedTime = Math.min(elapsedTime, 100);
 float angleChange = 0.0002f*elapsedTime;
 float distanceChange = .5f*elapsedTime;
 Transform3D camera = polygonRenderer.getCamera();
 Vector3D cameraLoc = camera.getLocation();
 // apply movement
 if (goForward.isPressed()) {
 cameraLoc.x -= distanceChange * camera.getSinAngleY();
 cameraLoc.z -= distanceChange * camera.getCosAngleY();
 }
 if (goBackward.isPressed()) {
 cameraLoc.x += distanceChange * camera.getSinAngleY();
 cameraLoc.z += distanceChange * camera.getCosAngleY();
 }
 if (goLeft.isPressed()) {
 cameraLoc.x -= distanceChange * camera.getCosAngleY();
 cameraLoc.z += distanceChange * camera.getSinAngleY();
 }
 if (goRight.isPressed()) {
 cameraLoc.x += distanceChange * camera.getCosAngleY();
 cameraLoc.z -= distanceChange * camera.getSinAngleY();
 }
 if (goUp.isPressed()) {
 cameraLoc.y += distanceChange;
 }
 if (goDown.isPressed()) {
 cameraLoc.y -= distanceChange;
 }
 // look up/down (rotate around x)
 int tilt = tiltUp.getAmount() - tiltDown.getAmount();
 tilt = Math.min(tilt, 200);
 tilt = Math.max(tilt, -200);
 // limit how far you can look up/down
 float newAngleX = camera.getAngleX() + tilt * angleChange;
 newAngleX = Math.max(newAngleX, (float)-Math.PI/2);
 newAngleX = Math.min(newAngleX, (float)Math.PI/2);
 camera.setAngleX(newAngleX);
 // turn (rotate around y)
 int turn = turnLeft.getAmount() - turnRight.getAmount();
 turn = Math.min(turn, 200);
 turn = Math.max(turn, -200);
 camera.rotateAngleY(turn * angleChange);
 // tilet head left/right (rotate around z)
 if (tiltLeft.isPressed()) {
 camera.rotateAngleZ(10*angleChange);
 }
 if (tiltRight.isPressed()) {
 camera.rotateAngleZ(-10*angleChange);
 }
}

The update() method takes both the keyboard and mouse as input, and moves the camera accordingly. It uses the mouselook feature you developed in , "Interactivity and User Interfaces," to enable the user to look around the 3D world. Also, it limits how far up and down the user can look so the player doesn't have to bend his or her head all the way back and around again. Here's a list of all the controls in the demo:

Mouse move Turn left/right; look up/down
Arrow keys; W, S, A, D Move forward, back, left, right
Page up/down Move up/down
Insert/delete Tilt left/right
+ Increase view window size
- Decrease view window size
R Show frame rate
Esc Exit
Using +/- to change the view window is handy in case a demo is running too slowly at full screen and you want to decrease the view window to make it run faster.
/**
 Sets the view bounds, centering the view on the screen.
*/
public void setViewBounds(int width, int height) {
 width = Math.min(width, screen.getWidth());
 height = Math.min(height, screen.getHeight());
 width = Math.max(64, width);
 height = Math.max(48, height);
 viewWindow.setBounds((screen.getWidth() - width) /2,
 (screen.getHeight() - height) /2, width, height);
}

When you run the demo, you'll also notice that the sides of the house are a shade darker than the rest. No, this isn't a secret shading feature of the 3D engine I forgot to mention-those polygons are just given a darker color by hand. In the next chapter, we get into real lighting and shading.