disallow unbounded Hittables and refactor Octree

main
jbb01 6 months ago
parent 414af5860b
commit 3a3949f518

@ -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<AABB> getBoundingBox(@NotNull List<? extends @NotNull Hittable> 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<Range> 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(),
};
}
}

@ -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));
}
}

@ -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<T> {
private final @NotNull NodeStorage<T> 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 <a href="https://doi.org/10.1007/978-3-642-76298-7_3">
* 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.</a>
*/
public void hit(@NotNull Ray ray, @NotNull Predicate<T> 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<T> {
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<T> add(@NotNull Entry<T> entry);
protected abstract boolean hit(@NotNull Ray ray, @NotNull Predicate<T> action);
protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<T> action) {
return hit(ray, action);
}
}
private static final class ListStorage<T> extends Storage<T> {
private final @NotNull List<Entry<T>> list = new ArrayList<>();
public ListStorage(@NotNull Vec3 center, double dimension) {
super(center, dimension);
}
@Override
public @NotNull Storage<T> add(@NotNull Entry<T> entry) {
if (list.size() >= LIST_SIZE_LIMIT) {
var node = new NodeStorage<T>(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<T> action) {
var hit = false;
for (Entry<T> entry : list) {
hit |= action.test(entry.object());
}
return hit;
}
}
private static final class NodeStorage<T> extends Storage<T> {
@SuppressWarnings("unchecked")
private final @Nullable Storage<T> @NotNull[] octants = new Storage[8];
private final @NotNull List<Entry<T>> list = new ArrayList<>(); // track elements spanning multiple octants separately
public NodeStorage(@NotNull Vec3 center, double dimension) {
super(center, dimension);
}
@Override
public @NotNull Storage<T> add(@NotNull Entry<T> 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<T> 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<T> 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<T> action) {
if (tmax < 0) return false;
// check for hit
var hit = false;
// process entries spanning multiple children
for (Entry<T> 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<T>(@NotNull BoundingBox bbox, T object) { }
}

@ -17,4 +17,10 @@ public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
origin().z() + t * direction.z() origin().z() + t * direction.z()
); );
} }
public int vmask() {
return (direction().x() < 0 ? 1 : 0)
| (direction().y() < 0 ? 2 : 0)
| (direction().z() < 0 ? 4 : 0);
}
} }

@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.scene; 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.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
@ -19,9 +19,7 @@ public interface Hittable {
*/ */
@NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range); @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range);
default @NotNull Optional<BoundingBox> getBoundingBox() { @NotNull AABB getBoundingBox();
return Optional.empty();
}
default @NotNull Hittable translate(@NotNull Vec3 offset) { default @NotNull Hittable translate(@NotNull Vec3 offset) {
return new Translate(this, offset); return new Translate(this, offset);

@ -1,5 +1,6 @@
package eu.jonahbauer.raytracing.scene; package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.Color; import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.scene.util.HittableCollection; import eu.jonahbauer.raytracing.scene.util.HittableCollection;
@ -58,6 +59,11 @@ public final class Scene extends HittableCollection {
list.hit(ray, state); list.hit(ray, state);
} }
@Override
public @NotNull AABB getBoundingBox() {
return objects.getBoundingBox();
}
public @NotNull Color getBackgroundColor(@NotNull Ray ray) { public @NotNull Color getBackgroundColor(@NotNull Ray ray) {
return background.getColor(ray); return background.getColor(ray);
} }

@ -1,15 +1,16 @@
package eu.jonahbauer.raytracing.scene.hittable2d; 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.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Ellipse extends Hittable2D { 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) { public Ellipse(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material); super(origin, u, v, material);
this.bbox = new AABB(origin.minus(u).minus(v), origin.plus(u).plus(v));
} }
@Override @Override
@ -18,9 +19,7 @@ public final class Ellipse extends Hittable2D {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
var a = origin.minus(u).minus(v); return bbox;
var b = origin.plus(u).plus(v);
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
} }
} }

@ -46,19 +46,13 @@ public abstract class Hittable2D implements Hittable {
var position = ray.at(t); var position = ray.at(t);
var p = position.minus(origin); 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; var frontFace = denominator < 0;
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace)); return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace));
} }
protected boolean isInterior(@NotNull Vec3 p) { protected abstract boolean isInterior(double alpha, double beta);
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;
}
} }

