A Simple Texture-Mapper

First, to describe the texture orientation, you'll create a Rectangle3D class, in Listing 8.1, which is a freely oriented rectangle in 3D space. In other words, it doesn't have to be aligned with an axis like the Rectangle2D class in java.awt.geom.

Listing 8.1 Rectangle3D.java
package com.brackeen.javagamebook.math3D;
/**
 A Rectangle3D is a rectangle in 3D space, defined as an origin
 and vectors pointing in the directions of the base (width) and
 side (height).
*/
public class Rectangle3D {
 private Vector3D origin;
 private Vector3D directionU;
 private Vector3D directionV;
 private Vector3D normal;
 private float width;
 private float height;
 /**
 Creates a rectangle at the origin with a width and height
 of zero.
 */
 public Rectangle3D() {
 origin = new Vector3D();
 directionU = new Vector3D(1,0,0);
 directionV = new Vector3D(0,1,0);
 width = 0;
 height = 0;
 }
 /**
 Creates a new Rectangle3D with the specified origin,
 direction of the base (directionU) and direction of
 the side (directionV).
 */
 public Rectangle3D(Vector3D origin, Vector3D directionU,
 Vector3D directionV, float width, float height)
 {
 this.origin = new Vector3D(origin);
 this.directionU = new Vector3D(directionU);
 this.directionU.normalize();
 this.directionV = new Vector3D(directionV);
 this.directionV.normalize();
 this.width = width;
 this.height = height;
 }
 /**
 Sets the values of this Rectangle3D to the specified
 Rectangle3D.
 */
 public void setTo(Rectangle3D rect) {
 origin.setTo(rect.origin);
 directionU.setTo(rect.directionU);
 directionV.setTo(rect.directionV);
 width = rect.width;
 height = rect.height;
 }
 /**
 Gets the origin of this Rectangle3D.
 */
 public Vector3D getOrigin() {
 return origin;
 }
 /**
 Gets the direction of the base of this Rectangle3D.
 */
 public Vector3D getDirectionU() {
 return directionU;
 }
 /**
 Gets the direction of the side of this Rectangle3D.
 */
 public Vector3D getDirectionV() {
 return directionV;
 }
 /**
 Gets the width of this Rectangle3D.
 */
 public float getWidth() {
 return width;
 }
 /**
 Sets the width of this Rectangle3D.
 */
 public void setWidth(float width) {
 this.width = width;
 }
 /**
 Gets the height of this Rectangle3D.
 */
 public float getHeight() {
 return height;
 }
 /**
 Sets the height of this Rectangle3D.
 */
 public void setHeight(float height) {
 this.height = height;
 }
 /**
 Calculates the normal vector of this Rectangle3D.
 */
 protected Vector3D calcNormal() {
 if (normal == null) {
 normal = new Vector3D();
 }
 normal.setToCrossProduct(directionU, directionV);
 normal.normalize();
 return normal;
 }
 /**
 Gets the normal of this Rectangle3D.
 */
 public Vector3D getNormal() {
 if (normal == null) {
 calcNormal();
 }
 return normal;
 }
 /**
 Sets the normal of this Rectangle3D.
 */
 public void setNormal(Vector3D n) {
 if (normal == null) {
 normal = new Vector3D(n);
 }
 else {
 normal.setTo(n);
 }
 }
 public void add(Vector3D u) {
 origin.add(u);
 // don't translate direction vectors or size
 }
 public void subtract(Vector3D u) {
 origin.subtract(u);
 // don't translate direction vectors or size
 }
 public void add(Transform3D xform) {
 addRotation(xform);
 add(xform.getLocation());
 }
 public void subtract(Transform3D xform) {
 subtract(xform.getLocation());
 subtractRotation(xform);
 }
 public void addRotation(Transform3D xform) {
 origin.addRotation(xform);
 directionU.addRotation(xform);
 directionV.addRotation(xform);
 }
 public void subtractRotation(Transform3D xform) {
 origin.subtractRotation(xform);
 directionU.subtractRotation(xform);
 directionV.subtractRotation(xform);
 }
}


Simply put, the Rectangle3D class keeps track of three vectors (the origin, U, and V) and the rectangle's normal. Now you'll create a SimpleTexturedPolygonRenderer class, shown in Listing 8.2. This is a very straightforward subclass of the PolygonRenderer you created in , and it simply follows the texture mapping equations.

