diff --git a/Pool3D/.classpath b/Pool3D/.classpath index a3e8e05..4c846b0 100644 --- a/Pool3D/.classpath +++ b/Pool3D/.classpath @@ -2,7 +2,6 @@ - diff --git a/Pool3D/src/com/brianmccutchon/pool3d/Controller.java b/Pool3D/src/com/brianmccutchon/pool3d/Controller.java index 02d52ae..6837ece 100644 --- a/Pool3D/src/com/brianmccutchon/pool3d/Controller.java +++ b/Pool3D/src/com/brianmccutchon/pool3d/Controller.java @@ -1,7 +1,6 @@ package com.brianmccutchon.pool3d; import static java.awt.event.KeyEvent.*; -import geometry.Point3D; import java.awt.Component; import java.awt.event.KeyAdapter; @@ -15,7 +14,7 @@ public class Controller { private static final double MOVE_SPEED = 0.5; - private static final double ROT_SPEED = 0.05; + private static final double ROT_SPEED = 0.02; /** Holds the keys that are currently being pressed. **/ private HashSet keysDown = new HashSet<>(); @@ -97,9 +96,9 @@ void switchMode() { pool.shooting = !pool.shooting; if (pool.shooting) { - //Point3D p = Physics.balls[0].center; + //Point3d p = Physics.balls[0].center; //camTransform.lookAt(new Point3d(p.x, p.y, p.z+4), - // new Point3d(p.x, p.y, p.z), new Vector3d(0, 1, 0)); + // p, new Vector3d(0, 1, 0)); camTransform.lookAt(new Point3d(0, 0, -4), new Point3d(0, 0, 0), new Vector3d(0, 1, 0)); cam.setTransform(camTransform); @@ -107,7 +106,7 @@ void switchMode() { } void shoot() { - + // TODO Write method Controller.shoot() } void moveForward() { @@ -202,9 +201,7 @@ void rotateAroundCue(Matrix3d rot) { camTransform.get(translateVec); // Subtract from it the position of the cue ball - //Point3d cue = new Point3d(Physics.balls[0].center.x, - // Physics.balls[0].center.y, Physics.balls[0].center.z); - //translateVec.sub(cue); + //translateVec.sub(Physics.balls[0].center); // Rotate the vector vecMatMult(rot, translateVec); @@ -232,6 +229,7 @@ void rotateUpShooting() { rotateUpShooting(ROT_SPEED); } + // FIXME Doesn't work void rotateUpShooting(double angle) { Vector3d translateVec = new Vector3d(); camTransform.get(translateVec); diff --git a/Pool3D/src/com/brianmccutchon/pool3d/Physics.java b/Pool3D/src/com/brianmccutchon/pool3d/Physics.java index 98df22b..580cf03 100644 --- a/Pool3D/src/com/brianmccutchon/pool3d/Physics.java +++ b/Pool3D/src/com/brianmccutchon/pool3d/Physics.java @@ -1,6 +1,6 @@ package com.brianmccutchon.pool3d; -import geometry.Point3D; +import javax.vecmath.*; /** * This is a purely static class containing information related to the physics @@ -18,16 +18,16 @@ public class Physics { public static final double EPSILON = Math.pow(10.0, -15); /** A unit vector along the x axis. **/ - static final Point3D X_UNIT_VEC = new Point3D(1, 0, 0); + static final Vector3d X_UNIT_VEC = new Vector3d(1, 0, 0); /** A unit vector along the y axis. **/ - static final Point3D Y_UNIT_VEC = new Point3D(0, 1, 0); + static final Vector3d Y_UNIT_VEC = new Vector3d(0, 1, 0); /** A unit vector along the y axis. **/ - static final Point3D Z_UNIT_VEC = new Point3D(0, 0, 1); + static final Vector3d Z_UNIT_VEC = new Vector3d(0, 0, 1); /** The origin of the coordinate system: (0, 0, 0) **/ - static final Point3D ORIGIN = new Point3D(0, 0, 0); + static final Point3d ORIGIN = new Point3d(0, 0, 0); // TODO Get rid of global variables like this one. // Maybe make Physics instantiable? or create a new class. @@ -100,35 +100,25 @@ static void transposeSquareMat(double[][] m) { } } - /** - * Normalizes a vector in place. This means that the vector will be - * converted into a unit vector with the same direction as the original. - * @param p The vector to nomalize. - */ - private static void normalize(Point3D p) { - double norm = p.dist(new Point3D(0, 0, 0)); - p.x /= norm; - p.y /= norm; - p.z /= norm; - } - /** * Computes the rotation matrix that, if the balls are translated so that * ball1 is at (0, 0, 0) and the matrix is applied to the locations of the * two balls, ball2's x and y coordinates will equal 0. Also, ball2's x * coordinate should then be greater than ball1's x coordinate. * - * @param ball1 A pool ball. - * @param ball2 Another pool ball. + * @param center A pool ball. + * @param center2 Another pool ball. * @return The collision rotation matrix. */ static double[][] findCollisionRotationMat( - Point3D ball1, Point3D ball2) { - Point3D ball2loc = ball2.subtract(ball1); - normalize(ball2loc); + Point3d center, Point3d center2) { + Vector3d ball2loc = new Vector3d(); + ball2loc.sub(center2, center); + ball2loc.normalize(); // Vector representing the axis of rotation - Point3D a = ball2loc.cross(X_UNIT_VEC); + Vector3d a = new Vector3d(); + a.cross(ball2loc, X_UNIT_VEC); // Since ball2Loc and X_UNIT_VEC are unit vectors, the following hold: @@ -136,13 +126,13 @@ static double[][] findCollisionRotationMat( double cos = ball2loc.dot(X_UNIT_VEC); // The magnitude of their cross product is the sin of the rotation angle - double sin = a.dist(ORIGIN); + double sin = a.length(); // The matrix below only works if the axis is a unit vector - if (almostEq(a.dist(ORIGIN), 0)) { + if (almostEq(sin, 0)) { a = Y_UNIT_VEC; } else { - normalize(a); + a.normalize(); } // Rotation matrix given an axis and an angle. Source: @@ -156,14 +146,14 @@ static double[][] findCollisionRotationMat( /** * Rotates a vector using a provided rotation matrix. - * @param v The vector to rotate. + * @param velocity The vector to rotate. * @param m The 3x3 rotation matrix. */ - static void rotateVec(Point3D v, double[][] m) { - v.setLocation( - v.x * m[0][0] + v.y * m[0][1] + v.z * m[0][2], - v.x * m[1][0] + v.y * m[1][1] + v.z * m[1][2], - v.x * m[2][0] + v.y * m[2][1] + v.z * m[2][2]); + static void rotateVec(Vector3d velocity, double[][] m) { + velocity.set( + velocity.x * m[0][0] + velocity.y * m[0][1] + velocity.z * m[0][2], + velocity.x * m[1][0] + velocity.y * m[1][1] + velocity.z * m[1][2], + velocity.x * m[2][0] + velocity.y * m[2][1] + velocity.z * m[2][2]); } /** @@ -173,12 +163,12 @@ public static void nextFrame() { ballsAreMoving = false; for (PoolBall b : balls) { - if (almostEq(b.velocity, ORIGIN, Physics.MOVEMENT_EPSILON)) { + if (b.velocity.epsilonEquals(ORIGIN, MOVEMENT_EPSILON)) { // "Close enough" to (0, 0, 0). - b.velocity.setLocation(0, 0, 0); + b.velocity.set(0, 0, 0); } else { ballsAreMoving = true; // We found a ball that is moving - b.center = b.center.add(b.velocity); + b.center.add(b.velocity); doAirResistance(b.velocity); } } @@ -221,33 +211,6 @@ static boolean almostEq(double d1, double d2) { return Math.abs(d1 - d2) < EPSILON; } - /** - * Checks whether two points are almost equal in each of their three - * components. That is, within {@link Physics#EPSILON}. - * @param p1 A point to compare. - * @param p2 A point to compare. - * @return {@code true} iff they are almost equal. - */ - private static boolean almostEq(Point3D p1, Point3D p2) { - return almostEq(p1.x, p2.x) && - almostEq(p1.y, p2.y) && - almostEq(p1.z, p2.z); - } - - /** - * Checks whether two points are almost equal in each of their three - * components. - * @param p1 A point to compare. - * @param p2 A point to compare. - * @param epsilon The maximum allowed difference. - * @return {@code true} iff they are almost equal. - */ - private static boolean almostEq(Point3D p1, Point3D p2, double epsilon) { - return almostEq(p1.x, p2.x, epsilon) && - almostEq(p1.y, p2.y, epsilon) && - almostEq(p1.z, p2.z, epsilon); - } - /** * Checks whether two numbers are almost equal to each other. That is, * within {@link Physics#EPSILON}. @@ -263,7 +226,7 @@ private static boolean almostEq(double x, double n, double epsilon) { * Computes linear "air resistance" on a ball's velocity. * @param p The ball's velocity. */ - private static void doAirResistance(Point3D p) { + private static void doAirResistance(Vector3d p) { p.x = Math.signum(p.x) * Math.max(0, Math.abs(p.x) - AIR_RESISTANCE); p.y = Math.signum(p.y) * Math.max(0, Math.abs(p.y) - AIR_RESISTANCE); p.z = Math.signum(p.z) * Math.max(0, Math.abs(p.z) - AIR_RESISTANCE); diff --git a/Pool3D/src/com/brianmccutchon/pool3d/Pool3D.java b/Pool3D/src/com/brianmccutchon/pool3d/Pool3D.java index 8e9a1cf..1bce85e 100644 --- a/Pool3D/src/com/brianmccutchon/pool3d/Pool3D.java +++ b/Pool3D/src/com/brianmccutchon/pool3d/Pool3D.java @@ -1,73 +1,61 @@ package com.brianmccutchon.pool3d; import java.awt.*; -import java.awt.event.KeyAdapter; - -import static java.awt.event.KeyEvent.*; - -import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; +import javax.media.j3d.*; import javax.swing.*; +import javax.vecmath.*; + +import com.sun.j3d.utils.geometry.*; +import com.sun.j3d.utils.image.TextureLoader; +import com.sun.j3d.utils.universe.*; -import geometry.Camera; -import geometry.Environment; -import geometry.Point3D; -import geometry.Triangle3D; import static com.brianmccutchon.pool3d.Physics.*; /** * This class represents the main class and GUI of 3D pool. * Currently, the game has two modes, shooting and not shooting, as * represented by {@link #shooting}. Each has its own set of event handlers. - * + * * @author Brian McCutchon */ -public class Pool3D extends JPanel { - - private static final long serialVersionUID = 2316556066963532682L; - - /** The rendering environment of this game **/ - Environment env = new Environment(); - - /** - * Holds the keys that are currently being pressed. - * Idea taken from Arena.java - */ - HashSet keysDown = new HashSet<>(); +public class Pool3D { /** {@code true} iff we are in shooting mode **/ - private boolean shooting = false; - - /** Key handlers for the moving mode. **/ - private HashMap moveHandlers = new HashMap<>(); - - /** Key handlers for the shooting mode. **/ - private HashMap shootHandlers = new HashMap<>(); + boolean shooting = false; /** Timer for rendering loop. **/ private Timer t; + private HashMap ballsToSpheres = new HashMap<>(); + /** The corners of the table. **/ - static Point3D[] corners = { - new Point3D( TABLE_X, TABLE_Y, TABLE_Z), - new Point3D( TABLE_X, TABLE_Y, -TABLE_Z), - new Point3D( TABLE_X, -TABLE_Y, TABLE_Z), - new Point3D( TABLE_X, -TABLE_Y, -TABLE_Z), - new Point3D(-TABLE_X, TABLE_Y, TABLE_Z), - new Point3D(-TABLE_X, TABLE_Y, -TABLE_Z), - new Point3D(-TABLE_X, -TABLE_Y, TABLE_Z), - new Point3D(-TABLE_X, -TABLE_Y, -TABLE_Z), + static Point3d[] corners = { + new Point3d( TABLE_X, TABLE_Y, TABLE_Z), + new Point3d( TABLE_X, TABLE_Y, -TABLE_Z), + new Point3d( TABLE_X, -TABLE_Y, TABLE_Z), + new Point3d( TABLE_X, -TABLE_Y, -TABLE_Z), + new Point3d(-TABLE_X, TABLE_Y, TABLE_Z), + new Point3d(-TABLE_X, TABLE_Y, -TABLE_Z), + new Point3d(-TABLE_X, -TABLE_Y, TABLE_Z), + new Point3d(-TABLE_X, -TABLE_Y, -TABLE_Z), }; // Divide each of the points above by two here so that we don't have to // have "/2" 24 times and the code looks cleaner static { - Arrays.asList(corners).replaceAll(p -> p.divide(2)); + Arrays.asList(corners).forEach(p -> { + p.x /= 2; + p.y /= 2; + p.z /= 2; + }); } + Controller controls; + /** * The vertices of each of the triangles as indices into {@link #corners}. */ @@ -80,208 +68,134 @@ public class Pool3D extends JPanel { { 0, 2, 6 }, { 0, 6, 4 }, }; - /** - * The angle by which the camera moves around the ball each frame when in - * shooting mode. - */ - private static final double ROTATE_ANGLE = 0.05; - /** Constructs a new Pool3D JFrame and starts the game. **/ public Pool3D() { - env.ambientLight = 0.5; - env.tempLightSource = new Point3D(20, -50, 20); - - Arrays.asList(balls).forEach(env::addObject); - for (int[] tri : triangles) { - env.addTriangle(new Triangle3D(corners[tri[0]], - corners[tri[1]], corners[tri[2]], Color.GREEN)); - } + SimpleUniverse univ = new SimpleUniverse(); + BranchGroup group = new BranchGroup(); - moveHandlers.put(VK_RIGHT, env::rotateRight); - moveHandlers.put(VK_LEFT, env::rotateLeft); - moveHandlers.put(VK_DOWN, env::moveBackward); - moveHandlers.put(VK_UP, env::moveForward); - moveHandlers.put(VK_S, env::moveDown); - moveHandlers.put(VK_W, env::moveUp); - moveHandlers.put(VK_D, env::moveRight); - moveHandlers.put(VK_A, env::moveLeft); - moveHandlers.put(VK_OPEN_BRACKET, env::nearFarther); - moveHandlers.put(VK_CLOSE_BRACKET, env::nearCloser); - - shootHandlers.put(VK_RIGHT, this::rotateRightShooting); - shootHandlers.put(VK_D, this::rotateRightShooting); - shootHandlers.put(VK_LEFT, this::rotateLeftShooting); - shootHandlers.put(VK_A, this::rotateLeftShooting); - shootHandlers.put(VK_DOWN, this::rotateDownShooting); - shootHandlers.put(VK_S, this::rotateDownShooting); - shootHandlers.put(VK_UP, this::rotateUpShooting); - shootHandlers.put(VK_W, this::rotateUpShooting); - shootHandlers.put(VK_SPACE, this::shoot); + controls = new Controller(this, univ.getCanvas(), + univ.getViewingPlatform().getViewPlatformTransform()); - t = new Timer(16, (e) -> { - Physics.nextFrame(); + for (PoolBall ball : balls) { + TransformGroup ballSphere = makeBallSphere(ball); + ballsToSpheres.put(ball, ballSphere); + group.addChild(ballSphere); + } - HashMap handlers = - shooting ? shootHandlers : moveHandlers; + // Add a directional light + DirectionalLight light1 = new DirectionalLight( + new Color3f(1, 1, 1), + new Vector3f(-8.0f, -14.0f, -6.0f)); + light1.setInfluencingBounds( + new BoundingSphere(new Point3d(0, 0, 0), Double.POSITIVE_INFINITY)); + group.addChild(light1); - for (int i : keysDown) { - if (handlers.containsKey(i)) { - handlers.get(i).run(); - } - } + univ.getViewingPlatform().setNominalViewingTransform(); + univ.addBranchGraph(group); - /* This works too (same as above) - keysDown.stream() - .filter(handlers::containsKey) - .map(handlers::get) - .forEach(Runnable::run); - */ + //for (int[] tri : triangles) { + // env.addTriangle(new Triangle3D(corners[tri[0]], + // corners[tri[1]], corners[tri[2]], Color.GREEN)); + //} - repaint(); + t = new Timer(16, (e) -> { + Physics.nextFrame(); + controls.processEvents(); }); t.start(); } public static void main(String[] args) { - Pool3D pool = new Pool3D(); - for (int i = 0; i < 20; i++) { - pool.env.moveBackward(); - } - + new Pool3D(); + /* JFrame frame = new JFrame(); - frame.setSize(new Dimension(800, 800)); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.add(pool); + JLabel label = new JLabel(); + label.setIcon(new ImageIcon(makeTextureImage(PoolBall.create(6)))); + frame.add(label); + frame.pack(); frame.setVisible(true); - - frame.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent ke) { - if (ke.getKeyCode() == VK_Q) { - pool.switchMode(); - } - - pool.keysDown.add(ke.getKeyCode()); - } - - @Override - public void keyReleased(KeyEvent ke) { - pool.keysDown.remove(ke.getKeyCode()); - } - }); - } - - @Override - public void paint(Graphics gg) { - Graphics2D g = (Graphics2D) gg; - env.render(g); + */ } - public void shoot() { - // To get the velocity of the ball after a shot, - balls[0].velocity = env.getCamera().position // get the difference - .subtract(balls[0].center) // between the cue ball and the camera, - .divide(-1) // negate it - .normalize(); // and normalize it - - switchMode(); - } + static TransformGroup makeBallSphere(PoolBall ball) { - private void switchMode() { - // Can't switch to shooting mode when balls are moving - if (Physics.ballsAreMoving) { - return; - } + Color3f white = new Color3f(Color.WHITE); + Color3f black = new Color3f(Color.BLACK); - shooting = !shooting; + Appearance appear = new Appearance(); - if (shooting) { - // Move the camera to the cue ball's position - Camera c = new Camera(balls[0].center.add( - new Point3D(4, 0, 0)), new Point3D(-1, 0, 0)); - env.setCamera(c); - } - } + // Make a material so that shading can work + Material mat = new Material(white, black, white, black, 1); + appear.setMaterial(mat); - private void rotateRightShooting() { - rotateLR(ROTATE_ANGLE); - } + // Apply texture + BufferedImage img = makeTextureImage(ball); + Texture tex = new TextureLoader(img).getTexture(); + tex.setBoundaryModeS(Texture.WRAP); + tex.setBoundaryModeT(Texture.WRAP); + appear.setTexture(tex); - private void rotateLR(double angle) { - rotateAroundCue(Environment.makeLRRotation(angle)); - } + // Set the mode for the texture so that it can be shaded properly + TextureAttributes texAttr = new TextureAttributes(); + texAttr.setTextureMode(TextureAttributes.MODULATE); + appear.setTextureAttributes(texAttr); - private void rotateLeftShooting() { - rotateLR(-ROTATE_ANGLE); - } + // Create a ball and add it to the group of objects + Sphere sphere = new Sphere(1, Primitive.GENERATE_NORMALS | + Primitive.GENERATE_TEXTURE_COORDS, 200, appear); + TransformGroup group = new TransformGroup(); + group.addChild(sphere); - /** - * Rotates the camera "up" or "down" around the ball by the specified - * angle. - * @param angle - */ - private void rotateUD(double angle) { - Camera cam = env.getCamera(); - cam.position = cam.position.subtract(balls[0].center); - rotateAroundCue(makeUDRotation(angle, cam.position)); - } + // Set the proper translation + // TODO Make PoolBall hold a Transform3D instead of just a center point + // This will allow spinning and make for fewer conversions + Transform3D trans = new Transform3D(); + trans.set(new Vector3d(ball.center)); + group.setTransform(trans); - /** - * Rotates the cmera around the cue ball in shooting mode. - * @param rotationMat The rotation matrix to use. - */ - private void rotateAroundCue(double[][] rotationMat) { - Camera cam = env.getCamera(); - // Translate so that cue ball is at the origin - cam.position = cam.position.subtract(balls[0].center); - // Rotate with the matrix provided - Physics.rotateVec(cam.position, rotationMat); - cam.direction = cam.position.divide(-1).normalize(); - cam.position = cam.position.add(balls[0].center); - env.setCamera(cam); + return group; } - /** - * Creates a rotation matrix to rotate the camera "up" or "down" around the - * origin. - * @param angle The angle by which to rotate. - * @param cameraPos The position of the camera. - * @return The rotation matrix. - */ - private double[][] makeUDRotation(double angle, Point3D cameraPos) { - // Vector representing the axis of rotation - Point3D a = cameraPos.cross(Z_UNIT_VEC); - - // The matrix below only works if the axis is a unit vector - if (almostEq(a.dist(ORIGIN), 0)) { - a = Y_UNIT_VEC; - } else { - a = a.normalize(); + static BufferedImage makeTextureImage(PoolBall ball) { + int height = 1 << 9; + int width = 2 * height; + + BufferedImage img = new BufferedImage(width, height, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = img.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + g.fillRect(0, 0, width, height); + + if (ball.type != BallType.CUE) { + g.setColor(ball.hue.get()); + int margin = (ball.type == BallType.STRIPE) ? + (int) (height / 3.5) : 0; + g.fillRect(0, margin, width, height - 2*margin); + + drawNumber(g, ball.ballNum, width/4, height/2, height/7); + drawNumber(g, ball.ballNum, width/4*3, height/2, height/7); } - double cos = Math.cos(angle); - double sin = Math.sin(angle); - - // Same source as in Physics.findCollisionRotationMat() - return new double[][] { - {cos+a.x*a.x*(1-cos), a.x*a.y*(1-cos)-a.z*sin, a.x*a.z*(1-cos)+a.y*sin}, - {a.y*a.x*(1-cos)+a.z*sin, cos+a.y*a.y*(1-cos), a.y*a.z*(1-cos)-a.x*sin}, - {a.z*a.x*(1-cos)-a.y*sin, a.z*a.y*(1-cos)+a.x*sin, cos+a.z*a.z*(1-cos)} - }; + return img; } - // XXX Neither of the below methods can be fully implemented currently - // because our camera has only one degree of freedom. - - /** Rotates the camera "up" around the cue ball in shooting mode. **/ - private void rotateUpShooting() { - rotateUD(ROTATE_ANGLE); + private static void drawNumber(Graphics2D g, + int number, int x, int y, int radius) { + g.setColor(Color.WHITE); + g.fillOval(x-radius, y-radius, radius*2, radius*2); + + g.setColor(Color.BLACK); + g.setFont(g.getFont().deriveFont(100f)); + drawStringCentered(g, Integer.toString(number), x, y); } - /** Rotates the camera "down" around the cue ball in shooting mode. **/ - private void rotateDownShooting() { - rotateUD(-ROTATE_ANGLE); + private static void drawStringCentered(Graphics2D g, + String string, int x, int y) { + FontMetrics fm = g.getFontMetrics(); + g.drawString(string, x - fm.stringWidth(string)/2, y - + (fm.getAscent() + fm.getDescent())/2 + fm.getAscent()); } } diff --git a/Pool3D/src/com/brianmccutchon/pool3d/PoolBall.java b/Pool3D/src/com/brianmccutchon/pool3d/PoolBall.java index 9e9d6cb..8022ec7 100644 --- a/Pool3D/src/com/brianmccutchon/pool3d/PoolBall.java +++ b/Pool3D/src/com/brianmccutchon/pool3d/PoolBall.java @@ -1,20 +1,13 @@ package com.brianmccutchon.pool3d; -import geometry.DSArrayList; -import geometry.EnvironmentObject; -import geometry.Point3D; -import geometry.Triangle3D; +import java.util.*; -import java.awt.Color; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import javax.vecmath.*; -import static java.awt.Color.*; import static com.brianmccutchon.pool3d.BallType.*; +import static java.awt.Color.*; -public class PoolBall extends EnvironmentObject { +public class PoolBall { public static final int RADIUS = 1; @@ -23,7 +16,7 @@ public class PoolBall extends EnvironmentObject { private static final double DIAMETER_SQUARED = DIAMETER*DIAMETER; /** The display color of this pool ball. **/ - public final Color hue; + public final Color3f hue; /** The type of this ball. **/ public final BallType type; @@ -31,89 +24,55 @@ public class PoolBall extends EnvironmentObject { /** The number of this ball. 0 if it is the cue ball. **/ public final int ballNum; - /** The golden ratio. **/ - private static final double PHI = (1 + Math.sqrt(5)) / 2; + /** The location of the center of this PoolBall. **/ + public Point3d center; + + /** The velocity of the ball. **/ + public Vector3d velocity; /** Just a couple of color constants. **/ - private static final Color - BROWN = new Color(139, 69, 19), - PURPLE = new Color(128, 0, 128); + private static final Color3f + BROWN = new Color3f(0.55f, 0.27f, 0.07f), + PURPLE = new Color3f(0.5f , 0f, 0.5f); /** * Possible locations of all balls except the cue and eight balls, * which have different placement rules. */ - private static List rackLocations = Arrays.asList( - new Point3D( 1.633, -1.0, -0.5774), - new Point3D( 1.633, 1.0, -0.5774), - new Point3D( 1.633, 0.0, 1.1547), - new Point3D( 0.0, -2.0, 0.0), - new Point3D( 0.0, 2.0, 0.0), - new Point3D( 0.0, 1.0, 1.7321), - new Point3D( 0.0, -1.0, 1.7321), - new Point3D( 0.0, 1.0, -1.7321), - new Point3D( 0.0, -1.0, -1.7321), - new Point3D( -1.633, -1.0, 0.5774), - new Point3D( -1.633, 1.0, 0.5774), - new Point3D( -1.633, 0.0, -1.1547), - new Point3D( -3.267, 0.0, 0.0), - new Point3D( 3.267, 0.0, 0.0)); - - /** - * The coordinates of vertices of the polyhedron that the wireframe - * generation algorithm starts with. These are normalized in the static - * code block below. - */ - private static Point3D[] polyCoords = { - new Point3D( 0, 1, PHI), - new Point3D( 0, -1, PHI), - new Point3D( 0, 1, -PHI), - new Point3D( 0, -1, -PHI), - new Point3D( 1, PHI, 0), - new Point3D( -1, PHI, 0), - new Point3D( 1, -PHI, 0), - new Point3D( -1, -PHI, 0), - new Point3D( PHI, 0, 1), - new Point3D(-PHI, 0, 1), - new Point3D( PHI, 0, -1), - new Point3D(-PHI, 0, -1), - }; - - // Normalize the coordinates of the polyhedron - static { - Arrays.asList(polyCoords).replaceAll(Point3D::normalize); - } - - /** - * The sides of the polyhedron that the wireframe generation algorithm - * starts with as arrays of indices into {@link #polyCoords}. - */ - private static int[][] polySides = { - { 0, 1, 8 }, { 0, 9, 1 }, { 0, 5, 9 }, { 0, 4, 5 }, - { 0, 8, 4 }, { 3, 6, 7 }, { 3, 10, 6 }, { 3, 2, 10 }, - { 3, 11, 2 }, { 3, 7, 11 }, { 1, 9, 7 }, { 1, 7, 6 }, - { 1, 6, 8 }, { 10, 8, 6 }, { 10, 4, 8 }, { 10, 2, 4 }, - { 5, 4, 2 }, { 5, 2, 11 }, { 5, 11, 9 }, { 7, 9, 11 }, - }; + private static List rackLocations = Arrays.asList( + new Point3d( 1.633, -1.0, -0.5774), + new Point3d( 1.633, 1.0, -0.5774), + new Point3d( 1.633, 0.0, 1.1547), + new Point3d( 0.0, -2.0, 0.0), + new Point3d( 0.0, 2.0, 0.0), + new Point3d( 0.0, 1.0, 1.7321), + new Point3d( 0.0, -1.0, 1.7321), + new Point3d( 0.0, 1.0, -1.7321), + new Point3d( 0.0, -1.0, -1.7321), + new Point3d( -1.633, -1.0, 0.5774), + new Point3d( -1.633, 1.0, 0.5774), + new Point3d( -1.633, 0.0, -1.1547), + new Point3d( -3.267, 0.0, 0.0), + new Point3d( 3.267, 0.0, 0.0)); /** This array holds information from which balls can be constructed. **/ private static final PoolBall[] balls = { - new PoolBall(0, 0, 0, WHITE, CUE, 0), - new PoolBall(0, 0, 0, YELLOW, SOLID, 1), - new PoolBall(0, 0, 0, BLUE, SOLID, 2), - new PoolBall(0, 0, 0, RED, SOLID, 3), - new PoolBall(0, 0, 0, PURPLE, SOLID, 4), - new PoolBall(0, 0, 0, ORANGE, SOLID, 5), - new PoolBall(0, 0, 0, GREEN, SOLID, 6), - new PoolBall(0, 0, 0, BROWN, SOLID, 7), - new PoolBall(0, 0, 0, BLACK, EIGHT, 8), - new PoolBall(0, 0, 0, YELLOW, STRIPE, 9), - new PoolBall(0, 0, 0, BLUE, STRIPE, 10), - new PoolBall(0, 0, 0, RED, STRIPE, 11), - new PoolBall(0, 0, 0, PURPLE, STRIPE, 12), - new PoolBall(0, 0, 0, ORANGE, STRIPE, 13), - new PoolBall(0, 0, 0, GREEN, STRIPE, 14), - new PoolBall(0, 0, 0, BROWN, STRIPE, 15), + new PoolBall(0, 0, 0, new Color3f( WHITE), CUE, 0), + new PoolBall(0, 0, 0, new Color3f(YELLOW), SOLID, 1), + new PoolBall(0, 0, 0, new Color3f( BLUE), SOLID, 2), + new PoolBall(0, 0, 0, new Color3f( RED), SOLID, 3), + new PoolBall(0, 0, 0, PURPLE, SOLID, 4), + new PoolBall(0, 0, 0, new Color3f(ORANGE), SOLID, 5), + new PoolBall(0, 0, 0, new Color3f( GREEN), SOLID, 6), + new PoolBall(0, 0, 0, BROWN, SOLID, 7), + new PoolBall(0, 0, 0, new Color3f( BLACK), EIGHT, 8), + new PoolBall(0, 0, 0, new Color3f(YELLOW), STRIPE, 9), + new PoolBall(0, 0, 0, new Color3f( BLUE), STRIPE, 10), + new PoolBall(0, 0, 0, new Color3f( RED), STRIPE, 11), + new PoolBall(0, 0, 0, PURPLE, STRIPE, 12), + new PoolBall(0, 0, 0, new Color3f(ORANGE), STRIPE, 13), + new PoolBall(0, 0, 0, new Color3f( GREEN), STRIPE, 14), + new PoolBall(0, 0, 0, BROWN, STRIPE, 15), }; /** @@ -123,26 +82,6 @@ public class PoolBall extends EnvironmentObject { */ static final int SMOOTHNESS = 4; - /** - * The global sphere wireframe. This is referenced in each of the balls' - * {@link EnvironmentObject#triangles triangles} lists. - */ - private static final DSArrayList WIREFRAME = new DSArrayList<>(); - - // Construct the wireframe - static { - // For each side of the polygon, recursively subdivide its faces into - // equilateral triangles. This results in a sphere wireframe with - // nearly equilateral triangles of similar size. - for (int[] side : polySides) { - ArrayList dome = makeDome(SMOOTHNESS - 1, - new Triangle3D(polyCoords[side[0]], - polyCoords[side[1]], polyCoords[side[2]])); - - dome.forEach(WIREFRAME::add); - } - } - /** * Constructs a new PoolBall, requiring the caller to supply data about it. * To have the data automatically determined by the ball number, use @@ -159,13 +98,12 @@ public class PoolBall extends EnvironmentObject { * smoothness provided. This should be a small number, such as 6 or 7. */ public PoolBall(double x, double y, double z, - Color hue, BallType type, int ballNum) { - this.center = new Point3D(x, y, z); + Color3f hue, BallType type, int ballNum) { + this.center = new Point3d(x, y, z); this.hue = hue; this.type = type; this.ballNum = ballNum; - this.velocity = new Point3D(0, 0, 0); - this.triangles = WIREFRAME; + this.velocity = new Vector3d(0, 0, 0); } /** @@ -180,107 +118,6 @@ public PoolBall(int ballNum) { throw new RuntimeException("Not yet implemented."); } - /** - * Splits the given triangle into smaller equilateral triangles, which are - * normalized to the unit circle. This is not really a "dome," I just - * couldn't think of a better name for it. - * - * @param depth The depth to which to recurse. - * @param tri The triangle to subdivide. - * @return An ArrayList containing {@code Math.pow(4, depth)} triangles. - */ - private static ArrayList makeDome(int depth, Triangle3D tri) { - ArrayList triangles = new ArrayList<>(); - - if (depth == 0) { // Depth limit reached, stop recursing - triangles.add(tri); - } else { - depth--; - Point3D[] ps = tri.points; - - // The center triangle - triangles.addAll(makeDome(depth, new Triangle3D( - normAvg(ps[0], ps[1]), - normAvg(ps[1], ps[2]), - normAvg(ps[2], ps[0])))); - - // The three smaller triangles - int n = ps.length; - for (int i = 0; i < n; i++) { - triangles.addAll(makeDome(depth, new Triangle3D(ps[i], - normAvg(ps[i], ps[(i+1)%n]), - normAvg(ps[i], ps[(i+2)%n])))); - } - } - - return triangles; - } - - /** Computes the normalized average of two points. **/ - private static Point3D normAvg(Point3D p1, Point3D p2) { - return p1.add(p2).divide(2).normalize(); - } - - /** - * The transformation matrix applied in the getTriangles method. - * A translation matrix is used because I may wish to add spinning later - * and it would be fairly easy to combine this translation matrix with a - * rotation matrix. - */ - private double[][] transformMat = { - { 1, 0, 0, center.x }, - { 0, 1, 0, center.y }, - { 0, 0, 1, center.z }, - { 0, 0, 0, 1 }, - }; - - @Override - public DSArrayList getTriangles() { - // Update the transformation matrix - transformMat[0][3] = center.x; - transformMat[1][3] = center.y; - transformMat[2][3] = center.z; - - // Compute the return value in parallel - DSArrayList retVal = new DSArrayList<>(); - triangles.stream().parallel() - .map(t -> { - // Apply transformMat to each coord of the triangle - Triangle3D transformed = new Triangle3D( - transform(t.points[0]), - transform(t.points[1]), - transform(t.points[2])); - - // Color the triangle if it is within a certain range. - // The range depends on what type of ball this is - if (type != BallType.CUE && - Math.abs((t.points[0].x+t.points[1].x+t.points[2].x)/3) < - (type == BallType.STRIPE ? 0.5 : 0.8)) { - transformed.triColor = hue; - } - - return transformed; - }) - .forEachOrdered(retVal::add);; - - return retVal; - } - - /** - * Transforms a Point3D using {@link #transformMat}. - * @param p The point to transform - * @return The transformed point - */ - private Point3D transform(Point3D p) { - return new Point3D( - p.x*transformMat[0][0] + p.y*transformMat[0][1] + - p.z*transformMat[0][2] + transformMat[0][3], - p.x*transformMat[1][0] + p.y*transformMat[1][1] + - p.z*transformMat[1][2] + transformMat[1][3], - p.x*transformMat[2][0] + p.y*transformMat[2][1] + - p.z*transformMat[2][2] + transformMat[2][3]); - } - /** * Returns a new PoolBall with the given ball number and the default values * for a ball with this number. @@ -317,15 +154,15 @@ public static PoolBall[] rack() { } } - retVal[0].center = new Point3D(10, 0, 0); - retVal[8].center = new Point3D(0, 0, 0); + retVal[0].center = new Point3d(10, 0, 0); + retVal[8].center = new Point3d(0, 0, 0); return retVal; } /** Determines if this pool ball intersects with another pool ball. **/ public boolean intersects(PoolBall pb) { - return center.distSq(pb.center) < DIAMETER_SQUARED; + return center.distanceSquared(pb.center) < DIAMETER_SQUARED; } /** diff --git a/Pool3D/test/com/brianmccutchon/pool3d/PhysTest.java b/Pool3D/test/com/brianmccutchon/pool3d/PhysTest.java index f0361c9..fa93c4a 100644 --- a/Pool3D/test/com/brianmccutchon/pool3d/PhysTest.java +++ b/Pool3D/test/com/brianmccutchon/pool3d/PhysTest.java @@ -1,7 +1,7 @@ package com.brianmccutchon.pool3d; import static org.junit.Assert.*; -import geometry.Point3D; +import javax.vecmath.*; import org.junit.Test; @@ -49,31 +49,31 @@ private double determinant(double[][] mat) { @Test public void testRotationMat() { - Point3D ball1 = new Point3D(0, 0, 0); - Point3D ball2 = new Point3D(PoolBall.DIAMETER - Physics.EPSILON, 0, 0); + Point3d ball1 = new Point3d(0, 0, 0); + Point3d ball2 = new Point3d(PoolBall.DIAMETER - Physics.EPSILON, 0, 0); // No rotation required, should return the identity matrix assertArrayEquals(identity, Physics.findCollisionRotationMat(ball1, ball2)); - checkRotationMat(new Point3D(2, 2, 2), - new Point3D(2 + Math.sqrt(PoolBall.DIAMETER), + checkRotationMat(new Point3d(2, 2, 2), + new Point3d(2 + Math.sqrt(PoolBall.DIAMETER), 2 + Math.sqrt(PoolBall.DIAMETER), 2)); - checkRotationMat(new Point3D(-2, -3, -5), - new Point3D(-2, -3 - Math.sqrt(PoolBall.DIAMETER), + checkRotationMat(new Point3d(-2, -3, -5), + new Point3d(-2, -3 - Math.sqrt(PoolBall.DIAMETER), -5 + Math.sqrt(PoolBall.DIAMETER))); - checkRotationMat(new Point3D(5, 4, 3), new Point3D(6, 5, 2)); + checkRotationMat(new Point3d(5, 4, 3), new Point3d(6, 5, 2)); } /** * Performs assertions to ensure that the rotation matrix is computed * correctly for the two pool balls provided. */ - private void checkRotationMat(Point3D p1, Point3D p2) { - Point3D ball1 = new Point3D(p1.x, p1.y, p1.z); - Point3D ball2 = new Point3D(p2.x, p2.y, p2.z); + private void checkRotationMat(Point3d p1, Point3d p2) { + Point3d ball1 = new Point3d(p1.x, p1.y, p1.z); + Point3d ball2 = new Point3d(p2.x, p2.y, p2.z); double[][] rotationMat = Physics.findCollisionRotationMat(ball1, ball2); @@ -81,7 +81,8 @@ private void checkRotationMat(Point3D p1, Point3D p2) { // Every rotation matrix should have a determinant of 1.0 assertEquals(1.0, determinant(rotationMat), Physics.EPSILON); - Point3D p = ball2.subtract(ball1); + Vector3d p = new Vector3d(); + p.sub(ball2, ball1); Physics.rotateVec(p, rotationMat); assertEquals(0.0, p.y, Physics.EPSILON); @@ -94,27 +95,27 @@ public void testHandleCollision() { PoolBall ball2 = new PoolBall( PoolBall.DIAMETER - Physics.EPSILON, 0, 0, null, null, 1); - ball1.velocity.setLocation( 1, 0, 0); - ball2.velocity.setLocation(-1, 0, 0); + ball1.velocity.set( 1, 0, 0); + ball2.velocity.set(-1, 0, 0); Physics.handleCollision(ball1, ball2); - assertEquals(new Point3D(-1, 0, 0), ball1.velocity); - assertEquals(new Point3D( 1, 0, 0), ball2.velocity); + assertEquals(new Point3d(-1, 0, 0), ball1.velocity); + assertEquals(new Point3d( 1, 0, 0), ball2.velocity); // Now they're headed in opposite directions; a collision check // shouldn't do anything Physics.handleCollision(ball1, ball2); - assertEquals(new Point3D(-1, 0, 0), ball1.velocity); - assertEquals(new Point3D( 1, 0, 0), ball2.velocity); + assertEquals(new Point3d(-1, 0, 0), ball1.velocity); + assertEquals(new Point3d( 1, 0, 0), ball2.velocity); // Now with a nonzero y in velocity ball1 = new PoolBall(2, 2, 2, null, null, 0); ball2 = new PoolBall(2 + Math.sqrt(PoolBall.DIAMETER), 2 + Math.sqrt(PoolBall.DIAMETER), 2, null, null, 0); - ball1.velocity.setLocation(0, 0, 0); - ball2.velocity.setLocation(-Math.sqrt(2), -Math.sqrt(2), 0); + ball1.velocity.set(0, 0, 0); + ball2.velocity.set(-Math.sqrt(2), -Math.sqrt(2), 0); Physics.handleCollision(ball1, ball2); @@ -126,8 +127,8 @@ public void testHandleCollision() { ball1 = new PoolBall(5, 4, 3, null, null, 0); ball2 = new PoolBall(6, 5, 2, null, null, 0); - ball1.velocity.setLocation(2, 2, 2); - ball2.velocity.setLocation(0, 0, 0); + ball1.velocity.set(2, 2, 2); + ball2.velocity.set(0, 0, 0); Physics.handleCollision(ball2, ball1); @@ -152,7 +153,7 @@ public void testHandleCollision() { // What if the balls are going in the same direction, // but still getting farther apart? - ball2.velocity.setLocation(1.3, 1.3, 2.0); + ball2.velocity.set(1.3, 1.3, 2.0); Physics.handleCollision(ball1, ball2); diff --git a/Pool3D/test/com/brianmccutchon/pool3d/PoolBallTest.java b/Pool3D/test/com/brianmccutchon/pool3d/PoolBallTest.java index b31b71c..5c63684 100644 --- a/Pool3D/test/com/brianmccutchon/pool3d/PoolBallTest.java +++ b/Pool3D/test/com/brianmccutchon/pool3d/PoolBallTest.java @@ -1,29 +1,15 @@ package com.brianmccutchon.pool3d; import static org.junit.Assert.*; -import geometry.Point3D; -import geometry.Tools3D; -import geometry.Triangle3D; import java.util.Iterator; +import javax.vecmath.Point3d; + import org.junit.Test; public class PoolBallTest { - @Test - public void testWireframe() { - PoolBall ball = new PoolBall(0, 0, 0, null, null, 0); - for (Triangle3D tri : ball.getTriangles()) { - assertEquals(-1, Math.signum(Tools3D.sigmaVal(tri.points[0], - tri.points[1], tri.points[2], new Point3D(0, 0, 0))), 0.0); - } - - assertEquals((int) (5 * Math.pow(4, PoolBall.SMOOTHNESS)), - new PoolBall(0, 0, 0, null, null, 0) - .getTriangles().size()); - } - @Test public void testRack() { PoolBall[] balls = PoolBall.rack(); @@ -37,7 +23,7 @@ public void testRack() { assertEquals(counter++, b.ballNum); } - assertEquals(new Point3D(0, 0, 0), balls[8].center); + assertEquals(new Point3d(0, 0, 0), balls[8].center); for (int i : range(0, balls.length-1)) for (int j : range(i+1, balls.length-1))