@ -1,16 +1,16 @@
package eu.jonahbauer.raytracing.scene.hittable2d; 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.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Parallelogram extends Hittable2D { 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) { public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material); super(origin, u, v, material);
this.bbox = new AABB(origin, origin.plus(u).plus(v));
} }
@Override @Override
@ -19,9 +19,7 @@ public final class Parallelogram extends Hittable2D {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
var a = origin; return bbox;
var b = origin.plus(u).plus(v);
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
} }
} }

@ -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;
}
}

@ -1,16 +1,16 @@
package eu.jonahbauer.raytracing.scene.hittable2d; 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.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Triangle extends Hittable2D { 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) { public Triangle(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material); super(origin, u, v, material);
this.bbox = new AABB(origin, origin.plus(u).plus(v));
} }
@Override @Override
@ -19,9 +19,7 @@ public final class Triangle extends Hittable2D {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
var a = origin; return bbox;
var b = origin.plus(u).plus(v);
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
} }
} }

@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.scene.hittable3d; 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.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
@ -36,7 +36,7 @@ public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNul
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return boundary().getBoundingBox(); return boundary().getBoundingBox();
} }
} }

@ -1,7 +1,7 @@
package eu.jonahbauer.raytracing.scene.hittable3d; package eu.jonahbauer.raytracing.scene.hittable3d;
import eu.jonahbauer.raytracing.render.material.Material; 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.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
@ -12,21 +12,28 @@ import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; 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 { private final @NotNull AABB bbox;
Objects.requireNonNull(center, "center");
Objects.requireNonNull(material, "material"); 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"); 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.bbox = new AABB(
this(new Vec3(x, y, z), r, material); center.minus(radius, radius, radius),
center.plus(radius, radius, radius)
);
} }
@Override @Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) { public @NotNull Optional<HitResult> 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 a = ray.direction().squared();
var h = ray.direction().times(oc); var h = ray.direction().times(oc);
@ -48,22 +55,7 @@ public record Sphere(@NotNull Vec3 center, double radius, @NotNull Material mate
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return Optional.of(new BoundingBox( return bbox;
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);
} }
} }

@ -1,26 +1,25 @@
package eu.jonahbauer.raytracing.scene.transform; 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.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class RotateY extends Transform { public final class RotateY extends Transform {
private final double cos; private final double cos;
private final double sin; private final double sin;
private final @NotNull Optional<BoundingBox> bbox; private final @NotNull AABB bbox;
public RotateY(@NotNull Hittable object, double angle) { public RotateY(@NotNull Hittable object, double angle) {
super(object); super(object);
this.cos = Math.cos(angle); this.cos = Math.cos(angle);
this.sin = Math.sin(angle); this.sin = Math.sin(angle);
this.bbox = object.getBoundingBox().map(bbox -> { var bbox = object.getBoundingBox();
var min = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); 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 max = new Vec3(- Double.MAX_VALUE, - Double.MAX_VALUE, - Double.MAX_VALUE);
@ -42,8 +41,7 @@ public final class RotateY extends Transform {
} }
} }
return new BoundingBox(min, max); this.bbox = new AABB(min, max);
});
} }
@Override @Override
@ -85,7 +83,7 @@ public final class RotateY extends Transform {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; return bbox;
} }
} }

@ -1,26 +1,25 @@
package eu.jonahbauer.raytracing.scene.transform; 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.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Translate extends Transform { public final class Translate extends Transform {
private final @NotNull Vec3 offset; private final @NotNull Vec3 offset;
private final @NotNull AABB bbox;
private final @NotNull Optional<BoundingBox> bbox;
public Translate(@NotNull Hittable object, @NotNull Vec3 offset) { public Translate(@NotNull Hittable object, @NotNull Vec3 offset) {
super(object); super(object);
this.offset = offset; this.offset = offset;
this.bbox = object.getBoundingBox().map(bbox -> new BoundingBox(
var bbox = object.getBoundingBox();
this.bbox = new AABB(
bbox.min().plus(offset), bbox.min().plus(offset),
bbox.max().plus(offset) bbox.max().plus(offset)
)); );
} }
@Override @Override
@ -40,7 +39,7 @@ public final class Translate extends Transform {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; return bbox;
} }
} }

