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 2158740..9846703 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/camera/Camera.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/camera/Camera.java @@ -18,7 +18,13 @@ public interface Camera { /** * Casts a ray through the given pixel. + * @param x the pixel x coordinate + * @param y the pixel y coordinate + * @param i the subpixel x coordinate + * @param j the subpixel y coordinate + * @param n the subpixel count (per side) + * @param random a random number generator * @return a new ray */ - @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random); + @NotNull Ray cast(int x, int y, int i, int j, int n, @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 5984104..a26686e 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java @@ -79,13 +79,20 @@ public final class SimpleCamera implements Camera { /** * {@inheritDoc} + * @param x {@inheritDoc} + * @param y {@inheritDoc} + * @param i {@inheritDoc} + * @param j {@inheritDoc} + * @param n {@inheritDoc} + * @param random {@inheritDoc} */ - public @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random) { + @Override + public @NotNull Ray cast(int x, int y, int i, int j, int n, @NotNull RandomGenerator random) { Objects.checkIndex(x, width); Objects.checkIndex(y, height); var origin = getRayOrigin(random); - var target = getRayTarget(x, y, random); + var target = getRayTarget(x, y, i, j, n, random); return new Ray(origin, target.minus(origin)); } @@ -98,8 +105,8 @@ public final class SimpleCamera implements Camera { if (blurRadius <= 0) return origin; while (true) { - var du = 2 * random.nextDouble() - 1; - var dv = 2 * random.nextDouble() - 1; + var du = Math.fma(2, random.nextDouble(), -1); + var dv = Math.fma(2, random.nextDouble(), -1); if (du * du + dv * dv >= 1) continue; var ru = blurRadius * du; @@ -116,9 +123,10 @@ 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, @NotNull RandomGenerator random) { - double dx = x + random.nextDouble() - 0.5; - double dy = y + random.nextDouble() - 0.5; + private @NotNull Vec3 getRayTarget(int x, int y, int i, int j, int n, @NotNull RandomGenerator random) { + var factor = 1d / n; + var dx = x + Math.fma(factor, i + random.nextDouble(), -0.5); + var dy = y + Math.fma(factor, j + random.nextDouble(), -0.5); return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy)); } 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 7c90722..d525ac4 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java @@ -14,7 +14,7 @@ import java.util.random.RandomGenerator; import java.util.stream.IntStream; public final class SimpleRenderer implements Renderer { - private final int samplesPerPixel; + private final int sqrtSamplesPerPixel; private final int maxDepth; private final double gamma; @@ -30,7 +30,7 @@ public final class SimpleRenderer implements Renderer { } private SimpleRenderer(@NotNull Builder builder) { - this.samplesPerPixel = builder.samplesPerPixel; + this.sqrtSamplesPerPixel = (int) Math.sqrt(builder.samplesPerPixel); this.maxDepth = builder.maxDepth; this.gamma = builder.gamma; @@ -38,6 +38,9 @@ public final class SimpleRenderer implements Renderer { this.iterative = builder.iterative; } + /** + * {@inheritDoc} + */ @Override public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) { if (canvas.getWidth() != camera.getWidth() || canvas.getHeight() != camera.getHeight()) { @@ -45,42 +48,66 @@ public final class SimpleRenderer implements Renderer { } if (iterative) { - var random = new Random(); + renderIterative(camera, scene, canvas); + } else { + renderNonIterative(camera, scene, canvas); + } + } - // render one sample after the other - for (int i = 1 ; i <= samplesPerPixel; i++) { - var sample = i; + /** + * Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}, taking one sample per pixel at + * a time and updating the canvas after each sample. + */ + private void renderIterative(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) { + var random = new Random(); + + // render one sample after the other + int i = 0; + for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) { + for (int si = 0; si < sqrtSamplesPerPixel; si++) { + var sample = ++i; + var sif = si; + var sjf = sj; getScanlineStream(camera.getHeight(), parallel).forEach(y -> { for (int x = 0; x < camera.getWidth(); x++) { - var ray = camera.cast(x, y, random); + var ray = camera.cast(x, y, sif, sjf, sqrtSamplesPerPixel, random); var c = getColor(scene, ray, random); canvas.set(x, y, Color.average(canvas.get(x, y), c, sample)); } }); } + } - // apply gamma correction - 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 - 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); + // apply gamma correction + 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)); + } + }); + } + + /** + * Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}, taking some amount of samples + * per pixel and updating the canvas after each pixel. + */ + private void renderNonIterative(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) { + var splittable = new SplittableRandom(); + // render one pixel after the other + getScanlineStream(camera.getHeight(), parallel).forEach(y -> { + var random = splittable.split(); + for (int x = 0; x < camera.getWidth(); x++) { + var color = Color.BLACK; + int i = 0; + for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) { + for (int si = 0; si < sqrtSamplesPerPixel; si++) { + var ray = camera.cast(x, y, si, sj, sqrtSamplesPerPixel, random); var c = getColor(scene, ray, random); - color = Color.average(color, c, i); + color = Color.average(color, c, ++i); } - canvas.set(x, y, Color.gamma(color, gamma)); } - }); - } + canvas.set(x, y, Color.gamma(color, gamma)); + } + }); } /**