diff --git a/src/main/java/eu/jonahbauer/raytracing/math/AABB.java b/src/main/java/eu/jonahbauer/raytracing/math/AABB.java new file mode 100644 index 0000000..4beb8ea --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/math/AABB.java @@ -0,0 +1,95 @@ +package eu.jonahbauer.raytracing.math; + +import eu.jonahbauer.raytracing.scene.Hittable; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Optional; + +/** + * An axis-aligned bounding box. + */ +public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) { + public static final AABB UNIVERSE = new AABB(Vec3.MIN, Vec3.MAX); + public static final AABB EMPTY = new AABB(Vec3.ZERO, Vec3.ZERO); + + public AABB { + var a = min; + var b = max; + min = Vec3.min(a, b); + max = Vec3.max(a, b); + } + + public AABB(@NotNull Range x, @NotNull Range y, @NotNull Range z) { + this(new Vec3(x.min(), y.min(), z.min()), new Vec3(x.max(), y.max(), z.max())); + } + + public static @NotNull Optional getBoundingBox(@NotNull List objects) { + var bbox = (AABB) null; + for (var object : objects) { + bbox = bbox == null ? object.getBoundingBox() : bbox.expand(object.getBoundingBox()); + } + return Optional.ofNullable(bbox); + } + + public @NotNull Range x() { + return new Range(min.x(), max.x()); + } + + public @NotNull Range y() { + return new Range(min.y(), max.y()); + } + + public @NotNull Range z() { + return new Range(min.z(), max.z()); + } + + public @NotNull Vec3 center() { + return Vec3.average(min, max, 2); + } + + public @NotNull AABB expand(@NotNull AABB box) { + return new AABB(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max)); + } + + public boolean hit(@NotNull Ray ray) { + return intersect(ray).isPresent(); + } + + public @NotNull Optional intersect(@NotNull Ray ray) { + if (this == UNIVERSE) return Optional.of(Range.UNIVERSE); + + int vmask = ray.vmask(); + + // calculate t values for intersection points of ray with planes through min + var tmin = intersect(min(), ray); + // calculate t values for intersection points of ray with planes through max + var tmax = intersect(max(), ray); + + // determine range of t for which the ray is inside this voxel + double tlmax = Double.NEGATIVE_INFINITY; // lower limit maximum + double tumin = Double.POSITIVE_INFINITY; // upper limit minimum + for (int i = 0; i < 3; i++) { + // classify t values as lower or upper limit based on vmask + if ((vmask & (1 << i)) == 0) { + // min is lower limit and max is upper limit + tlmax = Math.max(tlmax, tmin[i]); + tumin = Math.min(tumin, tmax[i]); + } else { + // max is lower limit and min is upper limit + tlmax = Math.max(tlmax, tmax[i]); + tumin = Math.min(tumin, tmin[i]); + } + } + + return tlmax < tumin ? Optional.of(new Range(tlmax, tumin)) : Optional.empty(); + } + + public static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Ray ray) { + return new double[] { + (corner.x() - ray.origin().x()) / ray.direction().x(), + (corner.y() - ray.origin().y()) / ray.direction().y(), + (corner.z() - ray.origin().z()) / ray.direction().z(), + }; + } +} diff --git a/src/main/java/eu/jonahbauer/raytracing/math/BoundingBox.java b/src/main/java/eu/jonahbauer/raytracing/math/BoundingBox.java deleted file mode 100644 index 81a53d4..0000000 --- a/src/main/java/eu/jonahbauer/raytracing/math/BoundingBox.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.jonahbauer.raytracing.math; - -import org.jetbrains.annotations.NotNull; - -public record BoundingBox(@NotNull Vec3 min, @NotNull Vec3 max) { - public BoundingBox { - var a = min; - var b = max; - min = Vec3.min(a, b); - max = Vec3.max(a, b); - } - - public @NotNull Vec3 center() { - return Vec3.average(min, max, 2); - } - - public @NotNull BoundingBox expand(@NotNull BoundingBox box) { - return new BoundingBox(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max)); - } -} diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Octree.java b/src/main/java/eu/jonahbauer/raytracing/math/Octree.java deleted file mode 100644 index cbfc38d..0000000 --- a/src/main/java/eu/jonahbauer/raytracing/math/Octree.java +++ /dev/null @@ -1,259 +0,0 @@ -package eu.jonahbauer.raytracing.math; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.function.Predicate; - -public final class Octree { - private final @NotNull NodeStorage storage; - - public Octree(@NotNull Vec3 center, double dimension) { - this.storage = new NodeStorage<>(center, dimension); - } - - public void add(@NotNull BoundingBox bbox, T object) { - storage.add(new Entry<>(bbox, object)); - } - - /** - * Use HERO algorithms to find all elements that could possibly be hit by the given ray. - * @see - * Agate, M., Grimsdale, R.L., Lister, P.F. (1991). - * The HERO Algorithm for Ray-Tracing Octrees. - * In: Grimsdale, R.L., Straßer, W. (eds) Advances in Computer Graphics Hardware IV. Eurographic Seminars. Springer, Berlin, Heidelberg. - */ - public void hit(@NotNull Ray ray, @NotNull Predicate action) { - storage.hit(ray, action); - } - - public static int getOctantIndex(@NotNull Vec3 center, @NotNull Vec3 pos) { - return (pos.x() < center.x() ? 0 : 1) - | (pos.y() < center.y() ? 0 : 2) - | (pos.z() < center.z() ? 0 : 4); - - } - - private static sealed abstract class Storage { - protected static final int LIST_SIZE_LIMIT = 32; - - protected final @NotNull Vec3 center; - protected final double dimension; - - public Storage(@NotNull Vec3 center, double dimension) { - this.center = Objects.requireNonNull(center); - this.dimension = dimension; - } - - public abstract @NotNull Storage add(@NotNull Entry entry); - - protected abstract boolean hit(@NotNull Ray ray, @NotNull Predicate action); - - protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate action) { - return hit(ray, action); - } - } - - private static final class ListStorage extends Storage { - private final @NotNull List> list = new ArrayList<>(); - - public ListStorage(@NotNull Vec3 center, double dimension) { - super(center, dimension); - } - - @Override - public @NotNull Storage add(@NotNull Entry entry) { - if (list.size() >= LIST_SIZE_LIMIT) { - var node = new NodeStorage(center, dimension); - list.forEach(node::add); - node.add(entry); - return node; - } else { - list.add(entry); - return this; - } - } - - @Override - protected boolean hit(@NotNull Ray ray, @NotNull Predicate action) { - var hit = false; - for (Entry entry : list) { - hit |= action.test(entry.object()); - } - return hit; - } - } - - private static final class NodeStorage extends Storage { - @SuppressWarnings("unchecked") - private final @Nullable Storage @NotNull[] octants = new Storage[8]; - private final @NotNull List> list = new ArrayList<>(); // track elements spanning multiple octants separately - - public NodeStorage(@NotNull Vec3 center, double dimension) { - super(center, dimension); - } - - @Override - public @NotNull Storage add(@NotNull Entry entry) { - var index = getOctantIndex(center, entry.bbox().min()); - if (index != getOctantIndex(center, entry.bbox().max())) { - list.add(entry); - } else { - var subnode = octants[index]; - if (subnode == null) { - subnode = newOctant(index); - } - octants[index] = subnode.add(entry); - } - return this; - } - - private @NotNull Storage newOctant(int index) { - var newSize = 0.5 * dimension; - var newCenter = this.center - .plus(new Vec3( - (index & 1) == 0 ? -newSize : newSize, - (index & 2) == 0 ? -newSize : newSize, - (index & 4) == 0 ? -newSize : newSize - )); - return new ListStorage<>(newCenter, newSize); - } - - @Override - protected boolean hit(@NotNull Ray ray, @NotNull Predicate action) { - int vmask = (ray.direction().x() < 0 ? 1 : 0) - | (ray.direction().y() < 0 ? 2 : 0) - | (ray.direction().z() < 0 ? 4 : 0); - - var min = center.minus(dimension, dimension, dimension); - var max = center.plus(dimension, dimension, dimension); - - // calculate t values for intersection points of ray with planes through min - var tmin = calculatePlaneIntersections(min, ray); - // calculate t values for intersection points of ray with planes through max - var tmax = calculatePlaneIntersections(max, ray); - - // determine range of t for which the ray is inside this voxel - double tlmax = Double.NEGATIVE_INFINITY; // lower limit maximum - double tumin = Double.POSITIVE_INFINITY; // upper limit minimum - for (int i = 0; i < 3; i++) { - // classify t values as lower or upper limit based on vmask - if ((vmask & (1 << i)) == 0) { - // min is lower limit and max is upper limit - tlmax = Math.max(tlmax, tmin[i]); - tumin = Math.min(tumin, tmax[i]); - } else { - // max is lower limit and min is upper limit - tlmax = Math.max(tlmax, tmax[i]); - tumin = Math.min(tumin, tmin[i]); - } - } - - var hit = tlmax < tumin; - if (!hit) return false; - - return hit0(ray, vmask, tlmax, tumin, action); - } - - @Override - protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate action) { - if (tmax < 0) return false; - - // check for hit - var hit = false; - - // process entries spanning multiple children - for (Entry entry : list) { - hit |= action.test(entry.object()); - } - - // t values for intersection points of ray with planes through center - var tmid = calculatePlaneIntersections(center, ray); - // masks of planes in the order of intersection, e.g. [2, 1, 4] for a ray intersection y = center.y() then x = center.x() then z = center.z() - var masklist = calculateMasklist(tmid); - // the first child to be hit by the ray assuming a ray with positive x, y and z coordinates - var childmask = (tmid[0] < tmin ? 1 : 0) - | (tmid[1] < tmin ? 2 : 0) - | (tmid[2] < tmin ? 4 : 0); - // the last child to be hit by the ray assuming a ray with positive x, y and z coordinates - var lastmask = (tmid[0] < tmax ? 1 : 0) - | (tmid[1] < tmax ? 2 : 0) - | (tmid[2] < tmax ? 4 : 0); - - var childTmin = tmin; - - int i = 0; - while (true) { - // use vmask to nullify the assumption of a positive ray made for childmask - var child = octants[childmask ^ vmask]; - - // calculate t value for exit of child - double childTmax; - if (childmask == lastmask) { - // last child shares tmax - childTmax = tmax; - } else { - // determine next child - while ((masklist[i] & childmask) != 0) { - i++; - } - childmask = childmask | masklist[i]; - // tmax of current child is the t value for the intersection with the plane dividing the current and next child - childTmax = tmid[Integer.numberOfTrailingZeros(masklist[i])]; - } - - // process child - var childHit = child != null && child.hit0(ray, vmask, childTmin, childTmax, action); - hit |= childHit; - - // break after last child has been processed or a hit has been found - if (childTmax == tmax || childHit) break; - - // tmin of next child is tmax of current child - childTmin = childTmax; - } - - return hit; - } - - private double @NotNull [] calculatePlaneIntersections(@NotNull Vec3 position, @NotNull Ray ray) { - return new double[] { - (position.x() - ray.origin().x()) / ray.direction().x(), - (position.y() - ray.origin().y()) / ray.direction().y(), - (position.z() - ray.origin().z()) / ray.direction().z(), - }; - } - - private static final int[][] MASKLISTS = new int[][] { - {1, 2, 4}, - {1, 4, 2}, - {4, 1, 2}, - {2, 1, 4}, - {2, 4, 1}, - {4, 2, 1} - }; - - private static int @NotNull [] calculateMasklist(double @NotNull[] tmid) { - if (tmid[0] < tmid[1]) { - if (tmid[1] < tmid[2]) { - return MASKLISTS[0]; // {1, 2, 4} - } else if (tmid[0] < tmid[2]) { - return MASKLISTS[1]; // {1, 4, 2} - } else { - return MASKLISTS[2]; // {4, 1, 2} - } - } else { - if (tmid[0] < tmid[2]) { - return MASKLISTS[3]; // {2, 1, 4} - } else if (tmid[1] < tmid[2]) { - return MASKLISTS[4]; // {2, 4, 1} - } else { - return MASKLISTS[5]; // {4, 2, 1} - } - } - } - } - - private record Entry(@NotNull BoundingBox bbox, T object) { } -} diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Ray.java b/src/main/java/eu/jonahbauer/raytracing/math/Ray.java index 9448ec5..cdddd36 100644 --- a/src/main/java/eu/jonahbauer/raytracing/math/Ray.java +++ b/src/main/java/eu/jonahbauer/raytracing/math/Ray.java @@ -17,4 +17,10 @@ public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) { origin().z() + t * direction.z() ); } + + public int vmask() { + return (direction().x() < 0 ? 1 : 0) + | (direction().y() < 0 ? 2 : 0) + | (direction().z() < 0 ? 4 : 0); + } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/Hittable.java b/src/main/java/eu/jonahbauer/raytracing/scene/Hittable.java index bd2fa0d..dc8cf45 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/Hittable.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/Hittable.java @@ -1,6 +1,6 @@ package eu.jonahbauer.raytracing.scene; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; @@ -19,9 +19,7 @@ public interface Hittable { */ @NotNull Optional hit(@NotNull Ray ray, @NotNull Range range); - default @NotNull Optional getBoundingBox() { - return Optional.empty(); - } + @NotNull AABB getBoundingBox(); default @NotNull Hittable translate(@NotNull Vec3 offset) { return new Translate(this, offset); diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/Scene.java b/src/main/java/eu/jonahbauer/raytracing/scene/Scene.java index dcac35b..c37c47e 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/Scene.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/Scene.java @@ -1,5 +1,6 @@ package eu.jonahbauer.raytracing.scene; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.render.Color; import eu.jonahbauer.raytracing.scene.util.HittableCollection; @@ -58,6 +59,11 @@ public final class Scene extends HittableCollection { list.hit(ray, state); } + @Override + public @NotNull AABB getBoundingBox() { + return objects.getBoundingBox(); + } + public @NotNull Color getBackgroundColor(@NotNull Ray ray) { return background.getColor(ray); } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Ellipse.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Ellipse.java index cb2ef6c..c6608f0 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Ellipse.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Ellipse.java @@ -1,15 +1,16 @@ package eu.jonahbauer.raytracing.scene.hittable2d; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.render.material.Material; import org.jetbrains.annotations.NotNull; -import java.util.Optional; - public final class Ellipse extends Hittable2D { + private final @NotNull AABB bbox; + public Ellipse(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) { super(origin, u, v, material); + this.bbox = new AABB(origin.minus(u).minus(v), origin.plus(u).plus(v)); } @Override @@ -18,9 +19,7 @@ public final class Ellipse extends Hittable2D { } @Override - public @NotNull Optional getBoundingBox() { - var a = origin.minus(u).minus(v); - var b = origin.plus(u).plus(v); - return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b))); + public @NotNull AABB getBoundingBox() { + return bbox; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java index 16d6197..df10e97 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java @@ -46,19 +46,13 @@ public abstract class Hittable2D implements Hittable { var position = ray.at(t); var p = position.minus(origin); - if (!isInterior(p)) return Optional.empty(); + var alpha = w.times(p.cross(v)); + var beta = w.times(u.cross(p)); + if (!isInterior(alpha, beta)) return Optional.empty(); var frontFace = denominator < 0; return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace)); } - protected boolean isInterior(@NotNull Vec3 p) { - var alpha = w.times(p.cross(v)); - var beta = w.times(u.cross(p)); - return isInterior(alpha, beta); - } - - protected boolean isInterior(double alpha, double beta) { - return false; - } + protected abstract boolean isInterior(double alpha, double beta); } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Parallelogram.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Parallelogram.java index cf84e89..93cb166 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Parallelogram.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Parallelogram.java @@ -1,16 +1,16 @@ package eu.jonahbauer.raytracing.scene.hittable2d; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.render.material.Material; import org.jetbrains.annotations.NotNull; -import java.util.Optional; - public final class Parallelogram extends Hittable2D { + private final @NotNull AABB bbox; public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) { super(origin, u, v, material); + this.bbox = new AABB(origin, origin.plus(u).plus(v)); } @Override @@ -19,9 +19,7 @@ public final class Parallelogram extends Hittable2D { } @Override - public @NotNull Optional getBoundingBox() { - var a = origin; - var b = origin.plus(u).plus(v); - return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b))); + public @NotNull AABB getBoundingBox() { + return bbox; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Plane.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Plane.java deleted file mode 100644 index 5a19b87..0000000 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Plane.java +++ /dev/null @@ -1,16 +0,0 @@ -package eu.jonahbauer.raytracing.scene.hittable2d; - -import eu.jonahbauer.raytracing.math.Vec3; -import eu.jonahbauer.raytracing.render.material.Material; -import org.jetbrains.annotations.NotNull; - -public final class Plane extends Hittable2D { - public Plane(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) { - super(origin, u, v, material); - } - - @Override - protected boolean isInterior(@NotNull Vec3 p) { - return true; - } -} diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Triangle.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Triangle.java index 67f28b7..6eab5e7 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Triangle.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Triangle.java @@ -1,16 +1,16 @@ package eu.jonahbauer.raytracing.scene.hittable2d; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.render.material.Material; import org.jetbrains.annotations.NotNull; -import java.util.Optional; - public final class Triangle extends Hittable2D { + private final @NotNull AABB bbox; public Triangle(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) { super(origin, u, v, material); + this.bbox = new AABB(origin, origin.plus(u).plus(v)); } @Override @@ -19,9 +19,7 @@ public final class Triangle extends Hittable2D { } @Override - public @NotNull Optional getBoundingBox() { - var a = origin; - var b = origin.plus(u).plus(v); - return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b))); + public @NotNull AABB getBoundingBox() { + return bbox; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/ConstantMedium.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/ConstantMedium.java index 675fd0b..ba72b50 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/ConstantMedium.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/ConstantMedium.java @@ -1,6 +1,6 @@ package eu.jonahbauer.raytracing.scene.hittable3d; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; @@ -36,7 +36,7 @@ public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNul } @Override - public @NotNull Optional getBoundingBox() { + public @NotNull AABB getBoundingBox() { return boundary().getBoundingBox(); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java index dcaa90b..ea1b990 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java @@ -1,7 +1,7 @@ package eu.jonahbauer.raytracing.scene.hittable3d; import eu.jonahbauer.raytracing.render.material.Material; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; @@ -12,21 +12,28 @@ import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.Optional; -public record Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) implements Hittable { +public final class Sphere implements Hittable { + private final @NotNull Vec3 center; + private final double radius; + private final @NotNull Material material; - public Sphere { - Objects.requireNonNull(center, "center"); - Objects.requireNonNull(material, "material"); + private final @NotNull AABB bbox; + + public Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) { + this.center = Objects.requireNonNull(center, "center"); + this.material = Objects.requireNonNull(material, "material"); if (radius <= 0 || !Double.isFinite(radius)) throw new IllegalArgumentException("radius must be positive"); - } + this.radius = radius; - public Sphere(double x, double y, double z, double r, @NotNull Material material) { - this(new Vec3(x, y, z), r, material); + this.bbox = new AABB( + center.minus(radius, radius, radius), + center.plus(radius, radius, radius) + ); } @Override public @NotNull Optional hit(@NotNull Ray ray, @NotNull Range range) { - var oc = ray.origin().minus(center()); + var oc = ray.origin().minus(center); var a = ray.direction().squared(); var h = ray.direction().times(oc); @@ -48,22 +55,7 @@ public record Sphere(@NotNull Vec3 center, double radius, @NotNull Material mate } @Override - public @NotNull Optional getBoundingBox() { - return Optional.of(new BoundingBox( - center.minus(radius, radius, radius), - center.plus(radius, radius, radius) - )); - } - - public @NotNull Sphere withCenter(@NotNull Vec3 center) { - return new Sphere(center, radius, material); - } - - public @NotNull Sphere withCenter(double x, double y, double z) { - return withCenter(new Vec3(x, y, z)); - } - - public @NotNull Sphere withRadius(double radius) { - return new Sphere(center, radius, material); + public @NotNull AABB getBoundingBox() { + return bbox; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java b/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java index a9a2719..fa675c8 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java @@ -1,49 +1,47 @@ package eu.jonahbauer.raytracing.scene.transform; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.Hittable; import org.jetbrains.annotations.NotNull; -import java.util.Optional; - public final class RotateY extends Transform { private final double cos; private final double sin; - private final @NotNull Optional bbox; + private final @NotNull AABB bbox; public RotateY(@NotNull Hittable object, double angle) { super(object); this.cos = Math.cos(angle); this.sin = Math.sin(angle); - this.bbox = object.getBoundingBox().map(bbox -> { - var min = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); - var max = new Vec3(- Double.MAX_VALUE, - Double.MAX_VALUE, - Double.MAX_VALUE); + var bbox = object.getBoundingBox(); + + var min = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); + var max = new Vec3(- Double.MAX_VALUE, - Double.MAX_VALUE, - Double.MAX_VALUE); - for (int i = 0; i < 2; i++) { - for (int j = 0; j < 2; j++) { - for (int k = 0; k < 2; k++) { - var x = i * bbox.max().x() + (1 - i) * bbox.min().x(); - var y = j * bbox.max().y() + (1 - j) * bbox.min().y(); - var z = k * bbox.max().z() + (1 - k) * bbox.min().z(); + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 2; j++) { + for (int k = 0; k < 2; k++) { + var x = i * bbox.max().x() + (1 - i) * bbox.min().x(); + var y = j * bbox.max().y() + (1 - j) * bbox.min().y(); + var z = k * bbox.max().z() + (1 - k) * bbox.min().z(); - var newx = cos * x + sin * z; - var newz = -sin * x + cos * z; + var newx = cos * x + sin * z; + var newz = -sin * x + cos * z; - var temp = new Vec3(newx, y, newz); + var temp = new Vec3(newx, y, newz); - min = Vec3.min(min, temp); - max = Vec3.max(max, temp); - } + min = Vec3.min(min, temp); + max = Vec3.max(max, temp); } } + } - return new BoundingBox(min, max); - }); + this.bbox = new AABB(min, max); } @Override @@ -85,7 +83,7 @@ public final class RotateY extends Transform { } @Override - public @NotNull Optional getBoundingBox() { + public @NotNull AABB getBoundingBox() { return bbox; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java b/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java index dbc52a0..0e72282 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java @@ -1,26 +1,25 @@ package eu.jonahbauer.raytracing.scene.transform; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.Hittable; import org.jetbrains.annotations.NotNull; -import java.util.Optional; - public final class Translate extends Transform { private final @NotNull Vec3 offset; - - private final @NotNull Optional bbox; + private final @NotNull AABB bbox; public Translate(@NotNull Hittable object, @NotNull Vec3 offset) { super(object); this.offset = offset; - this.bbox = object.getBoundingBox().map(bbox -> new BoundingBox( + + var bbox = object.getBoundingBox(); + this.bbox = new AABB( bbox.min().plus(offset), bbox.max().plus(offset) - )); + ); } @Override @@ -40,7 +39,7 @@ public final class Translate extends Transform { } @Override - public @NotNull Optional getBoundingBox() { + public @NotNull AABB getBoundingBox() { return bbox; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableCollection.java b/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableCollection.java index 26cb952..682f9a2 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableCollection.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableCollection.java @@ -1,14 +1,11 @@ package eu.jonahbauer.raytracing.scene.util; -import eu.jonahbauer.raytracing.math.BoundingBox; import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; -import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.Hittable; import org.jetbrains.annotations.NotNull; -import java.util.Collection; import java.util.Objects; import java.util.Optional; @@ -23,20 +20,6 @@ public abstract class HittableCollection implements Hittable { public abstract void hit(@NotNull Ray ray, @NotNull State state); - protected static @NotNull Optional getBoundingBox(@NotNull Collection objects) { - var bbox = new BoundingBox(Vec3.ZERO, Vec3.ZERO); - for (var object : objects) { - var b = object.getBoundingBox(); - if (b.isPresent()) { - bbox = bbox.expand(b.get()); - } else { - bbox = null; - break; - } - } - return Optional.ofNullable(bbox); - } - protected static boolean hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object) { var r = object.hit(ray, state.range); if (r.isPresent()) { diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableList.java b/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableList.java index af7f789..06a060a 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableList.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableList.java @@ -1,20 +1,19 @@ package eu.jonahbauer.raytracing.scene.util; -import eu.jonahbauer.raytracing.math.BoundingBox; +import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.scene.Hittable; import org.jetbrains.annotations.NotNull; import java.util.List; -import java.util.Optional; public final class HittableList extends HittableCollection { private final @NotNull List objects; - private final @NotNull Optional bbox; + private final @NotNull AABB bbox; public HittableList(@NotNull List objects) { this.objects = List.copyOf(objects); - this.bbox = getBoundingBox(this.objects); + this.bbox = AABB.getBoundingBox(this.objects).orElse(AABB.EMPTY); } public HittableList(@NotNull Hittable @NotNull... objects) { @@ -27,7 +26,7 @@ public final class HittableList extends HittableCollection { } @Override - public @NotNull Optional getBoundingBox() { + public @NotNull AABB getBoundingBox() { return bbox; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableOctree.java b/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableOctree.java index 9bb8b79..fe784fd 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableOctree.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/util/HittableOctree.java @@ -1,61 +1,253 @@ package eu.jonahbauer.raytracing.scene.util; -import eu.jonahbauer.raytracing.math.*; +import eu.jonahbauer.raytracing.math.AABB; +import eu.jonahbauer.raytracing.math.Range; +import eu.jonahbauer.raytracing.math.Ray; +import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.scene.Hittable; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.IntStream; public final class HittableOctree extends HittableCollection { - private final @NotNull Octree objects; - private final @NotNull Optional bbox; + private static final int LIST_SIZE_LIMIT = 16; + + private final @Nullable Storage storage; + private final @NotNull AABB bbox; public HittableOctree(@NotNull List objects) { - var result = newOctree(objects); - this.objects = result.getKey(); - this.bbox = Optional.of(result.getValue()); + bbox = AABB.getBoundingBox(objects).orElse(AABB.EMPTY); + storage = newStorage(bbox, objects); + } + + private static @NotNull AABB[] getBoundingBoxes(@NotNull AABB aabb, @NotNull Vec3 center) { + return new AABB[] { + new AABB(new Range(aabb.x().min(), center.x()), new Range(aabb.y().min(), center.y()), new Range(aabb.z().min(), center.z())), + new AABB(new Range(center.x(), aabb.x().max()), new Range(aabb.y().min(), center.y()), new Range(aabb.z().min(), center.z())), + new AABB(new Range(aabb.x().min(), center.x()), new Range(center.y(), aabb.y().max()), new Range(aabb.z().min(), center.z())), + new AABB(new Range(center.x(), aabb.x().max()), new Range(center.y(), aabb.y().max()), new Range(aabb.z().min(), center.z())), + new AABB(new Range(aabb.x().min(), center.x()), new Range(aabb.y().min(), center.y()), new Range(center.z(), aabb.z().max())), + new AABB(new Range(center.x(), aabb.x().max()), new Range(aabb.y().min(), center.y()), new Range(center.z(), aabb.z().max())), + new AABB(new Range(aabb.x().min(), center.x()), new Range(center.y(), aabb.y().max()), new Range(center.z(), aabb.z().max())), + new AABB(new Range(center.x(), aabb.x().max()), new Range(center.y(), aabb.y().max()), new Range(center.z(), aabb.z().max())), + }; } - public HittableOctree(@NotNull Hittable @NotNull... objects) { - this(List.of(objects)); + private static @Nullable Storage newStorage(@NotNull AABB aabb, @NotNull List objects) { + if (objects.isEmpty()) return null; + if (objects.size() < LIST_SIZE_LIMIT) { + return new ListStorage(aabb, objects); + } else { + var center = aabb.center(); + var octants = (List[]) new List[8]; + for (int i = 0; i < 8; i++) octants[i] = new ArrayList<>(); + var bboxes = getBoundingBoxes(aabb, center); + var list = new ArrayList(); + + for (var object : objects) { + var bbox = object.getBoundingBox(); + var imin = getOctantIndex(center, bbox.min()); + var imax = getOctantIndex(center, bbox.max()); + if (imin == imax) { + octants[imin].add(object); + } else { + list.add(object); + } + } + + return new NodeStorage(aabb, center, list, IntStream.range(0, 8).mapToObj(i -> newStorage(bboxes[i], octants[i])).toArray(Storage[]::new)); + } } @Override public void hit(@NotNull Ray ray, @NotNull State state) { - objects.hit(ray, object -> hit(state, ray, object)); + hit(ray, object -> hit(state, ray, object)); } @Override - public @NotNull Optional getBoundingBox() { + public @NotNull AABB getBoundingBox() { return bbox; } - private static @NotNull Entry<@NotNull Octree, @NotNull BoundingBox> newOctree(@NotNull List objects) { - Vec3 center = Vec3.ZERO, max = Vec3.MIN, min = Vec3.MAX; + /** + * Use HERO algorithms to find all elements that could possibly be hit by the given ray. + * @see + * Agate, M., Grimsdale, R.L., Lister, P.F. (1991). + * The HERO Algorithm for Ray-Tracing Octrees. + * In: Grimsdale, R.L., Straßer, W. (eds) Advances in Computer Graphics Hardware IV. Eurographic Seminars. Springer, Berlin, Heidelberg. + */ + private void hit(@NotNull Ray ray, @NotNull Predicate action) { + if (storage != null) storage.hit(ray, action); + } + + private static int getOctantIndex(@NotNull Vec3 center, @NotNull Vec3 pos) { + return (pos.x() < center.x() ? 0 : 1) + | (pos.y() < center.y() ? 0 : 2) + | (pos.z() < center.z() ? 0 : 4); + + } + + private abstract static sealed class Storage { + protected final @NotNull AABB bbox; - int i = 1; - for (var object : objects) { - var bbox = object.getBoundingBox().orElseThrow(); - center = Vec3.average(center, bbox.center(), i++); - max = Vec3.max(max, bbox.max()); - min = Vec3.min(min, bbox.min()); + public Storage(@NotNull AABB bbox) { + this.bbox = Objects.requireNonNull(bbox); } - var dimension = Arrays.stream(new double[] { - Math.abs(max.x() - center.x()), - Math.abs(max.y() - center.y()), - Math.abs(max.z() - center.z()), - Math.abs(min.x() - center.x()), - Math.abs(min.y() - center.y()), - Math.abs(min.z() - center.z()) - }).max().orElse(10); + protected boolean hit(@NotNull Ray ray, @NotNull Predicate action) { + var range = bbox.intersect(ray); + if (range.isEmpty()) return false; + + int vmask = ray.vmask(); + return hit0(ray, vmask, range.get().min(), range.get().max(), action); + } + + protected abstract boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate action); + } + + private static final class ListStorage extends Storage { + private final @NotNull List list; + + public ListStorage(@NotNull AABB bbox, @NotNull List entries) { + super(bbox); + this.list = List.copyOf(entries); + } + + @Override + protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate action) { + var hit = false; + for (Hittable hittable : list) { + hit |= action.test(hittable); + } + return hit; + } + } + + private static final class NodeStorage extends Storage { + private final @Nullable Storage @NotNull[] octants; + private final @NotNull Vec3 center; + private final int degenerate; - var out = new Octree(center, dimension); - objects.forEach(object -> out.add(object.getBoundingBox().get(), object)); - return Map.entry(out, new BoundingBox(min, max)); + private final @NotNull List list; // track elements spanning multiple octants separately + + public NodeStorage(@NotNull AABB bbox, @NotNull Vec3 center, @NotNull List list, @Nullable Storage @NotNull[] octants) { + super(bbox); + this.octants = octants; + this.center = center; + + this.list = List.copyOf(list); + + int count = 0; + int degenerate = 0; + for (int i = 0; i < octants.length; i++) { + if (octants[i] != null) { + count++; + degenerate = i; + } + } + this.degenerate = count == 1 ? degenerate : -1; + } + + @Override + protected boolean hit(@NotNull Ray ray, @NotNull Predicate action) { + if (degenerate >= 0 && list.isEmpty()) { + return octants[degenerate].hit(ray, action); + } else { + return super.hit(ray, action); + } + } + + @Override + protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate action) { + if (tmax < 0) return false; + + // check for hit + var hit = false; + + // process entries spanning multiple children + for (Hittable object : list) { + hit |= action.test(object); + } + + // t values for intersection points of ray with planes through center + var tmid = AABB.intersect(center, ray); + // masks of planes in the order of intersection, e.g. [2, 1, 4] for a ray intersection y = center.y() then x = center.x() then z = center.z() + var masklist = calculateMasklist(tmid); + // the first child to be hit by the ray assuming a ray with positive x, y and z coordinates + var childmask = (tmid[0] < tmin ? 1 : 0) + | (tmid[1] < tmin ? 2 : 0) + | (tmid[2] < tmin ? 4 : 0); + // the last child to be hit by the ray assuming a ray with positive x, y and z coordinates + var lastmask = (tmid[0] < tmax ? 1 : 0) + | (tmid[1] < tmax ? 2 : 0) + | (tmid[2] < tmax ? 4 : 0); + + var childTmin = tmin; + + int i = 0; + while (true) { + // use vmask to nullify the assumption of a positive ray made for childmask + var child = octants[childmask ^ vmask]; + + // calculate t value for exit of child + double childTmax; + if (childmask == lastmask) { + // last child shares tmax + childTmax = tmax; + } else { + // determine next child + while ((masklist[i] & childmask) != 0) { + i++; + } + childmask = childmask | masklist[i]; + // tmax of current child is the t value for the intersection with the plane dividing the current and next child + childTmax = tmid[Integer.numberOfTrailingZeros(masklist[i])]; + } + + // process child + var childHit = child != null && child.hit0(ray, vmask, childTmin, childTmax, action); + hit |= childHit; + + // break after last child has been processed or a hit has been found + if (childTmax == tmax || childHit) break; + + // tmin of next child is tmax of current child + childTmin = childTmax; + } + + return hit; + } + + private static final int[][] MASKLISTS = new int[][] { + {1, 2, 4}, + {1, 4, 2}, + {4, 1, 2}, + {2, 1, 4}, + {2, 4, 1}, + {4, 2, 1} + }; + + private static int @NotNull [] calculateMasklist(double @NotNull[] tmid) { + if (tmid[0] < tmid[1]) { + if (tmid[1] < tmid[2]) { + return MASKLISTS[0]; // {1, 2, 4} + } else if (tmid[0] < tmid[2]) { + return MASKLISTS[1]; // {1, 4, 2} + } else { + return MASKLISTS[2]; // {4, 1, 2} + } + } else { + if (tmid[0] < tmid[2]) { + return MASKLISTS[3]; // {2, 1, 4} + } else if (tmid[1] < tmid[2]) { + return MASKLISTS[4]; // {2, 4, 1} + } else { + return MASKLISTS[5]; // {4, 2, 1} + } + } + } } }