|
/*
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;
public class CollisionTest extends ShooterCore {
public static void main(String[] args) {
new CollisionTest(args).run();
}
protected BSPTree bspTree;
protected String mapFile;
public CollisionTest(String[] args) {
super(args);
for (int i = 0; mapFile == null && i < args.length; i++) {
if (mapFile == null && !args[i].startsWith("-")) {
mapFile = args[i];
}
}
if (mapFile == null) {
mapFile = "../images/sample.map";
}
}
public void createPolygons() {
Graphics2D g = screen.getGraphics();
g.setColor(Color.BLACK);
g.fillRect(0, 0, screen.getWidth(), screen.getHeight());
g.setColor(Color.WHITE);
g.drawString("Loading...", 5, screen.getHeight() - 5);
screen.update();
float ambientLightIntensity = .2f;
List lights = new LinkedList();
lights.add(new PointLight3D(-100, 100, 100, .3f, -1));
lights.add(new PointLight3D(100, 100, 0, .3f, -1));
MapLoader loader = new MapLoader();
loader.setObjectLights(lights, ambientLightIntensity);
try {
bspTree = loader.loadMap(mapFile);
} catch (IOException ex) {
ex.printStackTrace();
}
CollisionDetection collisionDetection = new CollisionDetection(bspTree);
gameObjectManager = new GridGameObjectManager(bspTree.calcBounds(),
collisionDetection);
gameObjectManager.addPlayer(new GameObject(new PolygonGroup("Player")));
// set up player bounds
PolygonGroupBounds playerBounds = gameObjectManager.getPlayer()
.getBounds();
playerBounds.setTopHeight(Player.DEFAULT_PLAYER_HEIGHT);
playerBounds.setRadius(Player.DEFAULT_PLAYER_RADIUS);
((BSPRenderer) polygonRenderer).setGameObjectManager(gameObjectManager);
createGameObjects(loader.getObjectsInMap());
Transform3D start = loader.getPlayerStartLocation();
gameObjectManager.getPlayer().getTransform().setTo(start);
}
private void createGameObjects(List mapObjects) {
Iterator i = mapObjects.iterator();
while (i.hasNext()) {
PolygonGroup group = (PolygonGroup) i.next();
String filename = group.getFilename();
if ("robot.obj".equals(filename)) {
gameObjectManager.add(new Bot(group));
} else {
// static object
gameObjectManager.add(new GameObject(group));
}
}
}
public void drawPolygons(Graphics2D g) {
polygonRenderer.startFrame(g);
// draw polygons in bsp tree (set z buffer)
((BSPRenderer) polygonRenderer).draw(g, bspTree);
// draw game object polygons (check and set z buffer)
gameObjectManager.draw(g, (GameObjectRenderer) polygonRenderer);
polygonRenderer.endFrame(g);
}
}
/**
* A GameObject that can jump.
*/
class JumpingGameObject extends GameObject {
public static final float DEFAULT_JUMP_HEIGHT = 64;
protected float jumpVelocity;
public JumpingGameObject(PolygonGroup group) {
super(group);
setJumpHeight(DEFAULT_JUMP_HEIGHT);
}
/**
* Sets how high this GameObject can jump.
*/
public void setJumpHeight(float jumpHeight) {
jumpVelocity = Physics.getInstance().getJumpVelocity(jumpHeight);
}
/**
* Causes this GameObject to jump if the jumping flag is set and this object
* is not already jumping.
*/
public void setJumping(boolean isJumping) {
if (isJumping() != isJumping) {
super.setJumping(isJumping);
if (isJumping) {
Physics.getInstance().jump(this, jumpVelocity);
}
}
}
public void notifyFloorCollision() {
// the object has landed.
setJumping(false);
}
}
/**
* The Physics class is a singleton that represents various attributes (like
* gravity) and the functions to manipulate objects based on those physical
* attributes. Currently, only gravity and scoot-up (acceleration when traveling
* up stairs) are supported.
*/
class Physics {
/**
* Default gravity in units per millisecond squared
*/
public static final float DEFAULT_GRAVITY_ACCEL = -.002f;
/**
* Default scoot-up (acceleration traveling up stairs) in units per
* millisecond squared.
*/
public static final float DEFAULT_SCOOT_ACCEL = .006f;
private static Physics instance;
private float gravityAccel;
private float scootAccel;
private Vector3D velocity = new Vector3D();
/**
* Gets the Physics instance. If a Physics instance does not yet exist, one
* is created with the default attributes.
*/
public static synchronized Physics getInstance() {
if (instance == null) {
instance = new Physics();
}
return instance;
}
protected Physics() {
gravityAccel = DEFAULT_GRAVITY_ACCEL;
scootAccel = DEFAULT_SCOOT_ACCEL;
}
/**
* Gets the gravity acceleration in units per millisecond squared.
*/
public float getGravityAccel() {
return gravityAccel;
}
/**
* Sets the gravity acceleration in units per millisecond squared.
*/
public void setGravityAccel(float gravityAccel) {
this.gravityAccel = gravityAccel;
}
/**
* Gets the scoot-up acceleration in units per millisecond squared. The
* scoot up acceleration can be used for smoothly traveling up stairs.
*/
public float getScootAccel() {
return scootAccel;
}
/**
* Sets the scoot-up acceleration in units per millisecond squared. The
* scoot up acceleration can be used for smoothly traveling up stairs.
*/
public void setScootAccel(float scootAccel) {
this.scootAccel = scootAccel;
}
/**
* Applies gravity to the specified GameObject according to the amount of
* time that has passed.
*/
public void applyGravity(GameObject object, long elapsedTime) {
velocity.setTo(0, gravityAccel * elapsedTime, 0);
object.getTransform().addVelocity(velocity);
}
/**
* Applies the scoot-up acceleration to the specified GameObject according
* to the amount of time that has passed.
*/
public void scootUp(GameObject object, long elapsedTime) {
velocity.setTo(0, scootAccel * elapsedTime, 0);
object.getTransform().addVelocity(velocity);
}
/**
* Applies the negative scoot-up acceleration to the specified GameObject
* according to the amount of time that has passed.
*/
public void scootDown(GameObject object, long elapsedTime) {
velocity.setTo(0, -scootAccel * elapsedTime, 0);
object.getTransform().addVelocity(velocity);
}
/**
* Sets the specified GameObject's vertical velocity to jump to the
* specified height. Calls getJumpVelocity() to calculate the velocity,
* which uses the Math.sqrt() function.
*/
public void jumpToHeight(GameObject object, float jumpHeight) {
jump(object, getJumpVelocity(jumpHeight));
}
/**
* Sets the specified GameObject's vertical velocity to the specified jump
* velocity.
*/
public void jump(GameObject object, float jumpVelocity) {
velocity.setTo(0, jumpVelocity, 0);
object.getTransform().getVelocity().y = 0;
object.getTransform().addVelocity(velocity);
}
/**
* Returns the vertical velocity needed to jump the specified height (based
* on current gravity). Uses the Math.sqrt() function.
*/
public float getJumpVelocity(float jumpHeight) {
// use velocity/acceleration formal: v*v = -2 * a(y-y0)
// (v is jump velocity, a is accel, y-y0 is max height)
return (float) Math.sqrt(-2 * gravityAccel * jumpHeight);
}
}
/**
* A Player object.
*/
class Player extends JumpingGameObject {
public static final float DEFAULT_PLAYER_RADIUS = 32;
public static final float DEFAULT_PLAYER_HEIGHT = 128;
public Player() {
this(new PolygonGroup("Player"));
// set up player bounds
PolygonGroupBounds playerBounds = getBounds();
playerBounds.setTopHeight(DEFAULT_PLAYER_HEIGHT);
playerBounds.setRadius(DEFAULT_PLAYER_RADIUS);
}
public Player(PolygonGroup group) {
super(group);
}
}
/**
* The GridGameObjectManager is a GameObjectManager that integrally arranges
* GameObjects on a 2D grid for visibility determination and to limit the number
* of tests for collision detection.
*/
class GridGameObjectManager implements GameObjectManager {
/**
* Default grid size of 512. The grid size should be larger than the largest
* object's diameter.
*/
private static final int GRID_SIZE_BITS = 9;
private static final int GRID_SIZE = 1 << GRID_SIZE_BITS;
/**
* The Cell class represents a cell in the grid. It contains a list of game
* objects and a visible flag.
*/
private static class Cell {
List objects;
boolean visible;
Cell() {
objects = new ArrayList();
visible = false;
}
}
private Cell[] grid;
private Rectangle mapBounds;
private int gridWidth;
private int gridHeight;
private List allObjects;
private GameObject player;
private Vector3D oldLocation;
private CollisionDetection collisionDetection;
/**
* Creates a new GridGameObjectManager with the specified map bounds and
* collision detection handler. GameObjects outside the map bounds will
* never be shown.
*/
public GridGameObjectManager(Rectangle mapBounds,
CollisionDetection collisionDetection) {
this.mapBounds = mapBounds;
this.collisionDetection = collisionDetection;
gridWidth = (mapBounds.width >> GRID_SIZE_BITS) + 1;
gridHeight = (mapBounds.height >> GRID_SIZE_BITS) + 1;
grid = new Cell[gridWidth * gridHeight];
for (int i = 0; i < grid.length; i++) {
grid[i] = new Cell();
}
allObjects = new ArrayList();
oldLocation = new Vector3D();
}
/**
* Converts a map x-coordinate to a grid x-coordinate.
*/
private int convertMapXtoGridX(int x) {
return (x - mapBounds.x) >> GRID_SIZE_BITS;
}
/**
* Converts a map y-coordinate to a grid y-coordinate.
*/
private int convertMapYtoGridY(int y) {
return (y - mapBounds.y) >> GRID_SIZE_BITS;
}
/**
* Marks all objects as potentially visible (should be drawn).
*/
public void markAllVisible() {
for (int i = 0; i < grid.length; i++) {
grid[i].visible = true;
}
}
/**
* Marks all objects within the specified 2D bounds as potentially visible
* (should be drawn).
*/
public void markVisible(Rectangle bounds) {
int x1 = Math.max(0, convertMapXtoGridX(bounds.x));
int y1 = Math.max(0, convertMapYtoGridY(bounds.y));
int x2 = Math.min(gridWidth - 1, convertMapXtoGridX(bounds.x
+ bounds.width));
int y2 = Math.min(gridHeight - 1, convertMapYtoGridY(bounds.y
+ bounds.height));
for (int y = y1; y <= y2; y++) {
int offset = y * gridWidth;
for (int x = x1; x <= x2; x++) {
grid[offset + x].visible = true;
}
}
}
/**
* Adds a GameObject to this manager.
*/
public void add(GameObject object) {
if (object != null) {
if (object == player) {
// ensure player always moves first
allObjects.add(0, object);
} else {
allObjects.add(object);
}
Cell cell = getCell(object);
if (cell != null) {
cell.objects.add(object);
}
}
}
/**
* Removes a GameObject from this manager.
*/
public void remove(GameObject object) {
if (object != null) {
allObjects.remove(object);
Cell cell = getCell(object);
if (cell != null) {
cell.objects.remove(object);
}
}
}
/**
* Adds a GameObject to this manager, specifying it as the player object. An
* existing player object, if any, is not removed.
*/
public void addPlayer(GameObject player) {
this.player = player;
if (player != null) {
player.notifyVisible(true);
add(player);
}
}
/**
* Gets the object specified as the Player object, or null if no player
* object was specified.
*/
public GameObject getPlayer() {
return player;
}
/**
* Gets the cell the specified GameObject is in, or null if the GameObject
* is not within the map bounds.
*/
private Cell getCell(GameObject object) {
int x = convertMapXtoGridX((int) object.getX());
int y = convertMapYtoGridY((int) object.getZ());
return getCell(x, y);
}
/**
* Gets the cell of the specified grid location, or null if the grid
* location is invalid.
*/
private Cell getCell(int x, int y) {
// check bounds
if (x < 0 || y < 0 || x >= gridWidth || y >= gridHeight) {
return null;
}
// get the cell at the x,y location
return grid[x + y * gridWidth];
}
/**
* Updates all objects based on the amount of time passed from the last
* update and applied collision detection.
*/
public void update(long elapsedTime) {
for (int i = 0; i < allObjects.size(); i++) {
GameObject object = (GameObject) allObjects.get(i);
// save the object's old position
Cell oldCell = getCell(object);
oldLocation.setTo(object.getLocation());
// move the object
object.update(player, elapsedTime);
// remove the object if destroyed
if (object.isDestroyed()) {
allObjects.remove(i);
i--;
if (oldCell != null) {
oldCell.objects.remove(object);
}
continue;
}
// if the object moved, do collision detection
if (!object.getLocation().equals(oldLocation)) {
// check walls, floors, and ceilings
collisionDetection.checkBSP(object, oldLocation, elapsedTime);
// check other objects
if (checkObjectCollision(object, oldLocation)) {
// revert to old position
object.getLocation().setTo(oldLocation);
}
// update grid location
Cell cell = getCell(object);
if (cell != oldCell) {
if (oldCell != null) {
oldCell.objects.remove(object);
}
if (cell != null) {
cell.objects.add(object);
}
}
}
}
}
/**
* Checks to see if the specified object collides with any other object.
*/
public boolean checkObjectCollision(GameObject object, Vector3D oldLocation) {
boolean collision = false;
// use the object's (x,z) position (ground plane)
int x = convertMapXtoGridX((int) object.getX());
int y = convertMapYtoGridY((int) object.getZ());
// check the object's surrounding 9 cells
for (int i = x - 1; i <= x + 1; i++) {
for (int j = y - 1; j <= y + 1; j++) {
Cell cell = getCell(i, j);
if (cell != null) {
collision |= collisionDetection.checkObject(object,
cell.objects, oldLocation);
}
}
}
return collision;
}
/**
* Draws all visible objects and marks all objects as not visible.
*/
public void draw(Graphics2D g, GameObjectRenderer r) {
for (int i = 0; i < grid.length; i++) {
List objects = grid[i].objects;
for (int j = 0; j < objects.size(); j++) {
GameObject object = (GameObject) objects.get(j);
boolean visible = false;
if (grid[i].visible) {
visible = r.draw(g, object);
}
if (object != player) {
// notify objects if they are visible
object.notifyVisible(visible);
}
}
grid[i].visible = false;
}
}
}
/**
* The CollisionDetection class handles collision detection between the
* GameObjects, and between GameObjects and a BSP tree. When a collision occurs,
* the GameObject stops.
*/
class CollisionDetection {
/**
* Bounding game object corners used to test for intersection with the BSP
* tree. Corners are in either clockwise or counter-clockwise order.
*/
private static final Point2D.Float[] CORNERS = { new Point2D.Float(-1, -1),
new Point2D.Float(-1, 1), new Point2D.Float(1, 1),
new Point2D.Float(1, -1), };
private BSPTree bspTree;
private BSPLine path;
private Point2D.Float intersection;
/**
* Creates a new CollisionDetection object for the specified BSP tree.
*/
public CollisionDetection(BSPTree bspTree) {
this.bspTree = bspTree;
path = new BSPLine();
intersection = new Point2D.Float();
}
/**
* Checks a GameObject against the BSP tree. Returns true if a wall
* collision occurred.
*/
public boolean checkBSP(GameObject object, Vector3D oldLocation,
long elapsedTime) {
boolean wallCollision = false;
// check walls if x or z position changed
if (object.getX() != oldLocation.x || object.getZ() != oldLocation.z) {
wallCollision = (checkWalls(object, oldLocation, elapsedTime) != null);
}
getFloorAndCeiling(object);
checkFloorAndCeiling(object, elapsedTime);
return wallCollision;
}
/**
* Gets the floor and ceiling values for the specified GameObject. Calls
* object.setFloorHeight() and object.setCeilHeight() to set the floor and
* ceiling values.
*/
public void getFloorAndCeiling(GameObject object) {
float x = object.getX();
float z = object.getZ();
float r = object.getBounds().getRadius() - 1;
float floorHeight = Float.MIN_VALUE;
float ceilHeight = Float.MAX_VALUE;
BSPTree.Leaf leaf = bspTree.getLeaf(x, z);
if (leaf != null) {
floorHeight = leaf.floorHeight;
ceilHeight = leaf.ceilHeight;
}
// check surrounding four points
for (int i = 0; i < CORNERS.length; i++) {
float xOffset = r * CORNERS[i].x;
float zOffset = r * CORNERS[i].y;
leaf = bspTree.getLeaf(x + xOffset, z + zOffset);
if (leaf != null) {
floorHeight = Math.max(floorHeight, leaf.floorHeight);
ceilHeight = Math.min(ceilHeight, leaf.ceilHeight);
}
}
object.setFloorHeight(floorHeight);
object.setCeilHeight(ceilHeight);
}
/**
* Checks for object collisions with the floor and ceiling. Uses
* object.getFloorHeight() and object.getCeilHeight() for the floor and
* ceiling values.
*/
protected void checkFloorAndCeiling(GameObject object, long elapsedTime) {
boolean collision = false;
float floorHeight = object.getFloorHeight();
float ceilHeight = object.getCeilHeight();
float bottomHeight = object.getBounds().getBottomHeight();
float topHeight = object.getBounds().getTopHeight();
if (!object.isFlying()) {
object.getLocation().y = floorHeight - bottomHeight;
}
// check if below floor
if (object.getY() + bottomHeight < floorHeight) {
object.notifyFloorCollision();
object.getTransform().getVelocity().y = 0;
object.getLocation().y = floorHeight - bottomHeight;
}
// check if hitting ceiling
else if (object.getY() + topHeight > ceilHeight) {
object.notifyCeilingCollision();
object.getTransform().getVelocity().y = 0;
object.getLocation().y = ceilHeight - topHeight;
}
}
/**
* Checks for a game object collision with the walls of the BSP tree.
* Returns the first wall collided with, or null if there was no collision.
*/
public BSPPolygon checkWalls(GameObject object, Vector3D oldLocation,
long elapsedTime) {
Vector3D v = object.getTransform().getVelocity();
PolygonGroupBounds bounds = object.getBounds();
float x = object.getX();
float y = object.getY();
float z = object.getZ();
float r = bounds.getRadius();
float stepSize = 0;
if (!object.isFlying()) {
stepSize = BSPPolygon.PASSABLE_WALL_THRESHOLD;
}
float bottom = object.getY() + bounds.getBottomHeight() + stepSize;
float top = object.getY() + bounds.getTopHeight();
// pick closest intersection of 4 corners
BSPPolygon closestWall = null;
float closestDistSq = Float.MAX_VALUE;
for (int i = 0; i < CORNERS.length; i++) {
float xOffset = r * CORNERS[i].x;
float zOffset = r * CORNERS[i].y;
BSPPolygon wall = getFirstWallIntersection(oldLocation.x + xOffset,
oldLocation.z + zOffset, x + xOffset, z + zOffset, bottom,
top);
if (wall != null) {
float x2 = intersection.x - xOffset;
float z2 = intersection.y - zOffset;
float dx = (x2 - oldLocation.x);
float dz = (z2 - oldLocation.z);
float distSq = dx * dx + dz * dz;
// pick the wall with the closest distance, or
// if the distances are equal, pick the current
// wall if the offset has the same sign as the
// velocity.
if (distSq < closestDistSq
|| (distSq == closestDistSq
&& MoreMath.sign(xOffset) == MoreMath.sign(v.x) && MoreMath
.sign(zOffset) == MoreMath.sign(v.z))) {
closestWall = wall;
closestDistSq = distSq;
object.getLocation().setTo(x2, y, z2);
}
}
}
if (closestWall != null) {
object.notifyWallCollision();
}
// make sure the player bounds is empty
// (avoid colliding with sharp corners)
x = object.getX();
z = object.getZ();
r -= 1;
for (int i = 0; i < CORNERS.length; i++) {
int next = i + 1;
if (next == CORNERS.length) {
next = 0;
}
// use (r-1) so this doesn't interfere with normal
// collisions
float xOffset1 = r * CORNERS[i].x;
float zOffset1 = r * CORNERS[i].y;
float xOffset2 = r * CORNERS[next].x;
float zOffset2 = r * CORNERS[next].y;
BSPPolygon wall = getFirstWallIntersection(x + xOffset1, z
+ zOffset1, x + xOffset2, z + zOffset2, bottom, top);
if (wall != null) {
object.notifyWallCollision();
object.getLocation().setTo(oldLocation.x, object.getY(),
oldLocation.z);
return wall;
}
}
return closestWall;
}
/**
* Gets the first intersection, if any, of the path (x1,z1)-> (x2,z2) with
* the walls of the BSP tree. Returns the first BSPPolygon intersection, or
* null if no intersection occurred.
*/
public BSPPolygon getFirstWallIntersection(float x1, float z1, float x2,
float z2, float yBottom, float yTop) {
return getFirstWallIntersection(bspTree.getRoot(), x1, z1, x2, z2,
yBottom, yTop);
}
/**
* Gets the first intersection, if any, of the path (x1,z1)-> (x2,z2) with
* the walls of the BSP tree, starting with the specified node. Returns the
* first BSPPolyon intersection, or null if no intersection occurred.
*/
protected BSPPolygon getFirstWallIntersection(BSPTree.Node node, float x1,
float z1, float x2, float z2, float yBottom, float yTop) {
if (node == null || node instanceof BSPTree.Leaf) {
return null;
}
int start = node.partition.getSideThick(x1, z1);
int end = node.partition.getSideThick(x2, z2);
float intersectionX;
float intersectionZ;
if (end == BSPLine.COLLINEAR) {
end = start;
}
if (start == BSPLine.COLLINEAR) {
intersectionX = x1;
intersectionZ = z1;
} else if (start != end) {
path.setLine(x1, z1, x2, z2);
node.partition.getIntersectionPoint(path, intersection);
intersectionX = intersection.x;
intersectionZ = intersection.y;
} else {
intersectionX = x2;
intersectionZ = z2;
}
if (start == BSPLine.COLLINEAR && start == end) {
return null;
}
// check front part of line
if (start != BSPLine.COLLINEAR) {
BSPPolygon wall = getFirstWallIntersection(
(start == BSPLine.FRONT) ? node.front : node.back, x1, z1,
intersectionX, intersectionZ, yBottom, yTop);
if (wall != null) {
return wall;
}
}
// test this boundary
if (start != end || start == BSPLine.COLLINEAR) {
BSPPolygon wall = getWallCollision(node.polygons, x1, z1, x2, z2,
yBottom, yTop);
if (wall != null) {
intersection.setLocation(intersectionX, intersectionZ);
return wall;
}
}
// check back part of line
if (start != end) {
BSPPolygon wall = getFirstWallIntersection(
(end == BSPLine.FRONT) ? node.front : node.back,
intersectionX, intersectionZ, x2, z2, yBottom, yTop);
if (wall != null) {
return wall;
}
}
// not found
return null;
}
/**
* Checks if the specified path collides with any of the collinear list of
* polygons. The path crosses the line represented by the polygons, but the
* polygons may not necessarily cross the path.
*/
protected BSPPolygon getWallCollision(List polygons, float x1, float z1,
float x2, float z2, float yBottom, float yTop) {
path.setLine(x1, z1, x2, z2);
for (int i = 0; i < polygons.size(); i++) {
BSPPolygon poly = (BSPPolygon) polygons.get(i);
BSPLine wall = poly.getLine();
// check if not wall
if (wall == null) {
continue;
}
// check if not vertically in the wall (y axis)
if (wall.top <= yBottom || wall.bottom > yTop) {
continue;
}
// check if moving to back of wall
if (wall.getSideThin(x2, z2) != BSPLine.BACK) {
continue;
}
// check if path crosses wall
int side1 = path.getSideThin(wall.x1, wall.y1);
int side2 = path.getSideThin(wall.x2, wall.y2);
if (side1 != side2) {
return poly;
}
}
return null;
}
/**
* Checks if the specified object collisions with any other object in the
* specified list.
*/
public boolean checkObject(GameObject objectA, List objects,
Vector3D oldLocation) {
boolean collision = false;
for (int i = 0; i < objects.size(); i++) {
GameObject objectB = (GameObject) objects.get(i);
collision |= checkObject(objectA, objectB, oldLocation);
}
return collision;
}
/**
* Returns true if the two specified objects collide. Object A is the moving
* object, and Object B is the object to check. Uses bounding upright
* cylinders (circular base and top) to determine collisions.
*/
public boolean checkObject(GameObject objectA, GameObject objectB,
Vector3D oldLocation) {
// don't collide with self
if (objectA == objectB) {
return false;
}
PolygonGroupBounds boundsA = objectA.getBounds();
PolygonGroupBounds boundsB = objectB.getBounds();
// first, check y axis collision (assume height is pos)
float Ay1 = objectA.getY() + boundsA.getBottomHeight();
float Ay2 = objectA.getY() + boundsA.getTopHeight();
float By1 = objectB.getY() + boundsB.getBottomHeight();
float By2 = objectB.getY() + boundsB.getTopHeight();
if (By2 < Ay1 || By1 > Ay2) {
return false;
}
// next, check 2D, x/z plane collision (circular base)
float dx = objectA.getX() - objectB.getX();
float dz = objectA.getZ() - objectB.getZ();
float minDist = boundsA.getRadius() + boundsB.getRadius();
float distSq = dx * dx + dz * dz;
float minDistSq = minDist * minDist;
if (distSq < minDistSq) {
return handleObjectCollision(objectA, objectB, distSq, minDistSq,
oldLocation);
}
return false;
}
/**
* Handles an object collision. Object A is the moving object, and Object B
* is the object that Object A collided with.
*/
protected boolean handleObjectCollision(GameObject objectA,
GameObject objectB, float distSq, float minDistSq,
Vector3D oldLocation) {
objectA.notifyObjectCollision(objectB);
return true;
}
}
/**
* The PolygonGroupBounds represents a cylinder bounds around a PolygonGroup
* that can be used for collision detection.
*/
class PolygonGroupBounds {
private float topHeight;
private float bottomHeight;
private float radius;
/**
* Creates a new PolygonGroupBounds with no bounds.
*/
public PolygonGroupBounds() {
}
/**
* Creates a new PolygonGroupBounds with the bounds of the specified
* PolygonGroup.
*/
public PolygonGroupBounds(PolygonGroup group) {
setToBounds(group);
}
/**
* Sets this to the bounds of the specified PolygonGroup.
*/
public void setToBounds(PolygonGroup group) {
topHeight = Float.MIN_VALUE;
bottomHeight = Float.MAX_VALUE;
radius = 0;
group.resetIterator();
while (group.hasNext()) {
Polygon3D poly = group.nextPolygon();
for (int i = 0; i < poly.getNumVertices(); i++) {
Vector3D v = poly.getVertex(i);
topHeight = Math.max(topHeight, v.y);
bottomHeight = Math.min(bottomHeight, v.y);
// compute radius squared
radius = Math.max(radius, v.x * v.x + v.z * v.z);
}
}
if (radius == 0) {
// empty polygon group!
topHeight = 0;
bottomHeight = 0;
} else {
radius = (float) Math.sqrt(radius);
}
}
public float getTopHeight() {
return topHeight;
}
public void setTopHeight(float topHeight) {
this.topHeight = topHeight;
}
public float getBottomHeight() {
return bottomHeight;
}
public void setBottomHeight(float bottomHeight) {
this.bottomHeight = bottomHeight;
}
public float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius;
}
}
/**
* 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());
}
}
/**
* 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;
}
}
}
/**
* 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&
|