disallow unbounded Hittables and refactor Octree
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) { }
|
|
||||||
}
|
|
@ -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,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…
Reference in New Issue