From ca9382d2726b46b9f2d1542e60003201c060a5d9 Mon Sep 17 00:00:00 2001 From: James Ball Date: Tue, 11 Feb 2020 21:00:47 +0000 Subject: [PATCH] Implemented 3D code from ICHack project --- src/audio/AudioClient.java | 18 ++++-- src/audio/AudioPlayer.java | 8 +-- src/engine/Camera.java | 43 +++++++++++++ src/engine/Main.java | 18 ++++++ src/engine/Mesh.java | 77 +++++++++++++++++++++++ src/engine/Renderer.java | 26 ++++++++ src/engine/Vector3.java | 79 ++++++++++++++++++++++++ src/engine/WorldObject.java | 38 ++++++++++++ src/graphs/Graph.java | 4 +- src/graphs/Node.java | 8 +-- src/shapes/Ellipse.java | 10 +-- src/shapes/Line.java | 18 +++--- src/shapes/Shape.java | 2 +- src/shapes/Shapes.java | 18 +++--- src/shapes/{Vector.java => Vector2.java} | 34 +++++----- 15 files changed, 345 insertions(+), 56 deletions(-) create mode 100644 src/engine/Camera.java create mode 100644 src/engine/Main.java create mode 100644 src/engine/Mesh.java create mode 100644 src/engine/Renderer.java create mode 100644 src/engine/Vector3.java create mode 100644 src/engine/WorldObject.java rename src/shapes/{Vector.java => Vector2.java} (57%) diff --git a/src/audio/AudioClient.java b/src/audio/AudioClient.java index a2efaf7..2e20657 100644 --- a/src/audio/AudioClient.java +++ b/src/audio/AudioClient.java @@ -2,7 +2,7 @@ package audio; import shapes.Ellipse; import shapes.Shapes; -import shapes.Vector; +import shapes.Vector2; public class AudioClient { private static final int SAMPLE_RATE = 192000; @@ -13,12 +13,20 @@ public class AudioClient { AudioPlayer player = new AudioPlayer(); AudioPlayer.FORMAT = AudioPlayer.defaultFormat(SAMPLE_RATE); - AudioPlayer.addShapes(Shapes.generatePolygram(5, 3, 0.5, 60)); +// AudioPlayer.addShapes(Shapes.generatePolygram(5, 3, 0.5, 60)); //AudioPlayer.addShapes(Shapes.generatePolygon(500, 0.5, 10)); - AudioPlayer.addShape(new Ellipse(0.5, 1)); +// AudioPlayer.addShape(new Ellipse(0.5, 1)); + +// AudioPlayer.setRotateSpeed(0.8); +// AudioPlayer.setTranslation(2, new Vector(0.5, 0.5)); + + AudioPlayer.addShapes(Shapes.generatePolygon(4, new Vector2(0.25, 0.25))); + AudioPlayer.addShapes(Shapes.generatePolygon(4, new Vector2(0.5, 0.5))); + AudioPlayer.addShapes(Shapes.generatePolygon(4, new Vector2(-0.8, -0.8))); + for (int i = 0; i < 10; i++) { + AudioPlayer.addShape(new Ellipse(i / 10d, i / 10d)); + } - AudioPlayer.setRotateSpeed(0.8); - AudioPlayer.setTranslation(2, new Vector(0.5, 0.5)); AudioPlayer.setScale(0.5); player.start(); diff --git a/src/audio/AudioPlayer.java b/src/audio/AudioPlayer.java index 6252bf5..7a4eb11 100644 --- a/src/audio/AudioPlayer.java +++ b/src/audio/AudioPlayer.java @@ -2,7 +2,7 @@ package audio; import com.xtaudio.xt.*; import shapes.Shape; -import shapes.Vector; +import shapes.Vector2; import java.util.ArrayList; import java.util.List; @@ -17,7 +17,7 @@ public class AudioPlayer extends Thread { public static XtFormat FORMAT; private static double TRANSLATE_SPEED = 0; - private static Vector TRANSLATE_VECTOR; + private static Vector2 TRANSLATE_VECTOR; private static final int TRANSLATE_PHASE_INDEX = 0; private static double ROTATE_SPEED = 0.4; private static final int ROTATE_PHASE_INDEX = 1; @@ -81,7 +81,7 @@ public class AudioPlayer extends Thread { } private static Shape translate(Shape shape, double sampleRate) { - if (TRANSLATE_SPEED != 0 && !TRANSLATE_VECTOR.equals(new Vector())) { + if (TRANSLATE_SPEED != 0 && !TRANSLATE_VECTOR.equals(new Vector2())) { return shape.translate(TRANSLATE_VECTOR.scale( Math.sin(nextTheta(sampleRate, TRANSLATE_SPEED, ROTATE_PHASE_INDEX)) )); @@ -110,7 +110,7 @@ public class AudioPlayer extends Thread { AudioPlayer.ROTATE_SPEED = speed; } - public static void setTranslation(double speed, Vector translation) { + public static void setTranslation(double speed, Vector2 translation) { AudioPlayer.TRANSLATE_SPEED = speed; AudioPlayer.TRANSLATE_VECTOR = translation; } diff --git a/src/engine/Camera.java b/src/engine/Camera.java new file mode 100644 index 0000000..78c0078 --- /dev/null +++ b/src/engine/Camera.java @@ -0,0 +1,43 @@ +package engine; + +import shapes.Line; +import shapes.Vector2; + +import java.util.ArrayList; +import java.util.List; + +public class Camera extends Renderer{ + + // position at 0,0,0,0 + // rotation towards positive z axis + + private double focalLength; + private double clipping = 0.001; + private Vector3 position; + private double fov; + + public Camera() { + this.focalLength = 0.6; + this.position = new Vector3(0,0,-2); + this.fov = 60; + } + + public List draw(WorldObject worldObject) { + List vertices = new ArrayList<>(); + for(Vector3 vertex : worldObject.getVertices()) { + vertices.add(this.project(vertex)); + } + + return this.getFrame(vertices, worldObject.getEdgeData()); + } + + private Vector2 project(Vector3 vertex) { + if(vertex.getZ() - this.position.getZ() < clipping) { + return new Vector2(0, 0); + } + return new Vector2( + vertex.getX() * focalLength / (vertex.getZ() - this.position.getZ()) + this.position.getX(), + vertex.getY() * focalLength / (vertex.getZ() - this.position.getZ()) + this.position.getY() + ); + } +} diff --git a/src/engine/Main.java b/src/engine/Main.java new file mode 100644 index 0000000..a0172a1 --- /dev/null +++ b/src/engine/Main.java @@ -0,0 +1,18 @@ +package engine; + +public class Main { + public static void main(String[] args) throws InterruptedException { + Camera camera = new Camera(); + + WorldObject cube = new WorldObject("resources/machine.obj", new Vector3(0,0,0), new Vector3()); + while(true) { + camera.draw(cube); + cube.rotate(new Vector3( + 0, + Math.PI / 100, + 0 + )); + Thread.sleep(1000/30); + } + } +} diff --git a/src/engine/Mesh.java b/src/engine/Mesh.java new file mode 100644 index 0000000..d5441a1 --- /dev/null +++ b/src/engine/Mesh.java @@ -0,0 +1,77 @@ +package engine; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.MatchResult; +import java.util.stream.Collectors; + +public class Mesh { + + private static final String VERTEX_PATTERN = "(?m)^v .*"; + private static final String FACE_PATTERN = "(?m)^f .*"; + + private List vertices; + private List edgeData; + + public Mesh() { + this.vertices = new ArrayList<>(); + this.edgeData = new ArrayList<>(); + } + + public Mesh(List vertices, List edgeData) { + this.vertices = vertices; + this.edgeData = edgeData; + } + + public List getVertices() { + return vertices; + } + + public List getEdgeData() { + return edgeData; + } + + public static Mesh loadFromFile(String filename) { + Scanner sc; + try { + sc = new Scanner(new File(filename)); + } catch (Exception e) { + System.err.println("Cannot load mesh data from: " + filename); + return new Mesh(); + } + // load vertices + List vertices = + sc.findAll(VERTEX_PATTERN) + .map(s -> parseVertex(s.group(0))) + .collect(Collectors.toList()); + // load edge data + List edgeData = new ArrayList<>(); + for(MatchResult result : sc.findAll(FACE_PATTERN).collect(Collectors.toList())) { + List indices = parseFace(result.group(0)); + for(int i = 0; i < indices.size(); i++ ) { + edgeData.add(indices.get(i)); + edgeData.add(indices.get((i + 1) % indices.size())); + } + } + return new Mesh(vertices, edgeData); + } + + private static Vector3 parseVertex(String data) { + String[] coords = data.split(" "); + double x = Double.parseDouble(coords[1]); + double y = Double.parseDouble(coords[2]); + double z = Double.parseDouble(coords[3]); + return new Vector3(x, y, z); + } + + private static List parseFace(String data) { + List indices = new ArrayList<>(); + String[] datas = data.split(" "); + for(int i = 1; i < datas.length; i++) { + indices.add(Integer.parseInt(datas[i].split("/")[0]) - 1); + } + return indices; + } +} diff --git a/src/engine/Renderer.java b/src/engine/Renderer.java new file mode 100644 index 0000000..286a594 --- /dev/null +++ b/src/engine/Renderer.java @@ -0,0 +1,26 @@ +package engine; + +import shapes.Line; +import shapes.Vector2; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Renderer { + public List getFrame(List vertices, List connections) { + List lines = new ArrayList<>(); + + for (int i = 0; i < connections.size(); i+=2) { + Vector2 start = vertices.get(connections.get(i)); + Vector2 end = vertices.get(connections.get(i+1)); + double x1 = start.getX(); + double y1 = start.getY(); + double x2 = end.getX(); + double y2 = end.getY(); + + lines.add(new Line(x1, y1, x2, y2)); + } + + return lines; + } +} diff --git a/src/engine/Vector3.java b/src/engine/Vector3.java new file mode 100644 index 0000000..6d87b38 --- /dev/null +++ b/src/engine/Vector3.java @@ -0,0 +1,79 @@ +package engine; + +import java.util.List; + +public class Vector3 { + private double x, y, z; + + public Vector3() { + this.x = 0; + this.y = 0; + this.z = 0; + } + + public Vector3(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + public Vector3 add(Vector3 other) { + return new Vector3( + this.getX() + other.getX(), + this.getY() + other.getY(), + this.getZ() + other.getZ() + ); + } + + public Vector3 scale(double scalar) { + return new Vector3( + this.getX() * scalar, + this.getY() * scalar, + this.getZ() * scalar + ); + } + + public Vector3 rotateX(double delta) { + return new Vector3( + getX(), + Math.cos(delta) * getY() - Math.sin(delta) * getZ(), + Math.sin(delta) * getY() + Math.cos(delta) * getZ() + ); + } + + public Vector3 rotateY(double delta) { + return new Vector3( + Math.cos(delta) * getX() + Math.sin(delta) * getZ(), + getY(), + -Math.sin(delta) * getX() + Math.cos(delta) * getZ() + ); + } + + public Vector3 rotateZ(double delta) { + return new Vector3( + Math.cos(delta) * getX() - Math.sin(delta) * getY(), + Math.sin(delta) * getX() + Math.cos(delta) * getY(), + getZ() + ); + } + + public static Vector3 meanPoint(List points) { + Vector3 mean = new Vector3(); + for(Vector3 point : points) { + mean = mean.add(point); + } + return mean.scale(1f / (points.size())); + } +} diff --git a/src/engine/WorldObject.java b/src/engine/WorldObject.java new file mode 100644 index 0000000..180c8fa --- /dev/null +++ b/src/engine/WorldObject.java @@ -0,0 +1,38 @@ +package engine; + +import java.util.ArrayList; +import java.util.List; + +public class WorldObject { + + private Mesh mesh; + private Vector3 position; + private Vector3 rotation; + + public WorldObject(String filename, Vector3 position, Vector3 rotation) { + this.mesh = Mesh.loadFromFile(filename); + this.position = position; + this.rotation = rotation; + } + + public void rotate(Vector3 theta) { + this.rotation = this.rotation.add(theta); + } + + public List getVertices() { + List vertices = new ArrayList<>(); + for(Vector3 vertex : mesh.getVertices()) { + vertices.add( + vertex + .rotateX(this.rotation.getX()) + .rotateY(this.rotation.getY()) + .rotateZ(this.rotation.getY()) + .add(this.position)); + } + return vertices; + } + + public List getEdgeData() { + return mesh.getEdgeData(); + } +} diff --git a/src/graphs/Graph.java b/src/graphs/Graph.java index 49c2789..7968313 100644 --- a/src/graphs/Graph.java +++ b/src/graphs/Graph.java @@ -1,7 +1,7 @@ package graphs; import shapes.Line; -import shapes.Vector; +import shapes.Vector2; import java.util.HashMap; import java.util.List; @@ -10,7 +10,7 @@ import java.util.Map; // TODO: Implement Chinese postman solving. public class Graph { - private Map nodes; + private Map nodes; public Graph(List lines) { this.nodes = new HashMap<>(); diff --git a/src/graphs/Node.java b/src/graphs/Node.java index 0880528..7ac8d21 100644 --- a/src/graphs/Node.java +++ b/src/graphs/Node.java @@ -1,23 +1,23 @@ package graphs; -import shapes.Vector; +import shapes.Vector2; import java.util.ArrayList; import java.util.List; import java.util.Objects; public class Node { - private Vector location; + private Vector2 location; private List adjacentNodes; private List adjacentWeights; - public Node(Vector location) { + public Node(Vector2 location) { this.location = location; this.adjacentNodes = new ArrayList<>(); this.adjacentWeights = new ArrayList<>(); } - public Vector getLocation() { + public Vector2 getLocation() { return location; } diff --git a/src/shapes/Ellipse.java b/src/shapes/Ellipse.java index 87a0d16..e9d460e 100644 --- a/src/shapes/Ellipse.java +++ b/src/shapes/Ellipse.java @@ -4,9 +4,9 @@ public class Ellipse extends Shape { private final double a; private final double b; private final double rotation; - private final Vector position; + private final Vector2 position; - public Ellipse(double a, double b, double weight, double rotation, Vector position) { + public Ellipse(double a, double b, double weight, double rotation, Vector2 position) { this.a = a; this.b = b; this.weight = weight; @@ -16,12 +16,12 @@ public class Ellipse extends Shape { this.length = 2 * Math.PI * Math.sqrt((a * a + b * b) / 2); } - public Ellipse(double a, double b, Vector position) { + public Ellipse(double a, double b, Vector2 position) { this(a, b, 100, 0, position); } public Ellipse(double a, double b) { - this(a, b, 100, 0, new Vector()); + this(a, b, 100, 0, new Vector2()); } @Override @@ -53,7 +53,7 @@ public class Ellipse extends Shape { } @Override - public Shape translate(Vector vector) { + public Shape translate(Vector2 vector) { return new Ellipse(a, b, weight, rotation, position.add(vector)); } } diff --git a/src/shapes/Line.java b/src/shapes/Line.java index d001d1a..4761e39 100644 --- a/src/shapes/Line.java +++ b/src/shapes/Line.java @@ -1,28 +1,28 @@ package shapes; public class Line extends Shape { - private final Vector a; - private final Vector b; + private final Vector2 a; + private final Vector2 b; public static final double DEFAULT_WEIGHT = 100; - public Line(Vector a, Vector b, double weight) { + public Line(Vector2 a, Vector2 b, double weight) { this.a = a; this.b = b; this.weight = weight; this.length = calculateLength(); } - public Line(Vector a, Vector b) { + public Line(Vector2 a, Vector2 b) { this(a, b, DEFAULT_WEIGHT); } public Line(double x1, double y1, double x2, double y2, double weight) { - this(new Vector(x1, y1), new Vector(x2, y2), weight); + this(new Vector2(x1, y1), new Vector2(x2, y2), weight); } public Line(double x1, double y1, double x2, double y2) { - this(new Vector(x1, y1), new Vector(x2, y2)); + this(new Vector2(x1, y1), new Vector2(x2, y2)); } private double calculateLength() { @@ -35,7 +35,7 @@ public class Line extends Shape { } @Override - public Line translate(Vector vector) { + public Line translate(Vector2 vector) { return new Line(getA().add(vector), getB().add(vector)); } @@ -58,11 +58,11 @@ public class Line extends Shape { return (float) (getY1() + (getY2() - getY1()) * drawingProgress); } - public Vector getA() { + public Vector2 getA() { return a; } - public Vector getB() { + public Vector2 getB() { return b; } diff --git a/src/shapes/Shape.java b/src/shapes/Shape.java index a8a0577..c8536de 100644 --- a/src/shapes/Shape.java +++ b/src/shapes/Shape.java @@ -8,7 +8,7 @@ public abstract class Shape { public abstract float nextY(double drawingProgress); public abstract Shape rotate(double theta); public abstract Shape scale(double factor); - public abstract Shape translate(Vector vector); + public abstract Shape translate(Vector2 vector); public double getWeight() { return weight; diff --git a/src/shapes/Shapes.java b/src/shapes/Shapes.java index 597311f..c0f7c8e 100644 --- a/src/shapes/Shapes.java +++ b/src/shapes/Shapes.java @@ -4,11 +4,11 @@ import java.util.ArrayList; import java.util.List; public class Shapes { - public static List generatePolygram(int sides, int angleJump, Vector start, double weight) { + public static List generatePolygram(int sides, int angleJump, Vector2 start, double weight) { List polygon = new ArrayList<>(); double theta = angleJump * 2 * Math.PI / sides; - Vector rotated = start.rotate(theta); + Vector2 rotated = start.rotate(theta); polygon.add(new Line(start, rotated, weight)); while (!rotated.equals(start)) { @@ -20,32 +20,32 @@ public class Shapes { return polygon; } - public static List generatePolygram(int sides, int angleJump, Vector start) { + public static List generatePolygram(int sides, int angleJump, Vector2 start) { return generatePolygram(sides, angleJump, start, Line.DEFAULT_WEIGHT); } public static List generatePolygram(int sides, int angleJump, double scale, double weight) { - return generatePolygram(sides, angleJump, new Vector(scale, scale), weight); + return generatePolygram(sides, angleJump, new Vector2(scale, scale), weight); } public static List generatePolygram(int sides, int angleJump, double scale) { - return generatePolygram(sides, angleJump, new Vector(scale, scale)); + return generatePolygram(sides, angleJump, new Vector2(scale, scale)); } - public static List generatePolygon(int sides, Vector start, double weight) { + public static List generatePolygon(int sides, Vector2 start, double weight) { return generatePolygram(sides, 1, start, weight); } - public static List generatePolygon(int sides, Vector start) { + public static List generatePolygon(int sides, Vector2 start) { return generatePolygram(sides, 1, start); } public static List generatePolygon(int sides, double scale, double weight) { - return generatePolygon(sides, new Vector(scale, scale), weight); + return generatePolygon(sides, new Vector2(scale, scale), weight); } public static List generatePolygon(int sides, double scale) { - return generatePolygon(sides, new Vector(scale, scale)); + return generatePolygon(sides, new Vector2(scale, scale)); } public static List cleanupLines(List lines) { diff --git a/src/shapes/Vector.java b/src/shapes/Vector2.java similarity index 57% rename from src/shapes/Vector.java rename to src/shapes/Vector2.java index 46fd5f9..3d6c90e 100644 --- a/src/shapes/Vector.java +++ b/src/shapes/Vector2.java @@ -2,18 +2,18 @@ package shapes; import java.util.Objects; -public class Vector { +public class Vector2 { private final double x; private final double y; private static final double EPSILON = 0.001; - public Vector(double x, double y) { + public Vector2(double x, double y) { this.x = x; this.y = y; } - public Vector() { + public Vector2() { this(0, 0); } @@ -25,28 +25,28 @@ public class Vector { return y; } - public Vector setX(double x) { - return new Vector(x, this.y); + public Vector2 setX(double x) { + return new Vector2(x, this.y); } - public Vector setY(double y) { - return new Vector(this.x, y); + public Vector2 setY(double y) { + return new Vector2(this.x, y); } - public Vector add(Vector vector) { - return new Vector(getX() + vector.getX(), getY() + vector.getY()); + public Vector2 add(Vector2 vector) { + return new Vector2(getX() + vector.getX(), getY() + vector.getY()); } - public Vector scale(double factor) { - return new Vector(getX() * factor, getY() * factor); + public Vector2 scale(double factor) { + return new Vector2(getX() * factor, getY() * factor); } @Override public boolean equals(Object obj) { if (obj == this) { return true; - } else if (obj instanceof Vector) { - Vector vector = (Vector) obj; + } else if (obj instanceof Vector2) { + Vector2 vector = (Vector2) obj; return approxEquals(vector.getX(), getX()) && approxEquals(vector.getY(), getY()); } else { @@ -63,12 +63,12 @@ public class Vector { return Math.round(d * 1000) / 1000d; } - public Vector copy() { - return new Vector(x, y); + public Vector2 copy() { + return new Vector2(x, y); } - public Vector rotate(double theta) { - Vector vector = setX(getX() * Math.cos(theta) - getY() * Math.sin(theta)); + public Vector2 rotate(double theta) { + Vector2 vector = setX(getX() * Math.cos(theta) - getY() * Math.sin(theta)); vector = vector.setY(getX() * Math.sin(theta) + getY() * Math.cos(theta)); return vector;