|
/*
DEVELOPING GAME IN JAVA
Caracteristiques
Editeur : NEW RIDERS
Auteur : BRACKEEN
Parution : 09 2003
Pages : 972
Isbn : 1-59273-005-1
Reliure : Paperback
Disponibilite : Disponible a la librairie
*/
import java.awt.AWTException;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.DisplayMode;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import java.awt.*;
import java.util.List;
import java.util.ArrayList;
public class BSPTest3D extends GameCore3D {
public static void main(String[] args) {
new BSPTest3D().run();
}
protected BSPTree bspTree;
public void init() {
init(LOW_RES_MODES);
}
public void createPolygons() {
ShadedTexture floorTexture = (ShadedTexture)
Texture.createTexture("../images/roof1.png", true);
ShadedTexture ceilingTexture = (ShadedTexture)
Texture.createTexture("../images/roof2.png", true);
ShadedTexture wallTexture = (ShadedTexture)
Texture.createTexture("../images/wall1.png", true);
// The floor/ceiling polygons
Polygon3D floor = new BSPPolygon(new Vector3D[] {
new Vector3D(0,0,150), new Vector3D(0,0,450),
new Vector3D(800,0,450), new Vector3D(800,0,300),
new Vector3D(500,0,300), new Vector3D(500,0,75),
}, BSPPolygon.TYPE_FLOOR);
Polygon3D ceiling = new BSPPolygon(new Vector3D[] {
new Vector3D(0,300,450), new Vector3D(0,300,150),
new Vector3D(500,300,75), new Vector3D(500,300,300),
new Vector3D(800,300,300), new Vector3D(800,300,450),
}, BSPPolygon.TYPE_FLOOR);
polygons.add(floor);
polygons.add(ceiling);
setTexture(floor, floorTexture);
setTexture(ceiling, ceilingTexture);
// vertices defined from left to right as the viewer
// looks at the wall
BSPPolygon wallA = createPolygon(
new BSPLine(0, 150, 500, 75), 0, 300);
BSPPolygon wallB = createPolygon(
new BSPLine(500, 75, 500, 300), 0, 300);
BSPPolygon wallC = createPolygon(
new BSPLine(500, 300, 800, 300), 0, 300);
BSPPolygon wallD = createPolygon(
new BSPLine(800, 450, 0, 450), 0, 300);
BSPPolygon wallE = createPolygon(
new BSPLine(0, 450, 0, 150), 0, 300);
BSPPolygon wallF = createPolygon(
new BSPLine(800, 300, 800, 450), 0, 300);
polygons.add(wallA);
polygons.add(wallB);
polygons.add(wallC);
polygons.add(wallD);
polygons.add(wallE);
polygons.add(wallF);
setTexture(wallA, wallTexture);
setTexture(wallB, wallTexture);
setTexture(wallC, wallTexture);
setTexture(wallD, wallTexture);
setTexture(wallE, wallTexture);
setTexture(wallF, wallTexture);
BSPTreeBuilder builder = new BSPTreeBuilder();
bspTree = builder.build(polygons);
// build surfaces
ArrayList lights = new ArrayList();
lights.add(new PointLight3D(400, 200, 100, 1, 300));
lights.add(new PointLight3D(700, 200, 400, .5f, 1000));
lights.add(new PointLight3D(65, 200, 385, 1, 100));
bspTree.createSurfaces(lights);
}
public BSPPolygon createPolygon(BSPLine line, float bottom,
float top)
{
return new BSPPolygon(new Vector3D[] {
new Vector3D(line.x1, bottom, line.y1),
new Vector3D(line.x2, bottom, line.y2),
new Vector3D(line.x2, top, line.y2),
new Vector3D(line.x1, top, line.y1)
}, BSPPolygon.TYPE_WALL);
}
public void setTexture(Polygon3D poly, Texture texture) {
Vector3D origin = poly.getVertex(1);
Vector3D dv = new Vector3D(poly.getVertex(0));
dv.subtract(origin);
Vector3D du = new Vector3D();
du.setToCrossProduct(poly.getNormal(), dv);
Rectangle3D texBounds = new Rectangle3D(origin, du, dv,
texture.getWidth(), texture.getHeight());
((TexturedPolygon3D)poly).setTexture(texture, texBounds);
}
public void createPolygonRenderer() {
// make the view window the entire screen
viewWindow = new ViewWindow(0, 0,
screen.getWidth(), screen.getHeight(),
(float)Math.toRadians(75));
Transform3D camera = new Transform3D(400,100,300);
polygonRenderer = new SimpleBSPRenderer(
camera, viewWindow);
}
public void draw(Graphics2D g) {
// draw polygons
polygonRenderer.startFrame(g);
((SimpleBSPRenderer)polygonRenderer).draw(g, bspTree);
polygonRenderer.endFrame(g);
super.drawText(g);
}
}
/**
The SimpleBSPRenderer class is a renderer capable of drawing
polygons in a BSP tree and any polygon objects in the scene.
No Z-buffering is used.
*/
class SimpleBSPRenderer extends ShadedSurfacePolygonRenderer
implements BSPTreeTraverseListener
{
protected Graphics2D currentGraphics2D;
protected BSPTreeTraverser traverser;
protected boolean viewNotFilledFirstTime;
/**
Creates a new BSP renderer with the specified camera
object and view window.
*/
public SimpleBSPRenderer(Transform3D camera,
ViewWindow viewWindow)
{
super(camera, viewWindow, false);
viewNotFilledFirstTime = true;
}
protected void init() {
traverser = new BSPTreeTraverser(this);
destPolygon = new TexturedPolygon3D();
scanConverter = new SortedScanConverter(viewWindow);
((SortedScanConverter)scanConverter).setSortedMode(true);
// create renderers for each texture (HotSpot optimization)
scanRenderers = new HashMap();
scanRenderers.put(PowerOf2Texture.class,
new PowerOf2TextureRenderer());
scanRenderers.put(ShadedTexture.class,
new ShadedTextureRenderer());
scanRenderers.put(ShadedSurface.class,
new ShadedSurfaceRenderer());
}
public void startFrame(Graphics2D g) {
super.startFrame(g);
((SortedScanConverter)scanConverter).clear();
}
public void endFrame(Graphics2D g) {
super.endFrame(g);
if (!((SortedScanConverter)scanConverter).isFilled()) {
g.drawString("View not completely filled", 5,
viewWindow.getTopOffset() +
viewWindow.getHeight() - 5);
if (viewNotFilledFirstTime) {
viewNotFilledFirstTime = false;
// print message to console in case user missed it
System.out.println("View not completely filled.");
}
// clear the background next time
clearViewEveryFrame = true;
}
else {
clearViewEveryFrame = false;
}
}
/**
Draws the visible polygons in a BSP tree based on
the camera location. The polygons are drawn front-to-back.
*/
public void draw(Graphics2D g, BSPTree tree) {
currentGraphics2D = g;
traverser.traverse(tree, camera.getLocation());
}
// from the BSPTreeTraverseListener interface
public boolean visitPolygon(BSPPolygon poly, boolean isBack) {
draw(currentGraphics2D, poly);
return !((SortedScanConverter)scanConverter).isFilled();
}
protected void drawCurrentPolygon(Graphics2D g) {
if (!(sourcePolygon instanceof TexturedPolygon3D)) {
// not a textured polygon - return
return;
}
buildSurface();
SortedScanConverter scanConverter =
(SortedScanConverter)this.scanConverter;
TexturedPolygon3D poly = (TexturedPolygon3D)destPolygon;
Texture texture = poly.getTexture();
ScanRenderer scanRenderer =
(ScanRenderer)scanRenderers.get(texture.getClass());
scanRenderer.setTexture(texture);
Rectangle3D textureBounds = poly.getTextureBounds();
a.setToCrossProduct(textureBounds.getDirectionV(),
textureBounds.getOrigin());
b.setToCrossProduct(textureBounds.getOrigin(),
textureBounds.getDirectionU());
c.setToCrossProduct(textureBounds.getDirectionU(),
textureBounds.getDirectionV());
int y = scanConverter.getTopBoundary();
viewPos.y = viewWindow.convertFromScreenYToViewY(y);
viewPos.z = -viewWindow.getDistance();
while (y<=scanConverter.getBottomBoundary()) {
for (int i=0; i<scanConverter.getNumScans(y); i++) {
ScanConverter.Scan scan =
scanConverter.getScan(y, i);
if (scan.isValid()) {
viewPos.x = viewWindow.
convertFromScreenXToViewX(scan.left);
int offset = (y - viewWindow.getTopOffset()) *
viewWindow.getWidth() +
(scan.left - viewWindow.getLeftOffset());
scanRenderer.render(offset, scan.left,
scan.right);
}
}
y++;
viewPos.y--;
}
}
}
/**
* The PolygonRenderer class is an abstract class that transforms and draws
* polygons onto the screen.
*/
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 ScanConverter class converts a projected polygon into a series of
* horizontal scans for drawing.
*/
class ScanConverter {
private static final int SCALE_BITS = 16;
private static final int SCALE = 1 << SCALE_BITS;
private static final int SCALE_MASK = SCALE - 1;
protected ViewWindow view;
protected Scan[] scans;
protected int top;
protected int bottom;
/**
* A horizontal scan line.
*/
public static class Scan {
public int left;
public int right;
/**
* Sets the left and right boundary for this scan if the x value is
* outside the current boundary.
*/
public void setBoundary(int x) {
if (x < left) {
left = x;
}
if (x - 1 > right) {
right = x - 1;
}
}
/**
* Clears this scan line.
*/
public void clear() {
left = Integer.MAX_VALUE;
right = Integer.MIN_VALUE;
}
/**
* Determines if this scan is valid (if left <= right).
*/
public boolean isValid() {
return (left <= right);
}
/**
* Sets this scan.
*/
public void setTo(int left, int right) {
this.left = left;
this.right = right;
}
/**
* Checks if this scan is equal to the specified values.
*/
public boolean equals(int left, int right) {
return (this.left == left && this.right == right);
}
}
/**
* Creates a new ScanConverter for the specified ViewWindow. The
* ViewWindow's properties can change in between scan conversions.
*/
public ScanConverter(ViewWindow view) {
this.view = view;
}
/**
* Gets the top boundary of the last scan-converted polygon.
*/
public int getTopBoundary() {
return top;
}
/**
* Gets the bottom boundary of the last scan-converted polygon.
*/
public int getBottomBoundary() {
return bottom;
}
/**
* Gets the scan line for the specified y value.
*/
public Scan getScan(int y) {
return scans[y];
}
/**
* Ensures this ScanConverter has the capacity to scan-convert a polygon to
* the ViewWindow.
*/
protected void ensureCapacity() {
int height = view.getTopOffset() + view.getHeight();
if (scans == null || scans.length != height) {
scans = new Scan[height];
for (int i = 0; i < height; i++) {
scans[i] = new Scan();
}
// set top and bottom so clearCurrentScan clears all
top = 0;
bottom = height - 1;
}
}
/**
* Clears the current scan.
*/
private void clearCurrentScan() {
for (int i = top; i <= bottom; i++) {
scans[i].clear();
}
top = Integer.MAX_VALUE;
bottom = Integer.MIN_VALUE;
}
/**
* Scan-converts a projected polygon. Returns true if the polygon is visible
* in the view window.
*/
public boolean convert(Polygon3D polygon) {
ensureCapacity();
clearCurrentScan();
int minX = view.getLeftOffset();
int maxX = view.getLeftOffset() + view.getWidth() - 1;
int minY = view.getTopOffset();
int maxY = view.getTopOffset() + view.getHeight() - 1;
int numVertices = polygon.getNumVertices();
for (int i = 0; i < numVertices; i++) {
Vector3D v1 = polygon.getVertex(i);
Vector3D v2;
if (i == numVertices - 1) {
v2 = polygon.getVertex(0);
} else {
v2 = polygon.getVertex(i + 1);
}
// ensure v1.y < v2.y
if (v1.y > v2.y) {
Vector3D temp = v1;
v1 = v2;
v2 = temp;
}
float dy = v2.y - v1.y;
// ignore horizontal lines
if (dy == 0) {
continue;
}
int startY = Math.max(MoreMath.ceil(v1.y), minY);
int endY = Math.min(MoreMath.ceil(v2.y) - 1, maxY);
top = Math.min(top, startY);
bottom = Math.max(bottom, endY);
float dx = v2.x - v1.x;
// special case: vertical line
if (dx == 0) {
int x = MoreMath.ceil(v1.x);
// ensure x within view bounds
x = Math.min(maxX + 1, Math.max(x, minX));
for (int y = startY; y <= endY; y++) {
scans[y].setBoundary(x);
}
} else {
// scan-convert this edge (line equation)
float gradient = dx / dy;
// (slower version)
/*
* for (int y=startY; y <=endY; y++) { int x =
* MoreMath.ceil(v1.x + (y - v1.y) * gradient); // ensure x
* within view bounds x = Math.min(maxX+1, Math.max(x, minX));
* scans[y].setBoundary(x); }
*/
// (faster version)
// trim start of line
float startX = v1.x + (startY - v1.y) * gradient;
if (startX < minX) {
int yInt = (int) (v1.y + (minX - v1.x) / gradient);
yInt = Math.min(yInt, endY);
while (startY <= yInt) {
scans[startY].setBoundary(minX);
startY++;
}
} else if (startX > maxX) {
int yInt = (int) (v1.y + (maxX - v1.x) / gradient);
yInt = Math.min(yInt, endY);
while (startY <= yInt) {
scans[startY].setBoundary(maxX + 1);
startY++;
}
}
if (startY > endY) {
continue;
}
// trim back of line
float endX = v1.x + (endY - v1.y) * gradient;
if (endX < minX) {
int yInt = MoreMath.ceil(v1.y + (minX - v1.x) / gradient);
yInt = Math.max(yInt, startY);
while (endY >= yInt) {
scans[endY].setBoundary(minX);
endY--;
}
} else if (endX > maxX) {
int yInt = MoreMath.ceil(v1.y + (maxX - v1.x) / gradient);
yInt = Math.max(yInt, startY);
while (endY >= yInt) {
scans[endY].setBoundary(maxX + 1);
endY--;
}
}
if (startY > endY) {
continue;
}
// line equation using integers
int xScaled = (int) (SCALE * v1.x + SCALE * (startY - v1.y)
* dx / dy)
+ SCALE_MASK;
int dxScaled = (int) (dx * SCALE / dy);
for (int y = startY; y <= endY; y++) {
scans[y].setBoundary(xScaled >> SCALE_BITS);
xScaled += dxScaled;
}
}
}
// check if visible (any valid scans)
for (int i = top; i <= bottom; i++) {
if (scans[i].isValid()) {
return true;
}
}
return false;
}
}
/**
* The MoreMath class provides functions not contained in the java.lang.Math or
* java.lang.StrictMath classes.
*/
class MoreMath {
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(short v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(int v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(long v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(float v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Returns the sign of the number. Returns -1 for negative, 1 for positive,
* and 0 otherwise.
*/
public static int sign(double v) {
return (v > 0) ? 1 : (v < 0) ? -1 : 0;
}
/**
* Faster ceil function to convert a float to an int. Contrary to the
* java.lang.Math ceil function, this function takes a float as an argument,
* returns an int instead of a double, and does not consider special cases.
*/
public static int ceil(float f) {
if (f > 0) {
return (int) f + 1;
} else {
return (int) f;
}
}
/**
* Faster floor function to convert a float to an int. Contrary to the
* java.lang.Math floor function, this function takes a float as an
* argument, returns an int instead of a double, and does not consider
* special cases.
*/
public static int floor(float f) {
if (f >= 0) {
return (int) f;
} else {
return (int) f - 1;
}
}
/**
* Returns true if the specified number is a power of 2.
*/
public static boolean isPowerOfTwo(int n) {
return ((n & (n - 1)) == 0);
}
/**
* Gets the number of "on" bits in an integer.
*/
public static int getBitCount(int n) {
int count = 0;
while (n > 0) {
count += (n & 1);
n >>= 1;
}
return count;
}
}
/**
* The ViewWindow class represents the geometry of a view window for 3D viewing.
*/
class ViewWindow {
private Rectangle bounds;
private float angle;
private float distanceToCamera;
/**
* Creates a new ViewWindow with the specified bounds on the screen and
* horizontal view angle.
*/
public ViewWindow(int left, int top, int width, int height, float angle) {
bounds = new Rectangle();
this.angle = angle;
setBounds(left, top, width, height);
}
/**
* Sets the bounds for this ViewWindow on the screen.
*/
public void setBounds(int left, int top, int width, int height) {
bounds.x = left;
bounds.y = top;
bounds.width = width;
bounds.height = height;
distanceToCamera = (bounds.width / 2) / (float) Math.tan(angle / 2);
}
/**
* Sets the horizontal view angle for this ViewWindow.
*/
public void setAngle(float angle) {
this.angle = angle;
distanceToCamera = (bounds.width / 2) / (float) Math.tan(angle / 2);
}
/**
* Gets the horizontal view angle of this view window.
*/
public float getAngle() {
return angle;
}
/**
* Gets the width of this view window.
*/
public int getWidth() {
return bounds.width;
}
/**
* Gets the height of this view window.
*/
public int getHeight() {
return bounds.height;
}
/**
* Gets the y offset of this view window on the screen.
*/
public int getTopOffset() {
return bounds.y;
}
/**
* Gets the x offset of this view window on the screen.
*/
public int getLeftOffset() {
return bounds.x;
}
/**
* Gets the distance from the camera to to this view window.
*/
public float getDistance() {
return distanceToCamera;
}
/**
* Converts an x coordinate on this view window to the corresponding x
* coordinate on the screen.
*/
public float convertFromViewXToScreenX(float x) {
return x + bounds.x + bounds.width / 2;
}
/**
* Converts a y coordinate on this view window to the corresponding y
* coordinate on the screen.
*/
public float convertFromViewYToScreenY(float y) {
return -y + bounds.y + bounds.height / 2;
}
/**
* Converts an x coordinate on the screen to the corresponding x coordinate
* on this view window.
*/
public float convertFromScreenXToViewX(float x) {
return x - bounds.x - bounds.width / 2;
}
/**
* Converts an y coordinate on the screen to the corresponding y coordinate
* on this view window.
*/
public float convertFromScreenYToViewY(float y) {
return -y + bounds.y + bounds.height / 2;
}
/**
* Projects the specified vector to the screen.
*/
public void project(Vector3D v) {
// project to view window
v.x = distanceToCamera * v.x / -v.z;
v.y = distanceToCamera * v.y / -v.z;
// convert to screen coordinates
v.x = convertFromViewXToScreenX(v.x);
v.y = convertFromViewYToScreenY(v.y);
}
}
/**
* 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).
*/
class Rectangle3D implements Transformable {
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 Rectange3D.
*/
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);
}
}
/**
* The Polygon3D class represents a polygon as a series of vertices.
*/
class Polygon3D implements Transformable {
// temporary vectors used for calculation
private static Vector3D temp1 = new Vector3D();
private static Vector3D temp2 = new Vector3D();
private Vector3D[] v;
private int numVertices;
private Vector3D normal;
/**
* Creates an empty polygon that can be used as a "scratch" polygon for
* transforms, projections, etc.
*/
public Polygon3D() {
numVertices = 0;
v = new Vector3D[0];
normal = new Vector3D();
}
/**
* Creates a new Polygon3D with the specified vertices.
*/
public Polygon3D(Vector3D v0, Vector3D v1, Vector3D v2) {
this(new Vector3D[] { v0, v1, v2 });
}
/**
* Creates a new Polygon3D with the specified vertices. All the vertices are
* assumed to be in the same plane.
*/
public Polygon3D(Vector3D v0, Vector3D v1, Vector3D v2, Vector3D v3) {
this(new Vector3D[] { v0, v1, v2, v3 });
}
/**
* Creates a new Polygon3D with the specified vertices. All the vertices are
* assumed to be in the same plane.
*/
public Polygon3D(Vector3D[] vertices) {
this.v = vertices;
numVertices = vertices.length;
calcNormal();
}
/**
* Sets this polygon to the same vertices as the specfied polygon.
*/
public void setTo(Polygon3D polygon) {
numVertices = polygon.numVertices;
normal.setTo(polygon.normal);
ensureCapacity(numVertices);
for (int i = 0; i < numVertices; i++) {
v[i].setTo(polygon.v[i]);
}
}
/**
* Ensures this polgon has enough capacity to hold the specified number of
* vertices.
*/
protected void ensureCapacity(int length) {
if (v.length < length) {
Vector3D[] newV = new Vector3D[length];
System.arraycopy(v, 0, newV, 0, v.length);
for (int i = v.length; i < newV.length; i++) {
newV[i] = new Vector3D();
}
v = newV;
}
}
/**
* Gets the number of vertices this polygon has.
*/
public int getNumVertices() {
return numVertices;
}
/**
* Gets the vertex at the specified index.
*/
public Vector3D getVertex(int index) {
return v[index];
}
/**
* Projects this polygon onto the view window.
*/
public void project(ViewWindow view) {
for (int i = 0; i < numVertices; i++) {
view.project(v[i]);
}
}
// methods from the Transformable interface.
public void add(Vector3D u) {
for (int i = 0; i < numVertices; i++) {
v[i].add(u);
}
}
public void subtract(Vector3D u) {
for (int i = 0; i < numVertices; i++) {
v[i].subtract(u);
}
}
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) {
for (int i = 0; i < numVertices; i++) {
v[i].addRotation(xform);
}
normal.addRotation(xform);
}
public void subtractRotation(Transform3D xform) {
for (int i = 0; i < numVertices; i++) {
v[i].subtractRotation(xform);
}
normal.subtractRotation(xform);
}
/**
* Calculates the unit-vector normal of this polygon. This method uses the
* first, second, and third vertices to calcuate the normal, so if these
* vertices are collinear, this method will not work. In this case, you can
* get the normal from the bounding rectangle. Use setNormal() to explicitly
* set the normal. This method uses static objects in the Polygon3D class
* for calculations, so this method is not thread-safe across all instances
* of Polygon3D.
*/
public Vector3D calcNormal() {
if (normal == null) {
normal = new Vector3D();
}
temp1.setTo(v[2]);
temp1.subtract(v[1]);
temp2.setTo(v[0]);
temp2.subtract(v[1]);
normal.setToCrossProduct(temp1, temp2);
normal.normalize();
return normal;
}
/**
* Gets the normal of this polygon. Use calcNormal() if any vertices have
* changed.
*/
public Vector3D getNormal() {
return normal;
}
/**
* Sets the normal of this polygon.
*/
public void setNormal(Vector3D n) {
if (normal == null) {
normal = new Vector3D(n);
} else {
normal.setTo(n);
}
}
/**
* Tests if this polygon is facing the specified location. This method uses
* static objects in the Polygon3D class for calculations, so this method is
* not thread-safe across all instances of Polygon3D.
*/
public boolean isFacing(Vector3D u) {
temp1.setTo(u);
temp1.subtract(v[0]);
return (normal.getDotProduct(temp1) >= 0);
}
/**
* Clips this polygon so that all vertices are in front of the clip plane,
* clipZ (in other words, all vertices have z <= clipZ). The value of clipZ
* should not be 0, as this causes divide-by-zero problems. Returns true if
* the polygon is at least partially in front of the clip plane.
*/
public boolean clip(float clipZ) {
ensureCapacity(numVertices * 3);
boolean isCompletelyHidden = true;
// insert vertices so all edges are either completly
// in front or behind the clip plane
for (int i = 0; i < numVertices; i++) {
int next = (i + 1) % numVertices;
Vector3D v1 = v[i];
Vector3D v2 = v[next];
if (v1.z < clipZ) {
isCompletelyHidden = false;
}
// ensure v1.z < v2.z
if (v1.z > v2.z) {
Vector3D temp = v1;
v1 = v2;
v2 = temp;
}
if (v1.z < clipZ && v2.z > clipZ) {
float scale = (clipZ - v1.z) / (v2.z - v1.z);
insertVertex(next, v1.x + scale * (v2.x - v1.x), v1.y + scale
* (v2.y - v1.y), clipZ);
// skip the vertex we just created
i++;
}
}
if (isCompletelyHidden) {
return false;
}
// delete all vertices that have z > clipZ
for (int i = numVertices - 1; i >= 0; i--) {
if (v[i].z > clipZ) {
deleteVertex(i);
}
}
return (numVertices >= 3);
}
/**
* Inserts a new vertex at the specified index.
*/
protected void insertVertex(int index, float x, float y, float z) {
Vector3D newVertex = v[v.length - 1];
newVertex.x = x;
newVertex.y = y;
newVertex.z = z;
for (int i = v.length - 1; i > index; i--) {
v[i] = v[i - 1];
}
v[index] = newVertex;
numVertices++;
}
/**
* Delete the vertex at the specified index.
*/
protected void deleteVertex(int index) {
Vector3D deleted = v[index];
for (int i = index; i < v.length - 1; i++) {
v[i] = v[i + 1];
}
v[v.length - 1] = deleted;
numVertices--;
}
/**
* Inserts a vertex into this polygon at the specified index. The exact
* vertex in inserted (not a copy).
*/
public void insertVertex(int index, Vector3D vertex) {
Vector3D[] newV = new Vector3D[numVertices + 1];
System.arraycopy(v, 0, newV, 0, index);
newV[index] = vertex;
System.arraycopy(v, index, newV, index + 1, numVertices - index);
v = newV;
numVertices++;
}
/**
* Calculates and returns the smallest bounding rectangle for this polygon.
*/
public Rectangle3D calcBoundingRectangle() {
// the smallest bounding rectangle for a polygon shares
// at least one edge with the polygon. so, this method
// finds the bounding rectangle for every edge in the
// polygon, and returns the smallest one.
Rectangle3D boundingRect = new Rectangle3D();
float minimumArea = Float.MAX_VALUE;
Vector3D u = new Vector3D();
Vector3D v = new Vector3D();
Vector3D d = new Vector3D();
for (int i = 0; i < getNumVertices(); i++) {
u.setTo(getVertex((i + 1) % getNumVertices()));
u.subtract(getVertex(i));
u.normalize();
v.setToCrossProduct(getNormal(), u);
v.normalize();
float uMin = 0;
float uMax = 0;
float vMin = 0;
float vMax = 0;
for (int j = 0; j < getNumVertices(); j++) {
if (j != i) {
d.setTo(getVertex(j));
d.subtract(getVertex(i));
float uLength = d.getDotProduct(u);
float vLength = d.getDotProduct(v);
uMin = Math.min(uLength, uMin);
uMax = Math.max(uLength, uMax);
vMin = Math.min(vLength, vMin);
vMax = Math.max(vLength, vMax);
}
}
// if this calculated area is the smallest, set
// the bounding rectangle
float area = (uMax - uMin) * (vMax - vMin);
if (area < minimumArea) {
minimumArea = area;
Vector3D origin = boundingRect.getOrigin();
origin.setTo(getVertex(i));
d.setTo(u);
d.multiply(uMin);
origin.add(d);
d.setTo(v);
d.multiply(vMin);
origin.add(d);
boundingRect.getDirectionU().setTo(u);
boundingRect.getDirectionV().setTo(v);
boundingRect.setWidth(uMax - uMin);
boundingRect.setHeight(vMax - vMin);
}
}
return boundingRect;
}
}
/**
* The Transform3D class represents a rotation and translation.
*/
class Transform3D {
protected Vector3D location;
private float cosAngleX;
private float sinAngleX;
private float cosAngleY;
private float sinAngleY;
private float cosAngleZ;
private float sinAngleZ;
/**
* Creates a new Transform3D with no translation or rotation.
*/
public Transform3D() {
this(0, 0, 0);
}
/**
* Creates a new Transform3D with the specified translation and no rotation.
*/
public Transform3D(float x, float y, float z) {
location = new Vector3D(x, y, z);
setAngle(0, 0, 0);
}
/**
* Creates a new Transform3D
*/
public Transform3D(Transform3D v) {
location = new Vector3D();
setTo(v);
}
public Object clone() {
return new Transform3D(this);
}
/**
* Sets this Transform3D to the specified Transform3D.
*/
public void setTo(Transform3D v) {
location.setTo(v.location);
this.cosAngleX = v.cosAngleX;
this.sinAngleX = v.sinAngleX;
this.cosAngleY = v.cosAngleY;
this.sinAngleY = v.sinAngleY;
this.cosAngleZ = v.cosAngleZ;
this.sinAngleZ = v.sinAngleZ;
}
/**
* Gets the location (translation) of this transform.
*/
public Vector3D getLocation() {
return location;
}
public float getCosAngleX() {
return cosAngleX;
}
public float getSinAngleX() {
return sinAngleX;
}
public float getCosAngleY() {
return cosAngleY;
}
public float getSinAngleY() {
return sinAngleY;
}
public float getCosAngleZ() {
return cosAngleZ;
}
public float getSinAngleZ() {
return sinAngleZ;
}
public float getAngleX() {
return (float) Math.atan2(sinAngleX, cosAngleX);
}
public float getAngleY() {
return (float) Math.atan2(sinAngleY, cosAngleY);
}
public float getAngleZ() {
return (float) Math.atan2(sinAngleZ, cosAngleZ);
}
public void setAngleX(float angleX) {
cosAngleX = (float) Math.cos(angleX);
sinAngleX = (float) Math.sin(angleX);
}
public void setAngleY(float angleY) {
cosAngleY = (float) Math.cos(angleY);
sinAngleY = (float) Math.sin(angleY);
}
public void setAngleZ(float angleZ) {
cosAngleZ = (float) Math.cos(angleZ);
sinAngleZ = (float) Math.sin(angleZ);
}
public void setAngle(float angleX, float angleY, float angleZ) {
setAngleX(angleX);
setAngleY(angleY);
setAngleZ(angleZ);
}
public void rotateAngleX(float angle) {
if (angle != 0) {
setAngleX(getAngleX() + angle);
}
}
public void rotateAngleY(float angle) {
if (angle != 0) {
setAngleY(getAngleY() + angle);
}
}
public void rotateAngleZ(float angle) {
if (angle != 0) {
setAngleZ(getAngleZ() + angle);
}
}
public void rotateAngle(float angleX, float angleY, float angleZ) {
rotateAngleX(angleX);
rotateAngleY(angleY);
rotateAngleZ(angleZ);
}
}
interface Transformable {
public void add(Vector3D u);
public void subtract(Vector3D u);
public void add(Transform3D xform);
public void subtract(Transform3D xform);
public void addRotation(Transform3D xform);
public void subtractRotation(Transform3D xform);
}
/**
* A MovingTransform3D is a Transform3D that has a location velocity and a
* angular rotation velocity for rotation around the x, y, and z axes.
*/
class MovingTransform3D extends Transform3D {
public static final int FOREVER = -1;
// Vector3D used for calculations
private static Vector3D temp = new Vector3D();
// velocity (units per millisecond)
private Vector3D velocity;
private Movement velocityMovement;
// angular velocity (radians per millisecond)
private Movement velocityAngleX;
private Movement velocityAngleY;
private Movement velocityAngleZ;
/**
* Creates a new MovingTransform3D
*/
public MovingTransform3D() {
init();
}
/**
* Creates a new MovingTransform3D, using the same values as the specified
* Transform3D.
*/
public MovingTransform3D(Transform3D v) {
super(v);
init();
}
protected void init() {
velocity = new Vector3D(0, 0, 0);
velocityMovement = new Movement();
velocityAngleX = new Movement();
velocityAngleY = new Movement();
velocityAngleZ = new Movement();
}
public Object clone() {
return new MovingTransform3D(this);
}
/**
* Updates this Transform3D based on the specified elapsed time. The
* location and angles are updated.
*/
public void update(long elapsedTime) {
float delta = velocityMovement.getDistance(elapsedTime);
if (delta != 0) {
temp.setTo(velocity);
temp.multiply(delta);
location.add(temp);
}
rotateAngle(velocityAngleX.getDistance(elapsedTime), velocityAngleY
.getDistance(elapsedTime), velocityAngleZ
.getDistance(elapsedTime));
}
/**
* Stops this Transform3D. Any moving velocities are set to zero.
*/
public void stop() {
velocity.setTo(0, 0, 0);
velocityMovement.set(0, 0);
velocityAngleX.set(0, 0);
velocityAngleY.set(0, 0);
velocityAngleZ.set(0, 0);
}
/**
* Sets the velocity to move to the following destination at the specified
* speed.
*/
public void moveTo(Vector3D destination, float speed) {
temp.setTo(destination);
temp.subtract(location);
// calc the time needed to move
float distance = temp.length();
long time = (long) (distance / speed);
// normalize the direction vector
temp.divide(distance);
temp.multiply(speed);
setVelocity(temp, time);
}
/**
* Returns true if currently moving.
*/
public boolean isMoving() {
return !velocityMovement.isStopped() && !velocity.equals(0, 0, 0);
}
/**
* Returns true if currently moving, ignoring the y movement.
*/
public boolean isMovingIgnoreY() {
return !velocityMovement.isStopped()
&& (velocity.x != 0 || velocity.z != 0);
}
/**
* Gets the amount of time remaining for this movement.
*/
public long getRemainingMoveTime() {
if (!isMoving()) {
return 0;
} else {
return velocityMovement.remainingTime;
}
}
/**
* Gets the velocity vector. If the velocity vector is modified directly,
* call setVelocity() to ensure the change is recognized.
*/
public Vector3D getVelocity() {
return velocity;
}
/**
* Sets the velocity to the specified vector.
*/
public void setVelocity(Vector3D v) {
setVelocity(v, FOREVER);
}
/**
* Sets the velocity. The velocity is automatically set to zero after the
* specified amount of time has elapsed. If the specified time is FOREVER,
* then the velocity is never automatically set to zero.
*/
public void setVelocity(Vector3D v, long time) {
if (velocity != v) {
velocity.setTo(v);
}
if (v.x == 0 && v.y == 0 && v.z == 0) {
velocityMovement.set(0, 0);
} else {
velocityMovement.set(1, time);
}
}
/**
* Adds the specified velocity to the current velocity. If this
* MovingTransform3D is currently moving, it's time remaining is not
* changed. Otherwise, the time remaining is set to FOREVER.
*/
public void addVelocity(Vector3D v) {
if (isMoving()) {
velocity.add(v);
} else {
setVelocity(v);
}
}
/**
* Turns the x axis to the specified angle with the specified speed.
*/
public void turnXTo(float angleDest, float speed) {
turnTo(velocityAngleX, getAngleX(), angleDest, speed);
}
/**
* Turns the y axis to the specified angle with the specified speed.
*/
public void turnYTo(float angleDest, float speed) {
turnTo(velocityAngleY, getAngleY(), angleDest, speed);
}
/**
* Turns the z axis to the specified angle with the specified speed.
*/
public void turnZTo(float angleDest, float speed) {
turnTo(velocityAngleZ, getAngleZ(), angleDest, speed);
}
/**
* Turns the x axis to face the specified (y,z) vector direction with the
* specified speed.
*/
public void turnXTo(float y, float z, float angleOffset, float speed) {
turnXTo((float) Math.atan2(-z, y) + angleOffset, speed);
}
/**
* Turns the y axis to face the specified (x,z) vector direction with the
* specified speed.
*/
public void turnYTo(float x, float z, float angleOffset, float speed) {
turnYTo((float) Math.atan2(-z, x) + angleOffset, speed);
}
/**
* Turns the z axis to face the specified (x,y) vector direction with the
* specified speed.
*/
public void turnZTo(float x, float y, float angleOffset, float speed) {
turnZTo((float) Math.atan2(y, x) + angleOffset, speed);
}
/**
* Ensures the specified angle is with -pi and pi. Returns the angle,
* corrected if it is not within these bounds.
*/
protected float ensureAngleWithinBounds(float angle) {
if (angle < -Math.PI || angle > Math.PI) {
// transform range to (0 to 1)
double newAngle = (angle + Math.PI) / (2 * Math.PI);
// validate range
newAngle = newAngle - Math.floor(newAngle);
// transform back to (-pi to pi) range
newAngle = Math.PI * (newAngle * 2 - 1);
return (float) newAngle;
}
return angle;
}
/**
* Turns the movement angle from the startAngle to the endAngle with the
* specified speed.
*/
protected void turnTo(Movement movement, float startAngle, float endAngle,
float speed) {
startAngle = ensureAngleWithinBounds(startAngle);
endAngle = ensureAngleWithinBounds(endAngle);
if (startAngle == endAngle) {
movement.set(0, 0);
} else {
float distanceLeft;
float distanceRight;
float pi2 = (float) (2 * Math.PI);
if (startAngle < endAngle) {
distanceLeft = startAngle - endAngle + pi2;
distanceRight = endAngle - startAngle;
} else {
distanceLeft = startAngle - endAngle;
distanceRight = endAngle - startAngle + pi2;
}
if (distanceLeft < distanceRight) {
speed = -Math.abs(speed);
movement.set(speed, (long) (distanceLeft / -speed));
} else {
speed = Math.abs(speed);
movement.set(speed, (long) (distanceRight / speed));
}
}
}
/**
* Sets the angular speed of the x axis.
*/
public void setAngleVelocityX(float speed) {
setAngleVelocityX(speed, FOREVER);
}
/**
* Sets the angular speed of the y axis.
*/
public void setAngleVelocityY(float speed) {
setAngleVelocityY(speed, FOREVER);
}
/**
* Sets the angular speed of the z axis.
*/
public void setAngleVelocityZ(float speed) {
setAngleVelocityZ(speed, FOREVER);
}
/**
* Sets the angular speed of the x axis over the specified time.
*/
public void setAngleVelocityX(float speed, long time) {
velocityAngleX.set(speed, time);
}
/**
* Sets the angular speed of the y axis over the specified time.
*/
public void setAngleVelocityY(float speed, long time) {
velocityAngleY.set(speed, time);
}
/**
* Sets the angular speed of the z axis over the specified time.
*/
public void setAngleVelocityZ(float speed, long time) {
velocityAngleZ.set(speed, time);
}
/**
* Sets the angular speed of the x axis over the specified time.
*/
public float getAngleVelocityX() {
return isTurningX() ? velocityAngleX.speed : 0;
}
/**
* Sets the angular speed of the y axis over the specified time.
*/
public float getAngleVelocityY() {
return isTurningY() ? velocityAngleY.speed : 0;
}
/**
* Sets the angular speed of the z axis over the specified time.
*/
public float getAngleVelocityZ() {
return isTurningZ() ? velocityAngleZ.speed : 0;
}
/**
* Returns true if the x axis is currently turning.
*/
public boolean isTurningX() {
return !velocityAngleX.isStopped();
}
/**
* Returns true if the y axis is currently turning.
*/
public boolean isTurningY() {
return !velocityAngleY.isStopped();
}
/**
* Returns true if the z axis is currently turning.
*/
public boolean isTurningZ() {
return !velocityAngleZ.isStopped();
}
/**
* The Movement class contains a speed and an amount of time to continue
* that speed.
*/
protected static class Movement {
// change per millisecond
float speed;
long remainingTime;
/**
* Sets this movement to the specified speed and time (in milliseconds).
*/
public void set(float speed, long time) {
this.speed = speed;
this.remainingTime = time;
}
public boolean isStopped() {
return (speed == 0) || (remainingTime == 0);
}
/**
* Gets the distance traveled in the specified amount of time in
* milliseconds.
*/
public float getDistance(long elapsedTime) {
if (remainingTime == 0) {
return 0;
} else if (remainingTime != FOREVER) {
elapsedTime = Math.min(elapsedTime, remainingTime);
remainingTime -= elapsedTime;
}
return speed * elapsedTime;
}
}
}
/**
* The PolygonGroup is a group of polygons with a MovingTransform3D.
* PolygonGroups can also contain other PolygonGroups.
*/
class PolygonGroup implements Transformable {
private String name;
private String filename;
private List objects;
private MovingTransform3D transform;
private int iteratorIndex;
/**
* Creates a new, empty PolygonGroup.
*/
public PolygonGroup() {
this("unnamed");
}
/**
* Creates a new, empty PolygonGroup with te specified name.
*/
public PolygonGroup(String name) {
setName(name);
objects = new ArrayList();
transform = new MovingTransform3D();
iteratorIndex = 0;
}
/**
* Gets the MovingTransform3D for this PolygonGroup.
*/
public MovingTransform3D getTransform() {
return transform;
}
/**
* Gets the name of this PolygonGroup.
*/
public String getName() {
return name;
}
/**
* Sets the name of this PolygonGroup.
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets the filename of this PolygonGroup.
*/
public String getFilename() {
return filename;
}
/**
* Sets the filename of this PolygonGroup.
*/
public void setFilename(String filename) {
this.filename = filename;
}
/**
* Adds a polygon to this group.
*/
public void addPolygon(Polygon3D o) {
objects.add(o);
}
/**
* Adds a PolygonGroup to this group.
*/
public void addPolygonGroup(PolygonGroup p) {
objects.add(p);
}
/**
* Clones this polygon group. Polygon3Ds are shared between this group and
* the cloned group; Transform3Ds are copied.
*/
public Object clone() {
PolygonGroup group = new PolygonGroup(name);
group.setFilename(filename);
for (int i = 0; i < objects.size(); i++) {
Object obj = objects.get(i);
if (obj instanceof Polygon3D) {
group.addPolygon((Polygon3D) obj);
} else {
PolygonGroup grp = (PolygonGroup) obj;
group.addPolygonGroup((PolygonGroup) grp.clone());
}
}
group.transform = (MovingTransform3D) transform.clone();
return group;
}
/**
* Gets the PolygonGroup in this group with the specified name, or null if
* none found.
*/
public PolygonGroup getGroup(String name) {
// check for this group
if (this.name != null && this.name.equals(name)) {
return this;
}
for (int i = 0; i < objects.size(); i++) {
Object obj = objects.get(i);
if (obj instanceof PolygonGroup) {
PolygonGroup subgroup = ((PolygonGroup) obj).getGroup(name);
if (subgroup != null) {
return subgroup;
}
}
}
// group not found
return null;
}
/**
* Resets the polygon iterator for this group.
*
* @see #hasNext
* @see #nextPolygon
*/
public void resetIterator() {
iteratorIndex = 0;
for (int i = 0; i < objects.size(); i++) {
Object obj = objects.get(i);
if (obj instanceof PolygonGroup) {
((PolygonGroup) obj).resetIterator();
}
}
}
/**
* Checks if there is another polygon in the current iteration.
*
* @see #resetIterator
* @see #nextPolygon
*/
public boolean hasNext() {
return (iteratorIndex < objects.size());
}
/**
* Gets the next polygon in the current iteration.
*
* @see #resetIterator
* @see #hasNext
*/
public Polygon3D nextPolygon() {
Object obj = objects.get(iteratorIndex);
if (obj instanceof PolygonGroup) {
PolygonGroup group = (PolygonGroup) obj;
Polygon3D poly = group.nextPolygon();
if (!group.hasNext()) {
iteratorIndex++;
}
return poly;
} else {
iteratorIndex++;
return (Polygon3D) obj;
}
}
/**
* Gets the next polygon in the current iteration, applying the
* MovingTransform3Ds to it, and storing it in 'cache'.
*/
public void nextPolygonTransformed(Polygon3D cache) {
Object obj = objects.get(iteratorIndex);
if (obj instanceof PolygonGroup) {
PolygonGroup group = (PolygonGroup) obj;
group.nextPolygonTransformed(cache);
if (!group.hasNext()) {
iteratorIndex++;
}
} else {
iteratorIndex++;
cache.setTo((Polygon3D) obj);
}
cache.add(transform);
}
/**
* Updates the MovingTransform3Ds of this group and any subgroups.
*/
public void update(long elapsedTime) {
transform.update(elapsedTime);
for (int i = 0; i < objects.size(); i++) {
Object obj = objects.get(i);
if (obj instanceof PolygonGroup) {
PolygonGroup group = (PolygonGroup) obj;
group.update(elapsedTime);
}
}
}
// from the Transformable interface
public void add(Vector3D u) {
transform.getLocation().add(u);
}
public void subtract(Vector3D u) {
transform.getLocation().subtract(u);
}
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) {
transform.rotateAngleX(xform.getAngleX());
transform.rotateAngleY(xform.getAngleY());
transform.rotateAngleZ(xform.getAngleZ());
}
public void subtractRotation(Transform3D xform) {
transform.rotateAngleX(-xform.getAngleX());
transform.rotateAngleY(-xform.getAngleY());
transform.rotateAngleZ(-xform.getAngleZ());
}
}
/**
* A BSPTreeTraverseListener is an interface for a BSPTreeTraverser to signal
* visited polygons.
*/
interface BSPTreeTraverseListener {
/**
* Visits a BSP polygon. Called by a BSPTreeTraverer. If this method returns
* true, the BSPTreeTraverer will stop the current traversal. Otherwise, the
* BSPTreeTraverer will continue if there are polygons in the tree that have
* not yet been traversed.
*/
public boolean visitPolygon(BSPPolygon poly, boolean isBackLeaf);
}
/**
* The BSPTreeBuilder class builds a BSP tree from a list of polygons. The
* polygons must be BSPPolygons.
*
* Currently, the builder does not try to optimize the order of the partitions,
* and could be optimized by choosing partitions in an order that minimizes
* polygon splits and provides a more balanced, complete tree.
*/
class BSPTreeBuilder {
/**
* The bsp tree currently being built.
*/
protected BSPTree currentTree;
/**
* Builds a BSP tree.
*/
public BSPTree build(List polygons) {
currentTree = new BSPTree(createNewNode(polygons));
buildNode(currentTree.getRoot());
return currentTree;
}
/**
* Builds a node in the BSP tree.
*/
protected void buildNode(BSPTree.Node node) {
// nothing to build if it's a leaf
if (node instanceof BSPTree.Leaf) {
return;
}
// classify all polygons relative to the partition
// (front, back, or collinear)
ArrayList collinearList = new ArrayList();
ArrayList frontList = new ArrayList();
ArrayList backList = new ArrayList();
List allPolygons = node.polygons;
node.polygons = null;
for (int i = 0; i < allPolygons.size(); i++) {
BSPPolygon poly = (BSPPolygon) allPolygons.get(i);
int side = node.partition.getSide(poly);
if (side == BSPLine.COLLINEAR) {
collinearList.add(poly);
} else if (side == BSPLine.FRONT) {
frontList.add(poly);
} else if (side == BSPLine.BACK) {
backList.add(poly);
} else if (side == BSPLine.SPANNING) {
BSPPolygon front = clipBack(poly, node.partition);
BSPPolygon back = clipFront(poly, node.partition);
if (front != null) {
frontList.add(front);
}
if (back != null) {
backList.add(back);
}
}
}
// clean and assign lists
collinearList.trimToSize();
frontList.trimToSize();
backList.trimToSize();
node.polygons = collinearList;
node.front = createNewNode(frontList);
node.back = createNewNode(backList);
// build front and back nodes
buildNode(node.front);
buildNode(node.back);
if (node.back instanceof BSPTree.Leaf) {
((BSPTree.Leaf) node.back).isBack = true;
}
}
/**
* Creates a new node from a list of polygons. If none of the polygons are
* walls, a leaf is created.
*/
protected BSPTree.Node createNewNode(List polygons) {
BSPLine partition = choosePartition(polygons);
// no partition available, so it's a leaf
if (partition == null) {
BSPTree.Leaf leaf = new BSPTree.Leaf();
leaf.polygons = polygons;
buildLeaf(leaf);
return leaf;
} else {
BSPTree.Node node = new BSPTree.Node();
node.polygons = polygons;
node.partition = partition;
return node;
}
}
/**
* Builds a leaf in the tree, calculating extra information like leaf
* bounds, floor height, and ceiling height.
*/
protected void buildLeaf(BSPTree.Leaf leaf) {
if (leaf.polygons.size() == 0) {
// leaf represents an empty space
leaf.ceilHeight = Float.MAX_VALUE;
leaf.floorHeight = Float.MIN_VALUE;
leaf.bounds = null;
return;
}
float minX = Float.MAX_VALUE;
float maxX = Float.MIN_VALUE;
float minY = Float.MAX_VALUE;
float maxY = Float.MIN_VALUE;
float minZ = Float.MAX_VALUE;
float maxZ = Float.MIN_VALUE;
// find min y, max y, and bounds
Iterator i = leaf.polygons.iterator();
while (i.hasNext()) {
BSPPolygon poly = (BSPPolygon) i.next();
for (int j = 0; j < poly.getNumVertices(); j++) {
Vector3D v = poly.getVertex(j);
minX = Math.min(minX, v.x);
maxX = Math.max(maxX, v.x);
minY = Math.min(minY, v.y);
maxY = Math.max(maxY, v.y);
minZ = Math.min(minZ, v.z);
maxZ = Math.max(maxZ, v.z);
}
}
// find any platform within the leaf
i = leaf.polygons.iterator();
while (i.hasNext()) {
BSPPolygon poly = (BSPPolygon) i.next();
// if a floor
if (poly.getNormal().y == 1) {
float y = poly.getVertex(0).y;
if (y > minY && y < maxY) {
minY = y;
}
}
}
// set the leaf values
leaf.ceilHeight = maxY;
leaf.floorHeight = minY;
leaf.bounds = new Rectangle((int) Math.floor(minX), (int) Math
.floor(minZ), (int) Math.ceil(maxX - minX + 1), (int) Math
.ceil(maxZ - minZ + 1));
}
/**
* Chooses a line from a list of polygons to use as a partition. This method
* just returns the line formed by the first vertical polygon, or null if
* none found. A smarter method would choose a partition that minimizes
* polygon splits and provides a more balanced, complete tree.
*/
protected BSPLine choosePartition(List polygons) {
for (int i = 0; i < polygons.size(); i++) {
BSPPolygon poly = (BSPPolygon) polygons.get(i);
if (poly.isWall()) {
return new BSPLine(poly);
}
}
return null;
}
/**
* Clips away the part of the polygon that lines in front of the specified
* line. The returned polygon is the part of the polygon in back of the
* line. Returns null if the line does not split the polygon. The original
* polygon is untouched.
*/
protected BSPPolygon clipFront(BSPPolygon poly, BSPLine line) {
return clip(poly, line, BSPLine.FRONT);
}
/**
* Clips away the part of the polygon that lines in back of the specified
* line. The returned polygon is the part of the polygon in front of the
* line. Returns null if the line does not split the polygon. The original
* polygon is untouched.
*/
protected BSPPolygon clipBack(BSPPolygon poly, BSPLine line) {
return clip(poly, line, BSPLine.BACK);
}
/**
* Clips a BSPPolygon so that the part of the polygon on the specified side
* (either BSPLine.FRONT or BSPLine.BACK) is removed, and returnes the
* clipped polygon. Returns null if the line does not split the polygon. The
* original polygon is untouched.
*/
protected BSPPolygon clip(BSPPolygon poly, BSPLine line, int clipSide) {
ArrayList vertices = new ArrayList();
BSPLine polyEdge = new BSPLine();
// add vertices that aren't on the clip side
Point2D.Float intersection = new Point2D.Float();
for (int i = 0; i < poly.getNumVertices(); i++) {
int next = (i + 1) % poly.getNumVertices();
Vector3D v1 = poly.getVertex(i);
Vector3D v2 = poly.getVertex(next);
int side1 = line.getSideThin(v1.x, v1.z);
int side2 = line.getSideThin(v2.x, v2.z);
if (side1 != clipSide) {
vertices.add(v1);
}
if ((side1 == BSPLine.FRONT && side2 == BSPLine.BACK)
|| (side2 == BSPLine.FRONT && side1 == BSPLine.BACK)) {
// ensure v1.z < v2.z
if (v1.z > v2.z) {
Vector3D temp = v1;
v1 = v2;
v2 = temp;
}
polyEdge.setLine(v1.x, v1.z, v2.x, v2.z);
float f = polyEdge.getIntersection(line);
Vector3D tPoint = new Vector3D(v1.x + f * (v2.x - v1.x), v1.y
+ f * (v2.y - v1.y), v1.z + f * (v2.z - v1.z));
vertices.add(tPoint);
// remove any created t-junctions
removeTJunctions(v1, v2, tPoint);
}
}
// Remove adjacent equal vertices. (A->A) becomes (A)
for (int i = 0; i < vertices.size(); i++) {
Vector3D v = (Vector3D) vertices.get(i);
Vector3D next = (Vector3D) vertices.get((i + 1) % vertices.size());
if (v.equals(next)) {
vertices.remove(i);
i--;
}
}
if (vertices.size() < 3) {
return null;
}
// make the polygon
Vector3D[] array = new Vector3D[vertices.size()];
vertices.toArray(array);
return poly.clone(array);
}
/**
* Remove any T-Junctions from the current tree along the line specified by
* (v1, v2). Find all polygons with this edge and insert the T-intersection
* point between them.
*/
protected void removeTJunctions(final Vector3D v1, final Vector3D v2,
final Vector3D tPoint) {
BSPTreeTraverser traverser = new BSPTreeTraverser(
new BSPTreeTraverseListener() {
public boolean visitPolygon(BSPPolygon poly,
boolean isBackLeaf) {
removeTJunctions(poly, v1, v2, tPoint);
return true;
}
});
traverser.traverse(currentTree);
}
/**
* Remove any T-Junctions from the specified polygon. The T-intersection
* point is inserted between the points v1 and v2 if there are no other
* points between them.
*/
protected void removeTJunctions(BSPPolygon poly, Vector3D v1, Vector3D v2,
Vector3D tPoint) {
for (int i = 0; i < poly.getNumVertices(); i++) {
int next = (i + 1) % poly.getNumVertices();
Vector3D p1 = poly.getVertex(i);
Vector3D p2 = poly.getVertex(next);
if ((p1.equals(v1) && p2.equals(v2))
|| (p1.equals(v2) && p2.equals(v1))) {
poly.insertVertex(next, tPoint);
return;
}
}
}
}
/**
* The TexturedPolygon3D class is a Polygon with a texture.
*/
class TexturedPolygon3D extends Polygon3D {
protected Rectangle3D textureBounds;
protected Texture texture;
public TexturedPolygon3D() {
textureBounds = new Rectangle3D();
}
public TexturedPolygon3D(Vector3D v0, Vector3D v1, Vector3D v2) {
this(new Vector3D[] { v0, v1, v2 });
}
public TexturedPolygon3D(Vector3D v0, Vector3D v1, Vector3D v2, Vector3D v3) {
this(new Vector3D[] { v0, v1, v2, v3 });
}
public TexturedPolygon3D(Vector3D[] vertices) {
super(vertices);
textureBounds = new Rectangle3D();
}
public void setTo(Polygon3D poly) {
super.setTo(poly);
if (poly instanceof TexturedPolygon3D) {
TexturedPolygon3D tPoly = (TexturedPolygon3D) poly;
textureBounds.setTo(tPoly.textureBounds);
texture = tPoly.texture;
}
}
/**
* Gets this polygon's texture.
*/
public Texture getTexture() {
return texture;
}
/**
* Gets this polygon's texture bounds.
*/
public Rectangle3D getTextureBounds() {
return textureBounds;
}
/**
* Sets this polygon's texture.
*/
public void setTexture(Texture texture) {
this.texture = texture;
textureBounds.setWidth(texture.getWidth());
textureBounds.setHeight(texture.getHeight());
}
/**
* Sets this polygon's texture and texture bounds.
*/
public void setTexture(Texture texture, Rectangle3D bounds) {
setTexture(texture);
textureBounds.setTo(bounds);
}
public void add(Vector3D u) {
super.add(u);
textureBounds.add(u);
}
public void subtract(Vector3D u) {
super.subtract(u);
textureBounds.subtract(u);
}
public void addRotation(Transform3D xform) {
super.addRotation(xform);
textureBounds.addRotation(xform);
}
public void subtractRotation(Transform3D xform) {
super.subtractRotation(xform);
textureBounds.subtractRotation(xform);
}
/**
* Calculates the bounding rectangle for this polygon that is aligned with
* the texture bounds.
*/
public Rectangle3D calcBoundingRectangle() {
Vector3D u = new Vector3D(textureBounds.getDirectionU());
Vector3D v = new Vector3D(textureBounds.getDirectionV());
Vector3D d = new Vector3D();
u.normalize();
v.normalize();
float uMin = 0;
float uMax = 0;
float vMin = 0;
float vMax = 0;
for (int i = 0; i < getNumVertices(); i++) {
d.setTo(getVertex(i));
d.subtract(getVertex(0));
float uLength = d.getDotProduct(u);
float vLength = d.getDotProduct(v);
uMin = Math.min(uLength, uMin);
uMax = Math.max(uLength, uMax);
vMin = Math.min(vLength, vMin);
vMax = Math.max(vLength, vMax);
}
Rectangle3D boundingRect = new Rectangle3D();
Vector3D origin = boundingRect.getOrigin();
origin.setTo(getVertex(0));
d.setTo(u);
d.multiply(uMin);
origin.add(d);
d.setTo(v);
d.multiply(vMin);
origin.add(d);
boundingRect.getDirectionU().setTo(u);
boundingRect.getDirectionV().setTo(v);
boundingRect.setWidth(uMax - uMin);
boundingRect.setHeight(vMax - vMin);
// explictly set the normal since the texture directions
// could create a normal negative to the polygon normal
boundingRect.setNormal(getNormal());
return boundingRect;
}
}
/**
* A BSPPolygon is a TexturedPolygon3D with a type (TYPE_FLOOR, TYPE_WALL, or
* TYPE_PASSABLE_WALL) an ambient light intensity value, and a BSPLine
* representation if the type is a TYPE_WALL or TYPE_PASSABLE_WALL.
*/
class BSPPolygon extends TexturedPolygon3D {
public static final int TYPE_FLOOR = 0;
public static final int TYPE_WALL = 1;
public static final int TYPE_PASSABLE_WALL = 2;
/**
* How short a wall must be so that monsters/players can step over it.
*/
public static final int PASSABLE_WALL_THRESHOLD = 32;
/**
* How tall an entryway must be so that monsters/players can pass through it
*/
public static final int PASSABLE_ENTRYWAY_THRESHOLD = 128;
private int type;
private float ambientLightIntensity;
private BSPLine line;
/**
* Creates a new BSPPolygon with the specified vertices and type
* (TYPE_FLOOR, TYPE_WALL, or TYPE_PASSABLE_WALL).
*/
public BSPPolygon(Vector3D[] vertices, int type) {
super(vertices);
this.type = type;
ambientLightIntensity = 0.5f;
if (isWall()) {
line = new BSPLine(this);
}
}
/**
* Clone this polygon, but with a different set of vertices.
*/
public BSPPolygon clone(Vector3D[] vertices) {
BSPPolygon clone = new BSPPolygon(vertices, type);
clone.setNormal(getNormal());
clone.setAmbientLightIntensity(getAmbientLightIntensity());
if (getTexture() != null) {
clone.setTexture(getTexture(), getTextureBounds());
}
return clone;
}
/**
* Returns true if the BSPPolygon is a wall.
*/
public boolean isWall() {
return (type == TYPE_WALL) || (type == TYPE_PASSABLE_WALL);
}
/**
* Returns true if the BSPPolygon is a solid wall (not passable).
*/
public boolean isSolidWall() {
return type == TYPE_WALL;
}
/**
* Gets the line representing the BSPPolygon. Returns null if this
* BSPPolygon is not a wall.
*/
public BSPLine getLine() {
return line;
}
public void setAmbientLightIntensity(float a) {
ambientLightIntensity = a;
}
public float getAmbientLightIntensity() {
return ambientLightIntensity;
}
}
/**
* The RoomDef class represents a convex room with walls, a floor, and a
* ceiling. The floor may be above the ceiling, in which case the RoomDef is a
* "pillar" or "block" structure, rather than a "room". RoomDefs are used as a
* shortcut to create the actual BSPPolygons used in the 2D BSP tree.
*/
class RoomDef {
private static final Vector3D FLOOR_NORMAL = new Vector3D(0, 1, 0);
private static final Vector3D CEIL_NORMAL = new Vector3D(0, -1, 0);
private HorizontalAreaDef floor;
private HorizontalAreaDef ceil;
private List vertices;
private float ambientLightIntensity;
/**
* The HorizontalAreaDef class represents a floor or ceiling.
*/
private static class HorizontalAreaDef {
float height;
Texture texture;
Rectangle3D textureBounds;
public HorizontalAreaDef(float height, Texture texture,
Rectangle3D textureBounds) {
this.height = height;
this.texture = texture;
this.textureBounds = textureBounds;
}
}
/**
* The Vertex class represents a Wall vertex.
*/
private static class Vertex {
float x;
float z;
float bottom;
float top;
Texture texture;
Rectangle3D textureBounds;
public Vertex(float x, float z, float bottom, float top,
Texture texture, Rectangle3D textureBounds) {
this.x = x;
this.z = z;
this.bottom = bottom;
this.top = top;
this.texture = texture;
this.textureBounds = textureBounds;
}
public boolean isWall() {
return (bottom != top) && (texture != null);
}
}
/**
* Creates a new RoomDef with an ambient light intensity of 0.5. The walls,
* floors and ceiling all use this ambient light intensity.
*/
public RoomDef() {
this(0.5f);
}
/**
* Creates a new RoomDef with the specified ambient light intensity. The
* walls, floors and ceiling all use this ambient light intensity.
*/
public RoomDef(float ambientLightIntensity) {
this.ambientLightIntensity = ambientLightIntensity;
vertices = new ArrayList();
}
/**
* Adds a new wall vertex at the specified (x,z) location, with the
* specified texture. The wall stretches from the floor to the ceiling. If
* the texture is null, no polygon for the wall is created.
*/
public void addVertex(float x, float z, Texture texture) {
addVertex(x, z, Math.min(floor.height, ceil.height), Math.max(
floor.height, ceil.height), texture);
}
/**
* Adds a new wall vertex at the specified (x,z) location, with the
* specified texture, bottom location, and top location. If the texture is
* null, no polygon for the wall is created.
*/
public void addVertex(float x, float z, float bottom, float top,
Texture texture) {
vertices.add(new Vertex(x, z, bottom, top, texture, null));
}
/**
* Adds a new wall vertex at the specified (x,z) location, with the
* specified texture, texture bounds, bottom location, and top location. If
* the texture is null, no polygon for the wall is created.
*/
public void addVertex(float x, float z, float bottom, float top,
Texture texture, Rectangle3D texBounds) {
vertices.add(new Vertex(x, z, bottom, top, texture, texBounds));
}
/**
* Sets the floor height and floor texture of this room. If the texture is
* null, no floor polygon is created, but the height of the floor is used as
* the default bottom wall boundary.
*/
public void setFloor(float height, Texture texture) {
setFloor(height, texture, null);
}
/**
* Sets the floor height, floor texture, and floor texture bounds of this
* room. If the texture is null, no floor polygon is created, but the height
* of the floor is used as the default bottom wall boundary. If the texture
* bounds is null, a default texture bounds is used.
*/
public void setFloor(float height, Texture texture, Rectangle3D texBounds) {
if (texture != null && texBounds == null) {
texBounds = new Rectangle3D(new Vector3D(0, height, 0),
new Vector3D(1, 0, 0), new Vector3D(0, 0, -1), texture
.getWidth(), texture.getHeight());
}
floor = new HorizontalAreaDef(height, texture, texBounds);
}
/**
* Sets the ceiling height and ceiling texture of this room. If the texture
* is null, no ceiling polygon is created, but the height of the ceiling is
* used as the default top wall boundary.
*/
public void setCeil(float height, Texture texture) {
setCeil(height, texture, null);
}
/**
* Sets the ceiling height, ceiling texture, and ceiling texture bounds of
* this room. If the texture is null, no floor polygon is created, but the
* height of the floor is used as the default bottom wall boundary. If the
* texture bounds is null, a default texture bounds is used.
*/
public void setCeil(float height, Texture texture, Rectangle3D texBounds) {
if (texture != null && texBounds == null) {
texBounds = new Rectangle3D(new Vector3D(0, height, 0),
new Vector3D(1, 0, 0), new Vector3D(0, 0, 1), texture
.getWidth(), texture.getHeight());
}
ceil = new HorizontalAreaDef(height, texture, texBounds);
}
/**
* Creates and returns a list of BSPPolygons that represent the walls,
* floor, and ceiling of this room.
*/
public List createPolygons() {
List walls = createVerticalPolygons();
List floors = createHorizontalPolygons();
List list = new ArrayList(walls.size() + floors.size());
list.addAll(walls);
list.addAll(floors);
return list;
}
/**
* Creates and returns a list of BSPPolygons that represent the vertical
* walls of this room.
*/
public List createVerticalPolygons() {
int size = vertices.size();
List list = new ArrayList(size);
if (size == 0) {
return list;
}
Vertex origin = (Vertex) vertices.get(0);
Vector3D textureOrigin = new Vector3D(origin.x, ceil.height, origin.z);
Vector3D textureDy = new Vector3D(0, -1, 0);
for (int i = 0; i < size; i++) {
Vertex curr = (Vertex) vertices.get(i);
if (!curr.isWall()) {
continue;
}
// determine if wall is passable (useful for portals)
int type = BSPPolygon.TYPE_WALL;
if (floor.height > ceil.height) {
if (floor.height - ceil.height <= BSPPolygon.PASSABLE_WALL_THRESHOLD) {
type = BSPPolygon.TYPE_PASSABLE_WALL;
}
} else if (curr.top - curr.bottom <= BSPPolygon.PASSABLE_WALL_THRESHOLD) {
type = BSPPolygon.TYPE_PASSABLE_WALL;
} else if (curr.bottom - floor.height >= BSPPolygon.PASSABLE_ENTRYWAY_THRESHOLD) {
type = BSPPolygon.TYPE_PASSABLE_WALL;
}
List wallVertices = new ArrayList();
Vertex prev;
Vertex next;
if (floor.height < ceil.height) {
prev = (Vertex) vertices.get((i + size - 1) % size);
next = (Vertex) vertices.get((i + 1) % size);
} else
|