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.Map;
import shapes.Line;
import shapes.Shape;
import shapes.Vector2;
import java.util.ArrayList;
@ -41,7 +42,7 @@ public class Camera {
this(DEFAULT_FOCAL_LENGTH, object);
}
public List<Line> draw(WorldObject worldObject) {
public List<Shape> draw(WorldObject worldObject) {
return getFrame(getProjectedVertices(worldObject), worldObject.getVertexPath());
}
@ -113,8 +114,8 @@ public class Camera {
);
}
public List<Line> getFrame(Map<Vector3, Vector2> projectionMap, List<Vector3> vertexPath) {
List<Line> lines = new ArrayList<>();
public List<Shape> getFrame(Map<Vector3, Vector2> projectionMap, List<Vector3> vertexPath) {
List<Shape> lines = new ArrayList<>();
for (int i = 0; i < vertexPath.size(); i += 2) {
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.Objects;
import shapes.Vector2;
public final class Vector3 {
@ -111,18 +110,16 @@ public final class Vector3 {
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() {
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.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jgrapht.Graph;
@ -18,27 +18,27 @@ import org.jgrapht.graph.DefaultWeightedEdge;
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.
private List<Vector3> vertexPath;
private Vector3 position;
private Vector3 rotation;
private WorldObject(List<Vector3> vertices, List<Vector3> vertexPath, Vector3 position, Vector3 rotation) {
this.vertices = vertices;
private WorldObject(List<Vector3> objVertices, List<Vector3> vertexPath, Vector3 position, Vector3 rotation) {
this.objVertices = objVertices;
this.position = position;
this.rotation = rotation;
this.vertexPath = vertexPath;
}
private WorldObject(List<Vector3> vertices, Vector3 position, Vector3 rotation) {
this(vertices, new ArrayList<>(), position, rotation);
private WorldObject(List<Vector3> objVertices, Vector3 position, Vector3 rotation) {
this(objVertices, new ArrayList<>(), position, rotation);
}
public WorldObject(String filename, Vector3 position, Vector3 rotation) throws IOException {
this(new ArrayList<>(), position, rotation);
this.vertexPath = getDrawPath(loadFromFile(filename));
getDrawPath(loadFromFile(filename));
}
public WorldObject(String filename) throws IOException {
@ -55,23 +55,22 @@ public class WorldObject {
return newVertices;
}
public List<Vector3> getDrawPath(List<Integer> edgeData) {
public void getDrawPath(Set<Line3D> edges) {
Graph<Vector3, DefaultWeightedEdge> graph = new DefaultUndirectedWeightedGraph<>(
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
// length of the line as this is directly proportional to draw time.
for (int i = 0; i < edgeData.size(); i += 2) {
Vector3 start = vertices.get(edgeData.get(i));
Vector3 end = vertices.get(edgeData.get(i + 1));
graph.addVertex(start);
graph.addVertex(end);
for (Line3D edge : edges) {
graph.addVertex(edge.getStart());
graph.addVertex(edge.getEnd());
DefaultWeightedEdge edge = new DefaultWeightedEdge();
graph.addEdge(start, end, edge);
graph.setEdgeWeight(edge, start.distance(end));
DefaultWeightedEdge weightedEdge = new DefaultWeightedEdge();
graph.addEdge(edge.getStart(), edge.getEnd(), weightedEdge);
graph.addEdge(edge.getStart(), edge.getEnd());
graph.setEdgeWeight(weightedEdge, edge.getStart().distance(edge.getEnd()));
}
ConnectivityInspector<Vector3, DefaultWeightedEdge> inspector = new ConnectivityInspector<>(
@ -82,22 +81,13 @@ public class WorldObject {
for (Set<Vector3> vertices : inspector.connectedSets()) {
AsSubgraph<Vector3, DefaultWeightedEdge> subgraph = new AsSubgraph<>(graph, vertices);
ChinesePostman<Vector3, DefaultWeightedEdge> cp = new ChinesePostman<>();
Collection<DefaultWeightedEdge> path;
List<Vector3> path = cp.getCPPSolution(subgraph).getVertexList();
try {
path = cp.getCPPSolution(subgraph).getEdgeList();
} catch (Exception e) {
// Safety in case getCPPSolution fails.
path = subgraph.edgeSet();
}
for (DefaultWeightedEdge edge : path) {
vertexPath.add(subgraph.getEdgeSource(edge));
vertexPath.add(subgraph.getEdgeTarget(edge));
for (int i = 1; i < path.size(); i++) {
vertexPath.add(path.get(i - 1));
vertexPath.add(path.get(i));
}
}
return vertexPath;
}
public void rotate(Vector3 theta) {
@ -119,22 +109,22 @@ public class WorldObject {
public List<Vector3> getVertices() {
List<Vector3> newVertices = new ArrayList<>();
for (Vector3 vertex : vertices) {
for (Vector3 vertex : objVertices) {
newVertices.add(vertex.rotate(rotation).add(position));
}
return newVertices;
}
private List<Integer> loadFromFile(String filename) throws IOException {
private Set<Line3D> loadFromFile(String filename) throws IOException {
InputStream in = new FileInputStream(filename);
final IOBJParser parser = new OBJParser();
final OBJModel model = parser.parse(in);
List<Integer> edgeData = new ArrayList<>();
Set<Line3D> edges = new HashSet<>();
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()) {
@ -143,17 +133,19 @@ public class WorldObject {
List<OBJDataReference> references = face.getReferences();
for (int i = 0; i < references.size(); i++) {
edgeData.add(references.get(i).vertexIndex);
edgeData.add(references.get((i + 1) % references.size()).vertexIndex);
edges.add(new Line3D(
objVertices.get(references.get(i).vertexIndex),
objVertices.get(references.get((i + 1) % references.size()).vertexIndex)
));
}
}
}
}
return edgeData;
return edges;
}
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);
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;
}
}

Wyświetl plik

@ -1,16 +1,7 @@
package shapes;
import java.util.ArrayList;
import java.util.Collection;
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.
public class Shapes {
@ -98,49 +89,4 @@ public class Shapes {
.reduce(Double::sum)
.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;
}
}