Listing 8.2 SimpleTexturedPolygonRenderer.java
package com.brackeen.javagamebook.graphics3D;
import java.awt.*;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import com.brackeen.javagamebook.math3D.*;
/**
 The SimpleTexturedPolygonRenderer class demonstrates
 the fundamentals of perspective-correct texture mapping.
 It is very slow and maps the same texture for every polygon.
*/
public class SimpleTexturedPolygonRenderer extends PolygonRenderer
{
 protected Vector3D a = new Vector3D();
 protected Vector3D b = new Vector3D();
 protected Vector3D c = new Vector3D();
 protected Vector3D viewPos = new Vector3D();
 protected Rectangle3D textureBounds = new Rectangle3D();
 protected BufferedImage texture;
 public SimpleTexturedPolygonRenderer(Transform3D camera,
 ViewWindow viewWindow, String textureFile)
 {
 super(camera, viewWindow);
 texture = loadTexture(textureFile);
 }
 /**
 Loads the texture image from a file. This image is used
 for all polygons.
 */
 public BufferedImage loadTexture(String filename) {
 try {
 return ImageIO.read(new File(filename));
 }
 catch (IOException ex) {
 ex.printStackTrace();
 return null;
 }
 }
 protected void drawCurrentPolygon(Graphics2D g) {
 // Calculate texture bounds.
 // Ideally texture bounds are pre-calculated and stored
 // with the polygon. Coordinates are computed here for
 // demonstration purposes.
 Vector3D textureOrigin = textureBounds.getOrigin();
 Vector3D textureDirectionU = textureBounds.getDirectionU();
 Vector3D textureDirectionV = textureBounds.getDirectionV();
 textureOrigin.setTo(sourcePolygon.getVertex(0));
 textureDirectionU.setTo(sourcePolygon.getVertex(3));
 textureDirectionU.subtract(textureOrigin);
 textureDirectionU.normalize();
 textureDirectionV.setTo(sourcePolygon.getVertex(1));
 textureDirectionV.subtract(textureOrigin);
 textureDirectionV.normalize();
 // transform the texture bounds
 textureBounds.subtract(camera);
 // start texture-mapping calculations
 a.setToCrossProduct(textureBounds.getDirectionV(),
 textureBounds.getOrigin());
 b.setToCrossProduct(textureBounds.getOrigin(),
 textureBounds.getDirectionU());
 c.setToCrossProduct(textureBounds.getDirectionU(),
 textureBounds.getDirectionV());
 int y = scanConverter.getTopBoundary();
 viewPos.z = -viewWindow.getDistance();
 while (y<=scanConverter.getBottomBoundary()) {
 ScanConverter.Scan scan = scanConverter.getScan(y);
 if (scan.isValid()) {
 viewPos.y =
 viewWindow.convertFromScreenYToViewY(y);
 for (int x=scan.left; x<=scan.right; x++) {
 viewPos.x =
 viewWindow.convertFromScreenXToViewX(x);
 // compute the texture location
 int tx = (int)(a.getDotProduct(viewPos) /
 c.getDotProduct(viewPos));
 int ty = (int)(b.getDotProduct(viewPos) /
 c.getDotProduct(viewPos));
 // get the color to draw
 try {
 int color = texture.getRGB(tx, ty);
 g.setColor(new Color(color));
 }
 catch (ArrayIndexOutOfBoundsException ex) {
 g.setColor(Color.red);
 }
 // draw the pixel
 g.drawLine(x,y,x,y);
 }
 }
 y++;
 }
 }
}


This class uses a BufferedImage to store the texture. During the rendering process, it creates a new Color object for every pixel. Because the Graphics2D class has no drawPixel() method, you use drawLine() to draw a 1-pixel-long line. One thing to note is that the renderer does not tile textures; it requires the texture be the same size as the polygon. We get to texture tiling in the next section, but for now, this is a start. Now let's create a simple test to try it out. The TextureMapTest1 class in Listing 8.3 creates a single polygon and draws it on the screen, allowing the user to wander around it.

Listing 8.3 TextureMapTest1.java
import com.brackeen.javagamebook.math3D.*;
import com.brackeen.javagamebook.graphics3D.*;
import com.brackeen.javagamebook.test.GameCore3D;
public class TextureMapTest1 extends GameCore3D {
 public static void main(String[] args) {
 new TextureMapTest1().run();
 }
 public void createPolygons() {
 Polygon3D poly;
 // one wall for now
 poly = new Polygon3D(
 new Vector3D(-128, 256, -1000),
 new Vector3D(-128, 0, -1000),
 new Vector3D(128, 0, -1000),
 new Vector3D(128, 256, -1000));
 polygons.add(poly);
 }
 public void createPolygonRenderer() {
 viewWindow = new ViewWindow(0, 0,
 screen.getWidth(), screen.getHeight(),
 (float)Math.toRadians(75));
 Transform3D camera = new Transform3D(0,100,0);
 polygonRenderer = new SimpleTexturedPolygonRenderer(
 camera, viewWindow, "../images/test_pattern.png");
 }
}


TextureMapTest1 extends the GameCore3D class, which is pretty much the same as Simple3DTest2 from the previous chapter. All TextureMapTest1 does is create a polygon and a polygon renderer, and lets the user wander around the world. See Screenshot to see what it looks like.

Screenshot The TextureMapTest1 demo draws a test-pattern texture over a polygon.

Java graphics 08fig05.gif


The texture in TextureMapTest1 is a simple asymmetrical test pattern that can be use to ensure the texture is mapped correctly onto the polygon. It is easy to tell which side is left or right and which side is up and down, so you know if you've got all of your math right. If the arrow in the texture was pointing the wrong away or if the image was flipped, you would know the equations might be off or that you're missing a sign somewhere.

Problems with the First Texture-Mapper

When you run the test, you'll probably notice that it is excruciatingly slow. It might take more than 1 second to draw a single frame, depending on the speed of your machine. There are several reasons for this slow behavior:

  • Creating a new Color object for every pixel is too much overhead per pixel. It also gives the garbage collector more work to do.
  • Calling the setColor() and drawLine() methods is also too much overhead per pixel.
  • The BufferedImage's getRGB() method must convert a pixel to 24-bit color if it's not already in this format.
  • You're performing too many calculations per pixel.
  • Catching a thrown ArrayIndexOutOfBoundsException is actually slower than manually checking the array bounds. However, this really isn't an issue in this case because the test is set up to avoid these exceptions.

Note that none of these issues is a problem for normal tasks—don't think that creating Color objects or catching exceptions are slow in general. For most purposes, these things are fine; they happen to be slow only when you try to do these things for every pixel on screen at a high frame rate. To get an idea of the magnitude of what you're trying to accomplish, with a 640x480 screen at 60 frames per second, you're drawing 18,432,000 pixels every second. So, it's important to use the fastest method possible for drawing a single pixel, which means you should avoid some coding techniques that would be acceptable in other situations.

Screenshot


   
Comments