From 7757b3d5735c20ea213528014b01bc922e5aa697 Mon Sep 17 00:00:00 2001 From: jbb01 <32650546+jbb01@users.noreply.github.com> Date: Sat, 3 Aug 2024 02:06:12 +0200 Subject: [PATCH] add Range to optimize Shape#hit --- .../eu/jonahbauer/raytracing/math/Range.java | 19 ++++++++++ .../jonahbauer/raytracing/render/Scene.java | 37 +++++++++++++------ .../eu/jonahbauer/raytracing/shape/Shape.java | 3 +- .../jonahbauer/raytracing/shape/Sphere.java | 7 ++-- .../raytracing/shape/SphereTest.java | 3 +- 5 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 src/main/java/eu/jonahbauer/raytracing/math/Range.java diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Range.java b/src/main/java/eu/jonahbauer/raytracing/math/Range.java new file mode 100644 index 0000000..2e48ae9 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/math/Range.java @@ -0,0 +1,19 @@ +package eu.jonahbauer.raytracing.math; + +public record Range(double min, double max) { + public static final Range EMPTY = new Range(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY); + public static final Range UNIVERSE = new Range(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + public static final Range NON_NEGATIVE = new Range(0, Double.POSITIVE_INFINITY); + + public Range { + if (Double.isNaN(min) || Double.isNaN(max)) throw new IllegalArgumentException("min and max must not be NaN"); + } + + public boolean contains(double value) { + return min <= value && value <= max; + } + + public boolean surrounds(double value) { + return min < value && value < max; + } +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/Scene.java b/src/main/java/eu/jonahbauer/raytracing/render/Scene.java index aed625f..8be64e1 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/Scene.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/Scene.java @@ -1,10 +1,12 @@ package eu.jonahbauer.raytracing.render; +import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; +import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.shape.HitResult; import eu.jonahbauer.raytracing.shape.Shape; import org.jetbrains.annotations.NotNull; -import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -26,18 +28,10 @@ public record Scene(@NotNull List<@NotNull Shape> shapes) { var y = pixel.y(); var ray = pixel.ray(); - var result = shapes.stream() - .map(shape -> shape.hit(ray)) - .flatMap(Optional::stream) - .min(Comparator.naturalOrder()); - + var result = hit(ray); if (result.isPresent()) { var normal = result.get().normal(); - image.set(x, y, new Color( - 0.5 * (normal.x() + 1), - 0.5 * (normal.y() + 1), - 0.5 * (normal.z() + 1) - )); + image.set(x, y, getNormalColor(normal)); } else { image.set(x, y, getSkyboxColor(ray)); } @@ -46,6 +40,27 @@ public record Scene(@NotNull List<@NotNull Shape> shapes) { return image; } + private @NotNull Optional hit(@NotNull Ray ray) { + var range = new Range(0, Double.POSITIVE_INFINITY); + var result = (HitResult) null; + for (var shape : shapes) { + var r = shape.hit(ray, range); + if (r.isPresent() && (result == null || r.get().t() < result.t())) { + result = r.get(); + range = new Range(0, result.t()); + } + } + return Optional.ofNullable(result); + } + + private @NotNull Color getNormalColor(@NotNull Vec3 normal) { + return new Color( + 0.5 * (normal.x() + 1), + 0.5 * (normal.y() + 1), + 0.5 * (normal.z() + 1) + ); + } + private @NotNull Color getSkyboxColor(@NotNull Ray ray) { // altitude from -pi/2 to pi/2 var alt = Math.copySign( diff --git a/src/main/java/eu/jonahbauer/raytracing/shape/Shape.java b/src/main/java/eu/jonahbauer/raytracing/shape/Shape.java index a6b2e57..43574d5 100644 --- a/src/main/java/eu/jonahbauer/raytracing/shape/Shape.java +++ b/src/main/java/eu/jonahbauer/raytracing/shape/Shape.java @@ -1,5 +1,6 @@ package eu.jonahbauer.raytracing.shape; +import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; import org.jetbrains.annotations.NotNull; @@ -12,5 +13,5 @@ public sealed interface Shape permits Sphere { * the ray origin, or Double.NaN if the ray does not intersect this shape} * @param ray a ray */ - @NotNull Optional hit(@NotNull Ray ray); + @NotNull Optional hit(@NotNull Ray ray, @NotNull Range range); } diff --git a/src/main/java/eu/jonahbauer/raytracing/shape/Sphere.java b/src/main/java/eu/jonahbauer/raytracing/shape/Sphere.java index e105f8a..c1763f7 100644 --- a/src/main/java/eu/jonahbauer/raytracing/shape/Sphere.java +++ b/src/main/java/eu/jonahbauer/raytracing/shape/Sphere.java @@ -1,5 +1,6 @@ package eu.jonahbauer.raytracing.shape; +import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; import org.jetbrains.annotations.NotNull; @@ -20,7 +21,7 @@ public record Sphere(@NotNull Vec3 center, double radius) implements Shape { } @Override - public @NotNull Optional hit(@NotNull Ray ray) { + public @NotNull Optional hit(@NotNull Ray ray, @NotNull Range range) { var oc = ray.origin().minus(center()); var a = ray.direction().squared(); @@ -33,8 +34,8 @@ public record Sphere(@NotNull Vec3 center, double radius) implements Shape { var sd = Math.sqrt(discriminant); double t = (- h - sd) / a; - if (t < 0) t = (- h + sd) / a; - if (t < 0) return Optional.empty(); + if (!range.surrounds(t)) t = (- h + sd) / a; + if (!range.surrounds(t)) return Optional.empty(); return Optional.of(new HitResult(t, ray.at(t).minus(center))); } diff --git a/src/test/java/eu/jonahbauer/raytracing/shape/SphereTest.java b/src/test/java/eu/jonahbauer/raytracing/shape/SphereTest.java index 6fe83f5..ac3fe8a 100644 --- a/src/test/java/eu/jonahbauer/raytracing/shape/SphereTest.java +++ b/src/test/java/eu/jonahbauer/raytracing/shape/SphereTest.java @@ -1,5 +1,6 @@ package eu.jonahbauer.raytracing.shape; +import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; import org.junit.jupiter.api.Test; @@ -18,7 +19,7 @@ class SphereTest { var direction = new Vec3(-1, -1, -1); var ray = new Ray(origin, direction); - var result = sphere.hit(ray); + var result = sphere.hit(ray, Range.NON_NEGATIVE); assertFalse(result.isEmpty()); assertEquals(center.plus(new Vec3(1, 1, 1).unit().times(radius)), ray.at(result.get().t())); }