kopia lustrzana https://github.com/jameshball/osci-render
Complete reduction of chinese postman solving, massively improving pre-rendering and cleaning up output
rodzic
c46afe24c9
commit
6d0e2f8217
|
@ -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(
|
||||||
|
|
|
@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue