Complete reduction of chinese postman solving, massively improving pre-rendering and cleaning up output

pull/35/head
James Ball 2020-11-09 19:07:32 +00:00
rodzic c46afe24c9
commit 6d0e2f8217
6 zmienionych plików z 92 dodań i 124 usunięć

Wyświetl plik

@ -3,6 +3,7 @@ package engine;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import shapes.Line; import shapes.Line;
import shapes.Shape;
import shapes.Vector2; import shapes.Vector2;
import java.util.ArrayList; import java.util.ArrayList;
@ -41,7 +42,7 @@ public class Camera {
this(DEFAULT_FOCAL_LENGTH, object); this(DEFAULT_FOCAL_LENGTH, object);
} }
public List<Line> draw(WorldObject worldObject) { public List<Shape> draw(WorldObject worldObject) {
return getFrame(getProjectedVertices(worldObject), worldObject.getVertexPath()); return getFrame(getProjectedVertices(worldObject), worldObject.getVertexPath());
} }
@ -113,8 +114,8 @@ public class Camera {
); );
} }
public List<Line> getFrame(Map<Vector3, Vector2> projectionMap, List<Vector3> vertexPath) { public List<Shape> getFrame(Map<Vector3, Vector2> projectionMap, List<Vector3> vertexPath) {
List<Line> lines = new ArrayList<>(); List<Shape> lines = new ArrayList<>();
for (int i = 0; i < vertexPath.size(); i += 2) { for (int i = 0; i < vertexPath.size(); i += 2) {
lines.add(new Line( lines.add(new Line(

Wyświetl plik

@ -0,0 +1,47 @@
package engine;
import java.util.Objects;
public class Line3D {
private final Vector3 start;
private final Vector3 end;
public Line3D(Vector3 start, Vector3 end) {
this.start = start;
this.end = end;
}
public Vector3 getStart() {
return start;
}
public Vector3 getEnd() {
return end;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Line3D line3D = (Line3D) o;
return Objects.equals(start, line3D.start) && Objects.equals(end, line3D.end);
}
@Override
public int hashCode() {
return Objects.hash(start, end);
}
@Override
public String toString() {
return "Line3D{" +
"start=" + start +
", end=" + end +
'}';
}
}

Wyświetl plik

@ -2,7 +2,6 @@ package engine;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import shapes.Vector2;
public final class Vector3 { public final class Vector3 {
@ -111,18 +110,16 @@ public final class Vector3 {
return Objects.hash(x, y, z); return Objects.hash(x, y, z);
} }
private static double round(double value, int places) {
if (places < 0) {
throw new IllegalArgumentException();
}
long factor = (long) Math.pow(10, places);
value *= factor;
return (double) Math.round(value) / factor;
}
public Vector3 clone() { public Vector3 clone() {
return new Vector3(x, y, z); return new Vector3(x, y, z);
} }
@Override
public String toString() {
return "Vector3{" +
"x=" + x +
", y=" + y +
", z=" + z +
'}';
}
} }

Wyświetl plik

@ -6,7 +6,7 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.jgrapht.Graph; import org.jgrapht.Graph;
@ -18,27 +18,27 @@ import org.jgrapht.graph.DefaultWeightedEdge;
public class WorldObject { public class WorldObject {
private final List<Vector3> vertices; private final List<Vector3> objVertices;
// These should be a path of vertices from the above vertex list. // These should be a path of vertices from the above vertex list.
private List<Vector3> vertexPath; private List<Vector3> vertexPath;
private Vector3 position; private Vector3 position;
private Vector3 rotation; private Vector3 rotation;
private WorldObject(List<Vector3> vertices, List<Vector3> vertexPath, Vector3 position, Vector3 rotation) { private WorldObject(List<Vector3> objVertices, List<Vector3> vertexPath, Vector3 position, Vector3 rotation) {
this.vertices = vertices; this.objVertices = objVertices;
this.position = position; this.position = position;
this.rotation = rotation; this.rotation = rotation;
this.vertexPath = vertexPath; this.vertexPath = vertexPath;
} }
private WorldObject(List<Vector3> vertices, Vector3 position, Vector3 rotation) { private WorldObject(List<Vector3> objVertices, Vector3 position, Vector3 rotation) {
this(vertices, new ArrayList<>(), position, rotation); this(objVertices, new ArrayList<>(), position, rotation);
} }
public WorldObject(String filename, Vector3 position, Vector3 rotation) throws IOException { public WorldObject(String filename, Vector3 position, Vector3 rotation) throws IOException {
this(new ArrayList<>(), position, rotation); this(new ArrayList<>(), position, rotation);
this.vertexPath = getDrawPath(loadFromFile(filename)); getDrawPath(loadFromFile(filename));
} }
public WorldObject(String filename) throws IOException { public WorldObject(String filename) throws IOException {
@ -55,23 +55,22 @@ public class WorldObject {
return newVertices; return newVertices;
} }
public List<Vector3> getDrawPath(List<Integer> edgeData) { public void getDrawPath(Set<Line3D> edges) {
Graph<Vector3, DefaultWeightedEdge> graph = new DefaultUndirectedWeightedGraph<>( Graph<Vector3, DefaultWeightedEdge> graph = new DefaultUndirectedWeightedGraph<>(
DefaultWeightedEdge.class); DefaultWeightedEdge.class);
List<Vector3> vertexPath = new ArrayList<>(); vertexPath = new ArrayList<>();
// Add all lines in frame to graph as vertices and edges. Edge weight is determined by the // Add all lines in frame to graph as vertices and edges. Edge weight is determined by the
// length of the line as this is directly proportional to draw time. // length of the line as this is directly proportional to draw time.
for (int i = 0; i < edgeData.size(); i += 2) { for (Line3D edge : edges) {
Vector3 start = vertices.get(edgeData.get(i)); graph.addVertex(edge.getStart());
Vector3 end = vertices.get(edgeData.get(i + 1)); graph.addVertex(edge.getEnd());
graph.addVertex(start);
graph.addVertex(end);
DefaultWeightedEdge edge = new DefaultWeightedEdge(); DefaultWeightedEdge weightedEdge = new DefaultWeightedEdge();
graph.addEdge(start, end, edge); graph.addEdge(edge.getStart(), edge.getEnd(), weightedEdge);
graph.setEdgeWeight(edge, start.distance(end)); graph.addEdge(edge.getStart(), edge.getEnd());
graph.setEdgeWeight(weightedEdge, edge.getStart().distance(edge.getEnd()));
} }
ConnectivityInspector<Vector3, DefaultWeightedEdge> inspector = new ConnectivityInspector<>( ConnectivityInspector<Vector3, DefaultWeightedEdge> inspector = new ConnectivityInspector<>(
@ -82,22 +81,13 @@ public class WorldObject {
for (Set<Vector3> vertices : inspector.connectedSets()) { for (Set<Vector3> vertices : inspector.connectedSets()) {
AsSubgraph<Vector3, DefaultWeightedEdge> subgraph = new AsSubgraph<>(graph, vertices); AsSubgraph<Vector3, DefaultWeightedEdge> subgraph = new AsSubgraph<>(graph, vertices);
ChinesePostman<Vector3, DefaultWeightedEdge> cp = new ChinesePostman<>(); ChinesePostman<Vector3, DefaultWeightedEdge> cp = new ChinesePostman<>();
Collection<DefaultWeightedEdge> path; List<Vector3> path = cp.getCPPSolution(subgraph).getVertexList();
try { for (int i = 1; i < path.size(); i++) {
path = cp.getCPPSolution(subgraph).getEdgeList(); vertexPath.add(path.get(i - 1));
} catch (Exception e) { vertexPath.add(path.get(i));
// Safety in case getCPPSolution fails.
path = subgraph.edgeSet();
}
for (DefaultWeightedEdge edge : path) {
vertexPath.add(subgraph.getEdgeSource(edge));
vertexPath.add(subgraph.getEdgeTarget(edge));
} }
} }
return vertexPath;
} }
public void rotate(Vector3 theta) { public void rotate(Vector3 theta) {
@ -119,22 +109,22 @@ public class WorldObject {
public List<Vector3> getVertices() { public List<Vector3> getVertices() {
List<Vector3> newVertices = new ArrayList<>(); List<Vector3> newVertices = new ArrayList<>();
for (Vector3 vertex : vertices) { for (Vector3 vertex : objVertices) {
newVertices.add(vertex.rotate(rotation).add(position)); newVertices.add(vertex.rotate(rotation).add(position));
} }
return newVertices; return newVertices;
} }
private List<Integer> loadFromFile(String filename) throws IOException { private Set<Line3D> loadFromFile(String filename) throws IOException {
InputStream in = new FileInputStream(filename); InputStream in = new FileInputStream(filename);
final IOBJParser parser = new OBJParser(); final IOBJParser parser = new OBJParser();
final OBJModel model = parser.parse(in); final OBJModel model = parser.parse(in);
List<Integer> edgeData = new ArrayList<>(); Set<Line3D> edges = new HashSet<>();
for (OBJVertex vertex : model.getVertices()) { for (OBJVertex vertex : model.getVertices()) {
vertices.add(new Vector3(vertex.x, vertex.y, vertex.z)); objVertices.add(new Vector3(vertex.x, vertex.y, vertex.z));
} }
for (OBJObject object : model.getObjects()) { for (OBJObject object : model.getObjects()) {
@ -143,17 +133,19 @@ public class WorldObject {
List<OBJDataReference> references = face.getReferences(); List<OBJDataReference> references = face.getReferences();
for (int i = 0; i < references.size(); i++) { for (int i = 0; i < references.size(); i++) {
edgeData.add(references.get(i).vertexIndex); edges.add(new Line3D(
edgeData.add(references.get((i + 1) % references.size()).vertexIndex); objVertices.get(references.get(i).vertexIndex),
objVertices.get(references.get((i + 1) % references.size()).vertexIndex)
));
} }
} }
} }
} }
return edgeData; return edges;
} }
public WorldObject clone() { public WorldObject clone() {
return new WorldObject(new ArrayList<>(vertices), new ArrayList<>(vertexPath), position, rotation); return new WorldObject(new ArrayList<>(objVertices), new ArrayList<>(vertexPath), position, rotation);
} }
} }

Wyświetl plik

@ -71,25 +71,10 @@ public class ObjParser extends FileParser {
int numFrames = (int) (2 * Math.PI / OBJ_ROTATE_SPEED); int numFrames = (int) (2 * Math.PI / OBJ_ROTATE_SPEED);
for (int i = 0; i < numFrames; i++) { for (int i = 0; i < numFrames; i++) {
preRenderedFrames.add(new ArrayList<>()); object.rotate(rotation);
preRenderedFrames.add(camera.draw(object));
} }
AtomicInteger renderedFrames = new AtomicInteger();
// pre-renders the WorldObject in parallel
IntStream.range(0, numFrames).parallel().forEach((frameNum) -> {
WorldObject clone = object.clone();
clone.rotate(rotation.scale(frameNum));
// Finds all lines to draw the object from the camera's view and then 'sorts' them by finding
// a hamiltonian path, which dramatically helps with rendering a clean image.
preRenderedFrames.set(frameNum, Shapes.sortLines(camera.draw(clone)));
int numRendered = renderedFrames.getAndIncrement();
if (numRendered % 50 == 0) {
System.out.println("Rendered " + numRendered + " frames of " + (numFrames + 1) + " total");
}
});
return preRenderedFrames; return preRenderedFrames;
} }
} }

Wyświetl plik

@ -1,16 +1,7 @@
package shapes; package shapes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Set;
import org.jgrapht.Graph;
import org.jgrapht.alg.connectivity.ConnectivityInspector;
import org.jgrapht.alg.cycle.ChinesePostman;
import org.jgrapht.graph.AsSubgraph;
import org.jgrapht.graph.DefaultUndirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;
// Helper functions for the Shape interface. // Helper functions for the Shape interface.
public class Shapes { public class Shapes {
@ -98,49 +89,4 @@ public class Shapes {
.reduce(Double::sum) .reduce(Double::sum)
.orElse(0d); .orElse(0d);
} }
// Performs chinese postman on the input lines to get a path that will render cleanly on the
// oscilloscope.
// TODO: Speed up.
public static List<Shape> sortLines(List<Line> lines) {
Graph<Vector2, DefaultWeightedEdge> graph = new DefaultUndirectedWeightedGraph<>(
DefaultWeightedEdge.class);
// Add all lines in frame to graph as vertices and edges. Edge weight is determined by the
// length of the line as this is directly proportional to draw time.
for (Line line : lines) {
graph.addVertex(line.getA());
graph.addVertex(line.getB());
DefaultWeightedEdge edge = new DefaultWeightedEdge();
graph.addEdge(line.getA(), line.getB(), edge);
graph.setEdgeWeight(edge, line.length);
}
ConnectivityInspector<Vector2, DefaultWeightedEdge> inspector = new ConnectivityInspector<>(
graph);
List<Shape> sortedLines = new ArrayList<>();
// Chinese Postman can only be performed on connected graphs, so iterate over all connected
// sub-graphs.
for (Set<Vector2> vertices : inspector.connectedSets()) {
AsSubgraph<Vector2, DefaultWeightedEdge> subgraph = new AsSubgraph<>(graph, vertices);
ChinesePostman<Vector2, DefaultWeightedEdge> cp = new ChinesePostman<>();
Collection<DefaultWeightedEdge> path;
try {
path = cp.getCPPSolution(subgraph).getEdgeList();
} catch (Exception e) {
// Safety in case getCPPSolution fails.
path = subgraph.edgeSet();
}
for (DefaultWeightedEdge edge : path) {
sortedLines.add(new Line(subgraph.getEdgeSource(edge), subgraph.getEdgeTarget(edge)));
}
}
return sortedLines;
}
} }