diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java index 52dad4e..b6e8c92 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Main.java +++ b/src/main/java/eu/jonahbauer/raytracing/Main.java @@ -24,6 +24,7 @@ public class Main { var renderer = SimpleRenderer.builder() .withSamplesPerPixel(config.samples) + .withSpectralSamples(config.spectralSamples) .withMaxDepth(config.depth) .withIterative(config.iterative) .withParallel(config.parallel) @@ -45,7 +46,7 @@ public class Main { PNGImageWriter.sRGB.write(canvas, config.path); } - private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, boolean parallel, int samples, int depth) { + private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, boolean parallel, int samples, int spectralSamples, int depth) { public static @NotNull Config parse(@NotNull String @NotNull[] args) { IntFunction example = null; Path path = null; @@ -53,6 +54,7 @@ public class Main { boolean iterative = false; boolean parallel = false; int samples = 1000; + int spectralSamples = 4; int depth = 50; int height = -1; @@ -81,6 +83,15 @@ public class Main { throw fail("value " + args[i] + " is not a valid integer"); } } + case "--spectral-samples" -> { + if (i + 1 == args.length) throw fail("missing value for parameter --spectral-samples"); + try { + spectralSamples = Integer.parseInt(args[++i]); + if (spectralSamples <= 0) throw fail("spectral samples must be positive"); + } catch (NumberFormatException ex) { + throw fail("value " + args[i] + " is not a valid integer"); + } + } case "--depth" -> { if (i + 1 == args.length) throw fail("missing value for parameter --depth"); try { @@ -112,7 +123,7 @@ public class Main { if (example == null) example = Examples::getCornellBoxSmoke; if (path == null) path = Path.of("scene-" + System.currentTimeMillis() + ".png"); - return new Config(example.apply(height), path, preview, iterative, parallel, samples, depth); + return new Config(example.apply(height), path, preview, iterative, parallel, samples, spectralSamples, depth); } private static @NotNull RuntimeException fail(@NotNull String message) { diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Ray.java b/src/main/java/eu/jonahbauer/raytracing/math/Ray.java index d4a8ca9..acbd811 100644 --- a/src/main/java/eu/jonahbauer/raytracing/math/Ray.java +++ b/src/main/java/eu/jonahbauer/raytracing/math/Ray.java @@ -1,6 +1,7 @@ package eu.jonahbauer.raytracing.math; import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths; +import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -19,4 +20,16 @@ public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction, @NotNull Sample public @NotNull Vec3 at(double t) { return Vec3.fma(t, direction, origin); } + + public @NotNull Ray with(@NotNull HitResult hit, @NotNull Vec3 direction) { + return new Ray(hit.position(), direction, lambda); + } + + public @NotNull Ray with(@NotNull Vec3 origin, @NotNull Vec3 direction) { + return new Ray(origin, direction, lambda); + } + + public @NotNull Ray with(@NotNull SampledWavelengths lambda) { + return new Ray(origin, direction, lambda); + } } 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 d1f315e..84f40cd 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java @@ -2,7 +2,6 @@ package eu.jonahbauer.raytracing.render.camera; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; -import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -94,7 +93,7 @@ public final class SimpleCamera implements Camera { var origin = getRayOrigin(random); var target = getRayTarget(x, y, i, j, n, random); - return new Ray(origin, target.minus(origin), SampledWavelengths.uniform(random.nextDouble())); + return new Ray(origin, target.minus(origin)); } /** 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 55c2d72..e794497 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java @@ -45,7 +45,7 @@ public record DielectricMaterial(@NotNull RefractiveIndex ri, @NotNull Texture t .orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal())); var attenuation = texture.get(hit); - return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection, ray.lambda()))); + return Optional.of(new SpecularScatterResult(attenuation, ray.with(hit, newDirection))); } private double reflectance(double cos, double ri) { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java index 1f38660..36b3626 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java @@ -41,7 +41,7 @@ public final class DirectionalMaterial implements Material { if (back != null) return back.scatter(ray, hit, random); } // let the ray pass through without obstruction - return Optional.of(new SpecularScatterResult(Spectra.WHITE, new Ray(ray.at(hit.t()), ray.direction(), ray.lambda()))); + return Optional.of(new SpecularScatterResult(Spectra.WHITE, ray.with(hit, ray.direction()))); } @Override 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 bb48815..d2d8201 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java @@ -28,6 +28,6 @@ public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements newDirection = Vec3.fma(fuzz, Vec3.random(random), newDirection.unit()); } var attenuation = texture.get(hit); - return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection, ray.lambda()))); + return Optional.of(new SpecularScatterResult(attenuation, ray.with(hit, newDirection))); } } 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 0acce55..0766781 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java @@ -7,6 +7,7 @@ import eu.jonahbauer.raytracing.render.renderer.pdf.MixtureProbabilityDensityFun import eu.jonahbauer.raytracing.render.spectrum.SampledSpectrum; import eu.jonahbauer.raytracing.render.camera.Camera; import eu.jonahbauer.raytracing.render.canvas.Canvas; +import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths; import eu.jonahbauer.raytracing.scene.Scene; import org.jetbrains.annotations.NotNull; @@ -20,7 +21,10 @@ import static eu.jonahbauer.raytracing.Main.DEBUG; public final class SimpleRenderer implements Renderer { private final int sqrtSamplesPerPixel; private final int maxDepth; - private final double gamma; + + private final int spectralSamples; + private final SampledSpectrum black; + private final SampledSpectrum white; private final boolean parallel; private final boolean iterative; @@ -36,7 +40,10 @@ public final class SimpleRenderer implements Renderer { private SimpleRenderer(@NotNull Builder builder) { this.sqrtSamplesPerPixel = (int) Math.sqrt(builder.samplesPerPixel); this.maxDepth = builder.maxDepth; - this.gamma = builder.gamma; + + this.spectralSamples = builder.spectralSamples; + this.black = new SampledSpectrum(spectralSamples, 0); + this.white = new SampledSpectrum(spectralSamples, 1); this.parallel = builder.parallel; this.iterative = builder.iterative; @@ -96,7 +103,8 @@ public final class SimpleRenderer implements Renderer { 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 lambda = SampledWavelengths.uniform(random.nextDouble(), spectralSamples); + var ray = camera.cast(x, y, si, sj, sqrtSamplesPerPixel, random).with(lambda); if (DEBUG) { System.out.println("Casting ray " + ray + " through pixel (" + x + "," + y + ") at subpixel (" + si + "," + sj + ")..."); } @@ -116,8 +124,8 @@ public final class SimpleRenderer implements Renderer { } private @NotNull SampledSpectrum getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) { - var color = SampledSpectrum.BLACK; - var attenuation = SampledSpectrum.WHITE; + var color = black; + var attenuation = white; while (depth-- > 0) { var optional = scene.hit(ray); @@ -136,7 +144,7 @@ public final class SimpleRenderer implements Renderer { } var material = hit.material(); var emitted = material.emitted(hit).sample(ray.lambda()); - if (DEBUG && !SampledSpectrum.BLACK.equals(emitted)) { + if (DEBUG && !black.equals(emitted)) { System.out.println(" Emitted: " + emitted); } @@ -213,7 +221,7 @@ public final class SimpleRenderer implements Renderer { public static class Builder { private int samplesPerPixel = 100; private int maxDepth = 10; - private double gamma = 2.0; + private int spectralSamples = 4; private boolean parallel = true; private boolean iterative = false; @@ -229,9 +237,9 @@ public final class SimpleRenderer implements Renderer { return this; } - public @NotNull Builder withGamma(double gamma) { - if (gamma <= 0 || !Double.isFinite(gamma)) throw new IllegalArgumentException("gamma must be positive"); - this.gamma = gamma; + public @NotNull Builder withSpectralSamples(int samples) { + if (samples <= 0) throw new IllegalArgumentException("samples must be positive"); + this.spectralSamples = samples; return this; } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectrum/SampledSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectrum/SampledSpectrum.java index 1c6fa45..1fdd326 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectrum/SampledSpectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectrum/SampledSpectrum.java @@ -10,16 +10,6 @@ import java.util.Arrays; // TODO use Vector API to parallelize operations public final class SampledSpectrum implements IVec { - public static final SampledSpectrum BLACK; - public static final SampledSpectrum WHITE; - - static { - BLACK = new SampledSpectrum(new double[SampledWavelengths.SAMPLES]); - var one = new double[SampledWavelengths.SAMPLES]; - Arrays.fill(one, 1); - WHITE = new SampledSpectrum(one); - } - private final double @NotNull[] values; public SampledSpectrum(@NotNull SampledWavelengths lambdas, @NotNull Spectrum spectrum) { @@ -30,6 +20,12 @@ public final class SampledSpectrum implements IVec { this.values = values; } + public SampledSpectrum(int count, double value) { + var values = new double[count]; + Arrays.fill(values, value); + this.values = values; + } + private SampledSpectrum(double @NotNull[] values) { this.values = values; } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectrum/SampledWavelengths.java b/src/main/java/eu/jonahbauer/raytracing/render/spectrum/SampledWavelengths.java index d276768..60fb91c 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectrum/SampledWavelengths.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectrum/SampledWavelengths.java @@ -10,32 +10,31 @@ import java.util.Arrays; * A set of sampled wavelength that can be tracked together. */ public final class SampledWavelengths { - public static final int SAMPLES = 4; public static final SampledWavelengths EMPTY = new SampledWavelengths(new double[0], new double[0]); private final double @NotNull[] lambdas; private final double @NotNull[] pdf; - public static @NotNull SampledWavelengths uniform(double rng) { - return uniform(rng, Spectrum.LAMBDA_MIN, Spectrum.LAMBDA_MAX); + public static @NotNull SampledWavelengths uniform(double rng, int count) { + return uniform(rng, count, Spectrum.LAMBDA_MIN, Spectrum.LAMBDA_MAX); } - public static @NotNull SampledWavelengths uniform(double rng, double min, double max) { - var lambdas = new double[SAMPLES]; + public static @NotNull SampledWavelengths uniform(double rng, int count, double min, double max) { + var lambdas = new double[count]; // choose first sample at random lambdas[0] = (1 - rng) * min + rng * max; // choose next samples in equal intervals, wrapping if necessary - var delta = (max - min) / SAMPLES; - for (int i = 1; i < SAMPLES; i++) { + var delta = (max - min) / count; + for (int i = 1; i < count; i++) { lambdas[i] = lambdas[i - 1] + delta; if (lambdas[i] > max) { lambdas[i] = min + (lambdas[i] - max); } } - var pdf = new double[SAMPLES]; + var pdf = new double[count]; Arrays.fill(pdf, 1 / (max - min)); return new SampledWavelengths(lambdas, pdf); }