@ -1,14 +1,11 @@
package eu.jonahbauer.raytracing.scene.util; package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -23,20 +20,6 @@ public abstract class HittableCollection implements Hittable {
public abstract void hit(@NotNull Ray ray, @NotNull State state); public abstract void hit(@NotNull Ray ray, @NotNull State state);
protected static @NotNull Optional<BoundingBox> getBoundingBox(@NotNull Collection<? extends @NotNull Hittable> 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) { protected static boolean hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object) {
var r = object.hit(ray, state.range); var r = object.hit(ray, state.range);
if (r.isPresent()) { if (r.isPresent()) {

@ -1,20 +1,19 @@
package eu.jonahbauer.raytracing.scene.util; 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.math.Ray;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
import java.util.Optional;
public final class HittableList extends HittableCollection { public final class HittableList extends HittableCollection {
private final @NotNull List<Hittable> objects; private final @NotNull List<Hittable> objects;
private final @NotNull Optional<BoundingBox> bbox; private final @NotNull AABB bbox;
public HittableList(@NotNull List<? extends @NotNull Hittable> objects) { public HittableList(@NotNull List<? extends @NotNull Hittable> objects) {
this.objects = List.copyOf(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) { public HittableList(@NotNull Hittable @NotNull... objects) {
@ -27,7 +26,7 @@ public final class HittableList extends HittableCollection {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; return bbox;
} }
} }

@ -1,61 +1,253 @@
package eu.jonahbauer.raytracing.scene.util; 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 eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays; import java.util.*;
import java.util.List; import java.util.function.Predicate;
import java.util.Map; import java.util.stream.IntStream;
import java.util.Map.Entry;
import java.util.Optional;
public final class HittableOctree extends HittableCollection { public final class HittableOctree extends HittableCollection {
private final @NotNull Octree<Hittable> objects; private static final int LIST_SIZE_LIMIT = 16;
private final @NotNull Optional<BoundingBox> bbox;
private final @Nullable Storage storage;
private final @NotNull AABB bbox;
public HittableOctree(@NotNull List<? extends @NotNull Hittable> objects) { public HittableOctree(@NotNull List<? extends @NotNull Hittable> objects) {
var result = newOctree(objects); bbox = AABB.getBoundingBox(objects).orElse(AABB.EMPTY);
this.objects = result.getKey(); storage = newStorage(bbox, objects);
this.bbox = Optional.of(result.getValue()); }
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())),
};
}
private static @Nullable Storage newStorage(@NotNull AABB aabb, @NotNull List<? extends @NotNull Hittable> 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<Hittable>[]) new List<?>[8];
for (int i = 0; i < 8; i++) octants[i] = new ArrayList<>();
var bboxes = getBoundingBoxes(aabb, center);
var list = new ArrayList<Hittable>();
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);
}
} }
public HittableOctree(@NotNull Hittable @NotNull... objects) { return new NodeStorage(aabb, center, list, IntStream.range(0, 8).mapToObj(i -> newStorage(bboxes[i], octants[i])).toArray(Storage[]::new));
this(List.of(objects)); }
} }
@Override @Override
public void hit(@NotNull Ray ray, @NotNull State state) { 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 @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; return bbox;
} }
private static @NotNull Entry<@NotNull Octree<Hittable>, @NotNull BoundingBox> newOctree(@NotNull List<? extends Hittable> 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 <a href="https://doi.org/10.1007/978-3-642-76298-7_3">
* 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.</a>
*/
private void hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> 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);
int i = 1; }
for (var object : objects) {
var bbox = object.getBoundingBox().orElseThrow(); private abstract static sealed class Storage {
center = Vec3.average(center, bbox.center(), i++); protected final @NotNull AABB bbox;
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()), protected boolean hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> action) {
Math.abs(max.y() - center.y()), var range = bbox.intersect(ray);
Math.abs(max.z() - center.z()), if (range.isEmpty()) return false;
Math.abs(min.x() - center.x()),
Math.abs(min.y() - center.y()), int vmask = ray.vmask();
Math.abs(min.z() - center.z()) return hit0(ray, vmask, range.get().min(), range.get().max(), action);
}).max().orElse(10); }
var out = new Octree<Hittable>(center, dimension); protected abstract boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<? super Hittable> action);
objects.forEach(object -> out.add(object.getBoundingBox().get(), object)); }
return Map.entry(out, new BoundingBox(min, max));
private static final class ListStorage extends Storage {
private final @NotNull List<Hittable> list;
public ListStorage(@NotNull AABB bbox, @NotNull List<? extends @NotNull Hittable> entries) {
super(bbox);
this.list = List.copyOf(entries);
}
@Override
protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<? super Hittable> 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;
private final @NotNull List<Hittable> list; // track elements spanning multiple octants separately
public NodeStorage(@NotNull AABB bbox, @NotNull Vec3 center, @NotNull List<? extends @NotNull Hittable> 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<? super Hittable> 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<? super Hittable> 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}
}
}
}
} }
} }

Loading…
Cancel
Save