diff --git a/src/main/java/labs/introtoprogramming/lab5/object/Box.java b/src/main/java/labs/introtoprogramming/lab5/object/Box.java index 88370e5..e95b3de 100644 --- a/src/main/java/labs/introtoprogramming/lab5/object/Box.java +++ b/src/main/java/labs/introtoprogramming/lab5/object/Box.java @@ -9,8 +9,8 @@ public class Box extends SceneObject { private static final double DELTA = 1e-6; private static final double DEFAULT_SIZE = 1; - private Vector3 lowerBounds; - private Vector3 upperBounds; + public Vector3 lowerBounds; + public Vector3 upperBounds; public Box(Transform transform, double size) { super(transform); @@ -20,6 +20,12 @@ public Box(Transform transform, double size) { lowerBounds = pos.add(bound.multiply(-1)); } + public Box(Transform transform, Vector3 lowerBounds, Vector3 upperBounds) { + super(transform); + this.lowerBounds = lowerBounds; + this.upperBounds = upperBounds; + } + public Box(Transform transform) { this(transform, DEFAULT_SIZE); } @@ -88,6 +94,11 @@ public boolean intersect(Ray ray) { return true; } + @Override + public Box getBoundary() { + return new Box(transform, lowerBounds, upperBounds); + } + private double axisDirection(double val) { return Math.abs(val) < DELTA ? val >= 0 ? 1 / DELTA : -1 / DELTA : 1 / val; } diff --git a/src/main/java/labs/introtoprogramming/lab5/object/OptimizedObject.java b/src/main/java/labs/introtoprogramming/lab5/object/OptimizedObject.java new file mode 100644 index 0000000..1415279 --- /dev/null +++ b/src/main/java/labs/introtoprogramming/lab5/object/OptimizedObject.java @@ -0,0 +1,27 @@ +package labs.introtoprogramming.lab5.object; + +import labs.introtoprogramming.lab5.geometry.Ray; +import labs.introtoprogramming.lab5.scene.SceneObject; +import labs.introtoprogramming.lab5.scene.Transform; +import labs.introtoprogramming.lab5.tree.KDTree; + +import java.util.List; + +public class OptimizedObject extends SceneObject { + private KDTree tree; + + public OptimizedObject(Transform transform, List objects) { + super(transform); + tree = new KDTree(objects); + } + + @Override + public boolean intersect(Ray ray) { + return tree.intersect(ray); + } + + @Override + public Box getBoundary() { + return tree.boundary(); + } +} diff --git a/src/main/java/labs/introtoprogramming/lab5/object/Sphere.java b/src/main/java/labs/introtoprogramming/lab5/object/Sphere.java index 529bf07..5576e72 100644 --- a/src/main/java/labs/introtoprogramming/lab5/object/Sphere.java +++ b/src/main/java/labs/introtoprogramming/lab5/object/Sphere.java @@ -48,4 +48,11 @@ public boolean intersect(Ray ray) { ray.setScale(intersection); return true; } + + @Override + public Box getBoundary() { + Vector3 pos = transform.position(); + Vector3 size = Vector3.ONE.multiply(radius); + return new Box(new Transform(), pos.subtract(size), pos.add(size)); + } } diff --git a/src/main/java/labs/introtoprogramming/lab5/object/Triangle.java b/src/main/java/labs/introtoprogramming/lab5/object/Triangle.java index dd5c6ae..042a7e2 100644 --- a/src/main/java/labs/introtoprogramming/lab5/object/Triangle.java +++ b/src/main/java/labs/introtoprogramming/lab5/object/Triangle.java @@ -56,7 +56,28 @@ public boolean intersect(Ray ray) { } double scale = v0v2.dotProduct(q) * invDet; + if (scale < 0) { + return false; + } ray.setScale(scale); return true; } + + @Override + public Box getBoundary() { + Vector3 v0 = transform.applyPoint(this.v0); + Vector3 v1 = transform.applyPoint(this.v1); + Vector3 v2 = transform.applyPoint(this.v2); + Vector3 lower = new Vector3( + Math.min(Math.min(v0.x, v1.x), v2.x), + Math.min(Math.min(v0.y, v1.y), v2.y), + Math.min(Math.min(v0.z, v1.z), v2.z) + ); + Vector3 upper = new Vector3( + Math.max(Math.max(v0.x, v1.x), v2.x), + Math.max(Math.max(v0.y, v1.y), v2.y), + Math.max(Math.max(v0.z, v1.z), v2.z) + ); + return new Box(new Transform(), lower, upper); + } } diff --git a/src/main/java/labs/introtoprogramming/lab5/scene/SceneObject.java b/src/main/java/labs/introtoprogramming/lab5/scene/SceneObject.java index 1af25e1..d2b3b78 100644 --- a/src/main/java/labs/introtoprogramming/lab5/scene/SceneObject.java +++ b/src/main/java/labs/introtoprogramming/lab5/scene/SceneObject.java @@ -1,6 +1,7 @@ package labs.introtoprogramming.lab5.scene; import labs.introtoprogramming.lab5.geometry.Ray; +import labs.introtoprogramming.lab5.object.Box; public class SceneObject { @@ -37,4 +38,8 @@ public Mesh getMesh() { public boolean intersect(Ray ray) { return false; } + + public Box getBoundary() { + return null; + } } diff --git a/src/main/java/labs/introtoprogramming/lab5/tree/KDNode.java b/src/main/java/labs/introtoprogramming/lab5/tree/KDNode.java new file mode 100644 index 0000000..9629b41 --- /dev/null +++ b/src/main/java/labs/introtoprogramming/lab5/tree/KDNode.java @@ -0,0 +1,60 @@ +package labs.introtoprogramming.lab5.tree; + +import labs.introtoprogramming.lab5.geometry.Ray; +import labs.introtoprogramming.lab5.object.Box; +import labs.introtoprogramming.lab5.scene.SceneObject; + +import java.util.List; + +public class KDNode { + Box boundaryBox; + private KDNode[] children; + + private List objects; + + public KDNode(List objects, Box boundaryBox, KDNode[] children) { + this.objects = objects; + this.boundaryBox = boundaryBox; + this.children = children; + } + + public SceneObject intersect(Ray ray) { + if (boundaryBox.intersect(ray)) { + double minDist = Double.MAX_VALUE; + SceneObject res = checkChildren(ray); + if (res != null) { + minDist = ray.getScale(); + } + for (SceneObject obj : objects) { + if (obj.intersect(ray)) { + double dist = ray.getScale(); + if (dist < minDist) { + minDist = dist; + res = obj; + } + } + } + return res; + } + return null; + } + + private SceneObject checkChildren(Ray ray) { + SceneObject res = null; + double minDist = Double.MAX_VALUE; + for (KDNode node : children) { + if (node == null) { + continue; + } + SceneObject obj = node.intersect(ray); + if (obj != null) { + double dist = ray.getScale(); + if (dist < minDist) { + res = obj; + minDist = dist; + } + } + } + return res; + } +} diff --git a/src/main/java/labs/introtoprogramming/lab5/tree/KDTree.java b/src/main/java/labs/introtoprogramming/lab5/tree/KDTree.java new file mode 100644 index 0000000..252a7d9 --- /dev/null +++ b/src/main/java/labs/introtoprogramming/lab5/tree/KDTree.java @@ -0,0 +1,127 @@ +package labs.introtoprogramming.lab5.tree; + +import labs.introtoprogramming.lab5.geometry.Ray; +import labs.introtoprogramming.lab5.geometry.Vector3; +import labs.introtoprogramming.lab5.object.Box; +import labs.introtoprogramming.lab5.scene.SceneObject; +import labs.introtoprogramming.lab5.scene.Transform; + +import java.util.ArrayList; +import java.util.List; + +public class KDTree { + private static final double MIN_NUMBER_OF_OBJECTS = 3; + private KDNode root; + private SceneObject intersection; + + public KDTree(List objects) { + Box box = getBoundary(objects); + root = buildNode(objects, box); + } + + private Box getBoundary(List objects) { + if (objects.size() == 0) { + return new Box(new Transform(), Vector3.ZERO, Vector3.ZERO); + } + Vector3 lowerBound = new Vector3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); + Vector3 upperBound = new Vector3(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE); + for (SceneObject obj : objects) { + Box boundary = obj.getBoundary(); + if (boundary == null) { + continue; + } + lowerBound = new Vector3( + Math.min(lowerBound.x, boundary.lowerBounds.x), + Math.min(lowerBound.y, boundary.lowerBounds.y), + Math.min(lowerBound.z, boundary.lowerBounds.z) + ); + upperBound = new Vector3( + Math.max(upperBound.x, boundary.upperBounds.x), + Math.max(upperBound.y, boundary.upperBounds.y), + Math.max(upperBound.z, boundary.upperBounds.z) + ); + } + if (lowerBound.equals(Vector3.ONE.multiply(Double.MAX_VALUE))) { + lowerBound = Vector3.ONE.multiply(Double.MIN_VALUE); + } + if (upperBound.equals(Vector3.ONE.multiply(Double.MIN_VALUE))) { + upperBound = Vector3.ONE.multiply(Double.MAX_VALUE); + } + return new Box(new Transform(), lowerBound, upperBound); + } + + private KDNode buildNode(List objects, Box box) { + if (objects.size() <= MIN_NUMBER_OF_OBJECTS) { + return new KDNode(objects, box, new KDNode[0]); + } + KDNode[] children = getChildren(objects, box); + return new KDNode(objects, box, children); + } + + private KDNode[] getChildren(List objects, Box box) { + Vector3 mid = new Vector3( + (box.upperBounds.x + box.lowerBounds.x) / 2, + (box.upperBounds.y + box.lowerBounds.y) / 2, + (box.upperBounds.z + box.lowerBounds.z) / 2 + ); + Vector3 u = box.upperBounds; + Vector3 l = box.lowerBounds; + KDNode[] children = new KDNode[8]; + Box b = new Box(new Transform(), mid, u); + children[0] = buildNode(getObjectInBox(objects, b), b); + b = new Box(new Transform(), new Vector3(l.x, mid.y, mid.z), new Vector3(mid.x, u.y, u.z)); + children[1] = buildNode(getObjectInBox(objects, b), b); + b = new Box(new Transform(), new Vector3(mid.x, l.y, mid.z), new Vector3(u.x, mid.y, u.z)); + children[2] = buildNode(getObjectInBox(objects, b), b); + b = new Box(new Transform(), new Vector3(l.x, l.y, mid.z), new Vector3(mid.x, mid.y, u.z)); + children[3] = buildNode(getObjectInBox(objects, b), b); + b = new Box(new Transform(), new Vector3(mid.x, mid.y, l.z), new Vector3(u.x, u.y, mid.z)); + children[4] = buildNode(getObjectInBox(objects, b), b); + b = new Box(new Transform(), new Vector3(l.x, mid.y, l.z), new Vector3(mid.x, u.y, mid.z)); + children[5] = buildNode(getObjectInBox(objects, b), b); + b = new Box(new Transform(), new Vector3(mid.x, l.y, l.z), new Vector3(u.x, mid.y, mid.z)); + children[6] = buildNode(getObjectInBox(objects, b), b); + b = new Box(new Transform(), l, mid); + children[7] = buildNode(getObjectInBox(objects, b), b); + return children; + } + + private List getObjectInBox(List objects, Box box) { + ArrayList res = new ArrayList<>(); + for (SceneObject obj : objects) { + Box boundary = obj.getBoundary(); + if (boundary == null) { + continue; + } + if (boxInBox(boundary, box)) { + res.add(obj); + } + } + for (SceneObject obj : res) { + objects.remove(obj); + } + return res; + } + + private boolean boxInBox(Box in, Box out) { + return in.upperBounds.x <= out.upperBounds.x + && in.upperBounds.y <= out.upperBounds.y + && in.upperBounds.z <= out.upperBounds.z + && in.lowerBounds.x >= out.lowerBounds.x + && in.lowerBounds.y >= out.lowerBounds.y + && in.lowerBounds.z >= out.lowerBounds.z; + } + + public boolean intersect(Ray ray) { + intersection = root.intersect(ray); + return intersection != null; + } + + public SceneObject getIntersection() { + return intersection; + } + + public Box boundary() { + return root.boundaryBox; + } +} diff --git a/src/test/java/labs/introtoprogramming/lab5/object/BoxTests.java b/src/test/java/labs/introtoprogramming/lab5/object/BoxTests.java index ac1334d..5151540 100644 --- a/src/test/java/labs/introtoprogramming/lab5/object/BoxTests.java +++ b/src/test/java/labs/introtoprogramming/lab5/object/BoxTests.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import labs.introtoprogramming.lab5.geometry.Ray; @@ -81,4 +82,12 @@ public void testIntersectionMinusZeroDirection() { Box box = new Box(new Transform(Vector3.RIGHT.multiply(2))); assertFalse(box.intersect(ray)); } + + @Test + public void testBoundary() { + Box box = new Box(new Transform(Vector3.RIGHT.multiply(2))); + Box boundary = box.getBoundary(); + assertEquals(box.upperBounds, boundary.upperBounds); + assertEquals(box.lowerBounds, boundary.lowerBounds); + } } diff --git a/src/test/java/labs/introtoprogramming/lab5/object/DiskTests.java b/src/test/java/labs/introtoprogramming/lab5/object/DiskTests.java index 33ad99a..e3b432c 100644 --- a/src/test/java/labs/introtoprogramming/lab5/object/DiskTests.java +++ b/src/test/java/labs/introtoprogramming/lab5/object/DiskTests.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import labs.introtoprogramming.lab5.geometry.Ray; @@ -43,4 +44,10 @@ public void testGreaterThanRadius() { assertFalse(disk.intersect(ray)); assertEquals(1, ray.getScale(), DELTA); } + + @Test + public void testBoundary() { + Disk disk = new Disk(new Transform()); + assertNull(disk.getBoundary()); + } } diff --git a/src/test/java/labs/introtoprogramming/lab5/object/OptimizedObjectTests.java b/src/test/java/labs/introtoprogramming/lab5/object/OptimizedObjectTests.java new file mode 100644 index 0000000..e803b78 --- /dev/null +++ b/src/test/java/labs/introtoprogramming/lab5/object/OptimizedObjectTests.java @@ -0,0 +1,40 @@ +package labs.introtoprogramming.lab5.object; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import labs.introtoprogramming.lab5.geometry.Ray; +import labs.introtoprogramming.lab5.geometry.Vector3; +import labs.introtoprogramming.lab5.scene.SceneObject; +import labs.introtoprogramming.lab5.scene.Transform; +import org.junit.Test; + +public class OptimizedObjectTests { + private static final double DELTA = 1e-10; + + private static List DUMMY_OBJECTS = Arrays.asList( + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new Triangle(new Transform(), Vector3.RIGHT, Vector3.RIGHT.multiply(-1), Vector3.UP), + new Sphere(new Transform(), 1) + ); + + @Test + public void testInstantiation() { + OptimizedObject obj = new OptimizedObject(new Transform(), DUMMY_OBJECTS); + Box boundary = obj.getBoundary(); + Box expected = DUMMY_OBJECTS.get(0).getBoundary(); + assertEquals(expected.lowerBounds, boundary.lowerBounds); + assertEquals(expected.upperBounds, boundary.upperBounds); + } + + @Test + public void testIntersection() { + OptimizedObject obj = new OptimizedObject(new Transform(), DUMMY_OBJECTS); + Ray ray = new Ray(Vector3.FORWARD.multiply(2), Vector3.FORWARD.multiply(-1)); + assertTrue(obj.intersect(ray)); + assertEquals(1, ray.getScale(), DELTA); + } +} diff --git a/src/test/java/labs/introtoprogramming/lab5/object/PlaneTests.java b/src/test/java/labs/introtoprogramming/lab5/object/PlaneTests.java index 3ababdd..8af0ece 100644 --- a/src/test/java/labs/introtoprogramming/lab5/object/PlaneTests.java +++ b/src/test/java/labs/introtoprogramming/lab5/object/PlaneTests.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import labs.introtoprogramming.lab5.geometry.Ray; @@ -43,4 +44,10 @@ public void testBelowRayOrigin() { assertFalse(plane.intersect(ray)); assertEquals(1, ray.getScale(), DELTA); } + + @Test + public void testBoundary() { + Plane plane = new Plane(new Transform()); + assertNull(plane.getBoundary()); + } } diff --git a/src/test/java/labs/introtoprogramming/lab5/object/SphereTests.java b/src/test/java/labs/introtoprogramming/lab5/object/SphereTests.java index 11e0696..38adda2 100644 --- a/src/test/java/labs/introtoprogramming/lab5/object/SphereTests.java +++ b/src/test/java/labs/introtoprogramming/lab5/object/SphereTests.java @@ -43,4 +43,12 @@ public void testIntersectionOppositeDirection() { assertFalse(sphere.intersect(ray)); assertEquals(1, ray.getScale(), DELTA); } + + @Test + public void testBoundary() { + Sphere sphere = new Sphere(new Transform(), 5); + Box boundary = sphere.getBoundary(); + assertEquals(Vector3.ONE.multiply(5), boundary.upperBounds); + assertEquals(Vector3.ONE.multiply(-5), boundary.lowerBounds); + } } diff --git a/src/test/java/labs/introtoprogramming/lab5/object/TriangleTests.java b/src/test/java/labs/introtoprogramming/lab5/object/TriangleTests.java index 5d960d2..7e60794 100644 --- a/src/test/java/labs/introtoprogramming/lab5/object/TriangleTests.java +++ b/src/test/java/labs/introtoprogramming/lab5/object/TriangleTests.java @@ -65,4 +65,21 @@ public void testNoIntersectionOutOfBounds() { assertFalse(triangle.intersect(ray)); assertEquals(1, ray.getScale(), DELTA); } + + @Test + public void testNoIntersectionOppositeDirection() { + Ray ray = new Ray(Vector3.ZERO, Vector3.FORWARD); + Triangle triangle = new Triangle(new Transform(Vector3.FORWARD.multiply(-9)), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)); + assertFalse(triangle.intersect(ray)); + assertEquals(1, ray.getScale(), DELTA); + } + + @Test + public void testBoundary() { + Triangle triangle = new Triangle(new Transform(), Vector3.RIGHT, Vector3.FORWARD, Vector3.UP); + Box boundary = triangle.getBoundary(); + assertEquals(Vector3.ONE, boundary.upperBounds); + assertEquals(Vector3.ZERO, boundary.lowerBounds); + } } diff --git a/src/test/java/labs/introtoprogramming/lab5/tree/KDNodeTests.java b/src/test/java/labs/introtoprogramming/lab5/tree/KDNodeTests.java new file mode 100644 index 0000000..6b4816d --- /dev/null +++ b/src/test/java/labs/introtoprogramming/lab5/tree/KDNodeTests.java @@ -0,0 +1,117 @@ +package labs.introtoprogramming.lab5.tree; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import labs.introtoprogramming.lab5.geometry.Ray; +import labs.introtoprogramming.lab5.geometry.Vector3; +import labs.introtoprogramming.lab5.object.Box; +import labs.introtoprogramming.lab5.object.Sphere; +import labs.introtoprogramming.lab5.object.Triangle; +import labs.introtoprogramming.lab5.scene.SceneObject; +import labs.introtoprogramming.lab5.scene.Transform; + +import org.junit.Test; + +public class KDNodeTests { + private static List DUMMY_OBJECTS = Arrays.asList( + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new Triangle(new Transform(), Vector3.RIGHT, Vector3.RIGHT.multiply(-1), Vector3.UP), + new Sphere(new Transform(), 1) + ); + + @Test + public void testInstantiation() { + KDNode node = new KDNode(Collections.emptyList(), + new Box(new Transform(), Vector3.ZERO, Vector3.ZERO), + new KDNode[0]); + assertEquals(node.boundaryBox.upperBounds, Vector3.ZERO); + assertEquals(node.boundaryBox.lowerBounds, Vector3.ZERO); + } + + @Test + public void testIntersectionNoObjects() { + KDNode node = new KDNode(Collections.emptyList(), + new Box(new Transform(), Vector3.ZERO, Vector3.ZERO), + new KDNode[0]); + assertNull(node.intersect(new Ray(Vector3.ZERO, Vector3.ZERO))); + } + + @Test + public void testIntersectionMissBox() { + KDNode node = new KDNode(DUMMY_OBJECTS, + new Box(new Transform(), Vector3.ONE.multiply(-1), Vector3.ZERO), + new KDNode[0]); + assertNull(node.intersect(new Ray(Vector3.FORWARD, Vector3.UP))); + } + + @Test + public void testIntersectionChildIntersection() { + KDNode child = new KDNode(DUMMY_OBJECTS, + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[0]); + KDNode node = new KDNode(Collections.emptyList(), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[] { child }); + assertEquals(DUMMY_OBJECTS.get(2), node.intersect(new Ray(Vector3.FORWARD, Vector3.UP))); + } + + @Test + public void testIntersectionOwnObjectIntersection() { + KDNode node = new KDNode(DUMMY_OBJECTS, + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[0]); + assertEquals(DUMMY_OBJECTS.get(2), node.intersect(new Ray(Vector3.FORWARD, Vector3.UP))); + } + + @Test + public void testIntersectionEmptyChild() { + KDNode child = new KDNode(Collections.emptyList(), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[0]); + KDNode node = new KDNode(Collections.emptyList(), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[] { child }); + assertNull(node.intersect(new Ray(Vector3.FORWARD, Vector3.UP))); + } + + @Test + public void testIntersectionNullChild() { + KDNode node = new KDNode(Collections.emptyList(), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[2]); + assertNull(node.intersect(new Ray(Vector3.FORWARD, Vector3.UP))); + } + + @Test + public void testIntersectionNoChildIntersection() { + KDNode child = new KDNode(DUMMY_OBJECTS, + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[0]); + KDNode node = new KDNode(Collections.emptyList(), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[] { child }); + assertNull(node.intersect(new Ray(Vector3.FORWARD.multiply(100), Vector3.FORWARD))); + } + + @Test + public void testIntersectionMultyChildIntersection() { + KDNode first = new KDNode(Collections.singletonList(DUMMY_OBJECTS.get(0)), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[0]); + KDNode second = new KDNode(Collections.singletonList(DUMMY_OBJECTS.get(1)), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[0]); + KDNode third = new KDNode(Collections.singletonList(DUMMY_OBJECTS.get(2)), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[0]); + KDNode node = new KDNode(Collections.emptyList(), + new Box(new Transform(), Vector3.ONE.multiply(-5), Vector3.ONE.multiply(5)), + new KDNode[] { first, second, third }); + assertEquals(DUMMY_OBJECTS.get(2), node.intersect(new Ray(Vector3.FORWARD, Vector3.UP))); + } +} diff --git a/src/test/java/labs/introtoprogramming/lab5/tree/KDTreeTests.java b/src/test/java/labs/introtoprogramming/lab5/tree/KDTreeTests.java new file mode 100644 index 0000000..61d0ae4 --- /dev/null +++ b/src/test/java/labs/introtoprogramming/lab5/tree/KDTreeTests.java @@ -0,0 +1,119 @@ +package labs.introtoprogramming.lab5.tree; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import labs.introtoprogramming.lab5.geometry.Ray; +import labs.introtoprogramming.lab5.geometry.Vector3; +import labs.introtoprogramming.lab5.object.Box; +import labs.introtoprogramming.lab5.object.Disk; +import labs.introtoprogramming.lab5.object.Plane; +import labs.introtoprogramming.lab5.object.Sphere; +import labs.introtoprogramming.lab5.object.Triangle; +import labs.introtoprogramming.lab5.scene.SceneObject; +import labs.introtoprogramming.lab5.scene.Transform; +import org.junit.Test; + + +public class KDTreeTests { + + private static List DUMMY_OBJECTS = Arrays.asList( + new Box(new Transform(), Vector3.ONE.multiply(-3), Vector3.ONE.multiply(3)), + new Triangle(new Transform(), Vector3.RIGHT, Vector3.RIGHT.multiply(-1), Vector3.UP), + new Sphere(new Transform(), 1) + ); + + private static List DUMMY_TRIANGLES = Arrays.asList( + new Triangle(new Transform(Vector3.FORWARD.multiply(-3)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-16)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-9)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.ONE.multiply(-10)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.ONE.multiply(-8)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-10).add(Vector3.UP)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-10).add(Vector3.UP.multiply(3))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-10).add(Vector3.UP.multiply(-3))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-10).add(Vector3.UP.multiply(-1))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-9)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-15)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-18)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.ONE.multiply(-9)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.ONE.multiply(-11)), Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(3))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(-3))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(-1))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(-1))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(-1))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(-1))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(-1))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(-1))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP.multiply(-1))), + Vector3.RIGHT, Vector3.UP, Vector3.RIGHT.multiply(-1)), + new Triangle(new Transform(Vector3.FORWARD.multiply(-12).add(Vector3.UP)), Vector3.RIGHT, + Vector3.UP, Vector3.RIGHT.multiply(-1)) + ); + + private static List DUMMY_OBJECTS_WITHOUT_BOUNDARY = Arrays.asList( + new Plane(new Transform()), + new Disk(new Transform(Vector3.ONE.multiply(-50))), + new Plane(new Transform(Vector3.ONE.multiply(100))), + new Disk(new Transform(Vector3.ONE.multiply(50))) + ); + + @Test + public void testInstantiation() { + KDTree tree = new KDTree(DUMMY_OBJECTS); + Box boundary = tree.boundary(); + assertEquals(boundary.lowerBounds, DUMMY_OBJECTS.get(0).getBoundary().lowerBounds); + assertEquals(boundary.upperBounds, DUMMY_OBJECTS.get(0).getBoundary().upperBounds); + } + + @Test + public void testEmptyList() { + KDTree tree = new KDTree(Collections.emptyList()); + assertFalse(tree.intersect(new Ray(Vector3.ZERO, Vector3.ZERO))); + assertNull(tree.getIntersection()); + } + + @Test + public void testIntersection() { + KDTree tree = new KDTree(new ArrayList<>(DUMMY_OBJECTS)); + assertTrue(tree.intersect(new Ray(Vector3.ZERO, Vector3.FORWARD))); + assertEquals(DUMMY_OBJECTS.get(1), tree.getIntersection()); + } + + @Test + public void testBuild() { + KDTree tree = new KDTree(new ArrayList<>(DUMMY_TRIANGLES)); + assertTrue(tree.intersect(new Ray(Vector3.FORWARD, Vector3.FORWARD.multiply(-1)))); + assertEquals(DUMMY_TRIANGLES.get(0), tree.getIntersection()); + } + + @Test + public void testBuildObjectWithoutBoundary() { + KDTree tree = new KDTree(new ArrayList<>(DUMMY_OBJECTS_WITHOUT_BOUNDARY)); + Box boundary = tree.boundary(); + assertEquals(Vector3.ONE.multiply(Double.MAX_VALUE), boundary.upperBounds); + assertEquals(Vector3.ONE.multiply(Double.MIN_VALUE), boundary.lowerBounds); + assertTrue(tree.intersect(new Ray(Vector3.UP.multiply(-2), Vector3.UP.multiply(1)))); + assertEquals(DUMMY_OBJECTS_WITHOUT_BOUNDARY.get(0), tree.getIntersection()); + } +}