From 9ebf8bd1c4eb338137283a8dbbf2ccdb863c9106 Mon Sep 17 00:00:00 2001 From: jbb01 <32650546+jbb01@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:17:06 +0200 Subject: [PATCH] use a more performant RandomGenerator --- .../eu/jonahbauer/raytracing/math/Vec3.java | 17 ++--- .../raytracing/render/camera/Camera.java | 4 +- .../render/camera/SimpleCamera.java | 19 +++--- .../render/material/DielectricMaterial.java | 5 +- .../render/material/DiffuseLight.java | 3 +- .../render/material/IsotropicMaterial.java | 5 +- .../render/material/LambertianMaterial.java | 5 +- .../raytracing/render/material/Material.java | 3 +- .../render/material/MetallicMaterial.java | 5 +- .../render/renderer/SimpleRenderer.java | 63 ++++++++++--------- 10 files changed, 71 insertions(+), 58 deletions(-) diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java b/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java index 56da0b0..0ddc26a 100644 --- a/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java +++ b/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java @@ -3,6 +3,7 @@ package eu.jonahbauer.raytracing.math; import org.jetbrains.annotations.NotNull; import java.util.Optional; +import java.util.random.RandomGenerator; public record Vec3(double x, double y, double z) { public static final Vec3 ZERO = new Vec3(0, 0, 0); @@ -16,17 +17,17 @@ public record Vec3(double x, double y, double z) { assert Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z) : "x, y and z must be finite"; } - public static @NotNull Vec3 random() { - return random(false); + public static @NotNull Vec3 random(@NotNull RandomGenerator random) { + return random(random, false); } - public static @NotNull Vec3 random(boolean unit) { - var random = new Vec3( - 2 * Math.random() - 1, - 2 * Math.random() - 1, - 2 * Math.random() - 1 + public static @NotNull Vec3 random(@NotNull RandomGenerator random, boolean unit) { + var vec = new Vec3( + 2 * random.nextDouble() - 1, + 2 * random.nextDouble() - 1, + 2 * random.nextDouble() - 1 ); - return unit ? random.unit() : random; + return unit ? vec.unit() : vec; } public static @NotNull Vec3 reflect(@NotNull Vec3 vec, @NotNull Vec3 normal) { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/camera/Camera.java b/src/main/java/eu/jonahbauer/raytracing/render/camera/Camera.java index ccac043..2158740 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/camera/Camera.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/camera/Camera.java @@ -3,6 +3,8 @@ package eu.jonahbauer.raytracing.render.camera; import eu.jonahbauer.raytracing.math.Ray; import org.jetbrains.annotations.NotNull; +import java.util.random.RandomGenerator; + public interface Camera { /** * {@return the width of this camera in pixels} @@ -18,5 +20,5 @@ public interface Camera { * Casts a ray through the given pixel. * @return a new ray */ - @NotNull Ray cast(int x, int y); + @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java b/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java index a8c1f79..5984104 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; +import java.util.random.RandomGenerator; public final class SimpleCamera implements Camera { // image size @@ -79,12 +80,12 @@ public final class SimpleCamera implements Camera { /** * {@inheritDoc} */ - public @NotNull Ray cast(int x, int y) { + public @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random) { Objects.checkIndex(x, width); Objects.checkIndex(y, height); - var origin = getRayOrigin(); - var target = getRayTarget(x, y); + var origin = getRayOrigin(random); + var target = getRayTarget(x, y, random); return new Ray(origin, target.minus(origin)); } @@ -93,12 +94,12 @@ public final class SimpleCamera implements Camera { * radius {@link #blurRadius} centered on the camera position and perpendicular to the direction to simulate depth * of field. */ - private @NotNull Vec3 getRayOrigin() { + private @NotNull Vec3 getRayOrigin(@NotNull RandomGenerator random) { if (blurRadius <= 0) return origin; while (true) { - var du = 2 * Math.random() - 1; - var dv = 2 * Math.random() - 1; + var du = 2 * random.nextDouble() - 1; + var dv = 2 * random.nextDouble() - 1; if (du * du + dv * dv >= 1) continue; var ru = blurRadius * du; @@ -115,9 +116,9 @@ public final class SimpleCamera implements Camera { /** * {@return the target vector for a ray through the given pixel} The position is randomized within the pixel. */ - private @NotNull Vec3 getRayTarget(int x, int y) { - double dx = x + Math.random() - 0.5; - double dy = y + Math.random() - 0.5; + private @NotNull Vec3 getRayTarget(int x, int y, @NotNull RandomGenerator random) { + double dx = x + random.nextDouble() - 0.5; + double dy = y + random.nextDouble() - 0.5; return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy)); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java index 8e22c6f..03b3502 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.Optional; +import java.util.random.RandomGenerator; public record DielectricMaterial(double refractionIndex, @NotNull Color albedo) implements Material { public DielectricMaterial(double refractionIndex) { @@ -19,12 +20,12 @@ public record DielectricMaterial(double refractionIndex, @NotNull Color albedo) } @Override - public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit) { + public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { var ri = hit.frontFace() ? (1 / refractionIndex) : refractionIndex; var cosTheta = Math.min(- ray.direction().unit().times(hit.normal()), 1.0); var reflectance = reflectance(cosTheta); - var reflect = reflectance > Math.random(); + var reflect = reflectance > random.nextDouble(); var newDirection = (reflect ? Optional.empty() : Vec3.refract(ray.direction(), hit.normal(), ri)) .orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal())); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/DiffuseLight.java b/src/main/java/eu/jonahbauer/raytracing/render/material/DiffuseLight.java index 74ae62e..f89e348 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DiffuseLight.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DiffuseLight.java @@ -6,10 +6,11 @@ import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; import java.util.Optional; +import java.util.random.RandomGenerator; public record DiffuseLight(@NotNull Color emit) implements Material { @Override - public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit) { + public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { return Optional.empty(); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java index 549992c..74e0ecf 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java @@ -7,10 +7,11 @@ import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; import java.util.Optional; +import java.util.random.RandomGenerator; public record IsotropicMaterial(@NotNull Color albedo) implements Material{ @Override - public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit) { - return Optional.of(new ScatterResult(new Ray(hit.position(), Vec3.random(true)), albedo())); + public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { + return Optional.of(new ScatterResult(new Ray(hit.position(), Vec3.random(random, true)), albedo())); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java index 9be0fcd..ded69c7 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.Optional; +import java.util.random.RandomGenerator; public record LambertianMaterial(@NotNull Color albedo) implements Material { public LambertianMaterial { @@ -15,8 +16,8 @@ public record LambertianMaterial(@NotNull Color albedo) implements Material { } @Override - public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit) { - var newDirection = hit.normal().plus(Vec3.random(true)); + public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { + var newDirection = hit.normal().plus(Vec3.random(random, true)); if (newDirection.isNearZero()) newDirection = hit.normal(); var scattered = new Ray(hit.position(), newDirection); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java b/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java index 955e5b1..85f46c8 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java @@ -7,10 +7,11 @@ import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.Optional; +import java.util.random.RandomGenerator; public interface Material { - @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit); + @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random); default @NotNull Color emitted(@NotNull HitResult hit) { return Color.BLACK; diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java index d75fa93..2ff15ee 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull; import java.util.Objects; import java.util.Optional; +import java.util.random.RandomGenerator; public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Material { @@ -21,10 +22,10 @@ public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Ma } @Override - public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit) { + public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { var newDirection = Vec3.reflect(ray.direction(), hit.normal()); if (fuzz > 0) { - newDirection = newDirection.unit().plus(Vec3.random(true).times(fuzz)); + newDirection = newDirection.unit().plus(Vec3.random(random, true).times(fuzz)); } return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), albedo)); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java index 527111e..d503194 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java @@ -8,9 +8,10 @@ import eu.jonahbauer.raytracing.render.canvas.Canvas; import eu.jonahbauer.raytracing.scene.Scene; import org.jetbrains.annotations.NotNull; -import java.util.function.Function; +import java.util.Random; +import java.util.SplittableRandom; +import java.util.random.RandomGenerator; import java.util.stream.IntStream; -import java.util.stream.LongStream; public final class SimpleRenderer implements Renderer { private final int samplesPerPixel; @@ -44,36 +45,40 @@ public final class SimpleRenderer implements Renderer { } if (iterative) { + var random = new Random(); + // render one sample after the other for (int i = 1 ; i <= samplesPerPixel; i++) { var sample = i; - getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> { - var y = (int) (pixel >> 32); - var x = (int) pixel; - var ray = camera.cast(x, y); - var c = getColor(scene, ray); - canvas.set(x, y, Color.average(canvas.get(x, y), c, sample)); + getScanlineStream(camera.getHeight(), parallel).forEach(y -> { + for (int x = 0; x < camera.getWidth(); x++) { + var ray = camera.cast(x, y, random); + var c = getColor(scene, ray, random); + canvas.set(x, y, Color.average(canvas.get(x, y), c, sample)); + } }); } + // apply gamma correction - getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> { - var y = (int) (pixel >> 32); - var x = (int) pixel; - canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma)); + getScanlineStream(camera.getHeight(), parallel).forEach(y -> { + for (int x = 0; x < camera.getWidth(); x++) { + canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma)); + } }); } else { + var splittable = new SplittableRandom(); // render one pixel after the other - getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> { - var y = (int) (pixel >> 32); - var x = (int) pixel; - - var color = Color.BLACK; - for (int i = 1; i <= samplesPerPixel; i++) { - var ray = camera.cast(x, y); - var c = getColor(scene, ray); - color = Color.average(color, c, i); + getScanlineStream(camera.getHeight(), parallel).forEach(y -> { + var random = splittable.split(); + for (int x = 0; x < camera.getWidth(); x++) { + var color = Color.BLACK; + for (int i = 1; i <= samplesPerPixel; i++) { + var ray = camera.cast(x, y, random); + var c = getColor(scene, ray, random); + color = Color.average(color, c, i); + } + canvas.set(x, y, Color.gamma(color, gamma)); } - canvas.set(x, y, Color.gamma(color, gamma)); }); } } @@ -81,11 +86,11 @@ public final class SimpleRenderer implements Renderer { /** * {@return the color of the given ray in the given scene} */ - private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray) { - return getColor0(scene, ray, maxDepth); + private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray, @NotNull RandomGenerator random) { + return getColor0(scene, ray, maxDepth, random); } - private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth) { + private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) { var color = Color.BLACK; var attenuation = Color.WHITE; @@ -99,7 +104,7 @@ public final class SimpleRenderer implements Renderer { var hit = optional.get(); var material = hit.material(); var emitted = material.emitted(hit); - var scatter = material.scatter(ray, hit); + var scatter = material.scatter(ray, hit, random); color = Color.add(color, Color.multiply(attenuation, emitted)); if (scatter.isEmpty()) break; @@ -114,10 +119,8 @@ public final class SimpleRenderer implements Renderer { * {@return a stream of the pixels in a canvas with the given size} The pixels {@code x} and {@code y} coordinate * are encoded in the longs lower and upper 32 bits respectively. */ - private static @NotNull LongStream getPixelStream(int width, int height, boolean parallel) { - var stream = IntStream.range(0, height) - .mapToObj(y -> IntStream.range(0, width).mapToLong(x -> (long) y << 32 | x)) - .flatMapToLong(Function.identity()); + private static @NotNull IntStream getScanlineStream(int height, boolean parallel) { + var stream = IntStream.range(0, height); return parallel ? stream.parallel() : stream; }