diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java index b673af4..5de8a45 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Main.java +++ b/src/main/java/eu/jonahbauer/raytracing/Main.java @@ -5,11 +5,14 @@ import eu.jonahbauer.raytracing.render.Color; import eu.jonahbauer.raytracing.render.ImageFormat; import eu.jonahbauer.raytracing.render.camera.Camera; import eu.jonahbauer.raytracing.render.camera.SimpleCamera; +import eu.jonahbauer.raytracing.render.canvas.Canvas; import eu.jonahbauer.raytracing.render.canvas.Image; import eu.jonahbauer.raytracing.render.canvas.LiveCanvas; import eu.jonahbauer.raytracing.render.material.*; import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer; -import eu.jonahbauer.raytracing.scene.*; +import eu.jonahbauer.raytracing.scene.Hittable; +import eu.jonahbauer.raytracing.scene.Scene; +import eu.jonahbauer.raytracing.scene.SkyBox; import eu.jonahbauer.raytracing.scene.hittable2d.Parallelogram; import eu.jonahbauer.raytracing.scene.hittable3d.ConstantMedium; import eu.jonahbauer.raytracing.scene.hittable3d.Sphere; @@ -17,185 +20,287 @@ import eu.jonahbauer.raytracing.scene.util.Hittables; import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Random; +import java.util.function.IntFunction; public class Main { public static void main(String[] args) throws IOException { - var example = getCornellBoxSmoke(); + var config = Config.parse(args); + var example = config.example; var scene = example.scene(); var camera = example.camera(); var renderer = SimpleRenderer.builder() - .withSamplesPerPixel(1000) - .withMaxDepth(50) - .withIterative(true) + .withSamplesPerPixel(config.samples) + .withMaxDepth(config.depth) + .withIterative(config.iterative) .build(); - var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight())); - image.preview(); + Canvas canvas; + if (config.preview) { + var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight())); + image.preview(); + canvas = image; + } else { + canvas = new Image(camera.getWidth(), camera.getHeight()); + } long time = System.nanoTime(); - renderer.render(camera, scene, image); + renderer.render(camera, scene, canvas); System.out.printf("rendering finished after %dms", (System.nanoTime() - time) / 1_000_000); - ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png")); + ImageFormat.PNG.write(canvas, config.path); } - private static @NotNull Example getSpheres() { - var rng = new Random(1); - var objects = new ArrayList(); - objects.add(new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.5, 0.5, 0.5)))); - - for (int a = -11; a < 11; a++) { - for (int b = -11; b < 11; b++) { - var center = new Vec3(a + 0.9 * rng.nextDouble(), 0.2, b + 0.9 * rng.nextDouble()); - if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue; - - Material material; - var rnd = rng.nextDouble(); - if (rnd < 0.8) { - // diffuse - var albedo = Color.multiply(Color.random(rng), Color.random(rng)); - material = new LambertianMaterial(albedo); - } else if (rnd < 0.95) { - // metal - var albedo = Color.random(rng, 0.5, 1.0); - var fuzz = rng.nextDouble() * 0.5; - material = new MetallicMaterial(albedo, fuzz); - } else { - // glass - material = new DielectricMaterial(1.5); - } + private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, int samples, int depth) { - objects.add(new Sphere(center, 0.2, material)); + public static @NotNull Config parse(@NotNull String @NotNull[] args) { + IntFunction example = null; + Path path = null; + boolean preview = true; + boolean iterative = false; + int samples = 1000; + int depth = 50; + int height = -1; + + for (int i = 0; i < args.length; i++) { + switch (args[i]) { + case "--output" -> { + if (i + 1 == args.length) throw fail("missing value for parameter --output"); + try { + path = Path.of(args[++i]); + } catch (InvalidPathException ex) { + throw fail("value " + args[i] + " is not a valid path"); + } + } + case "--preview" -> preview = true; + case "--no-preview" -> preview = false; + case "--iterative" -> iterative = true; + case "--no-iterative" -> iterative = false; + case "--samples" -> { + if (i + 1 == args.length) throw fail("missing value for parameter --samples"); + try { + samples = Integer.parseInt(args[++i]); + if (samples <= 0) throw fail("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 { + depth = Integer.parseInt(args[++i]); + if (depth <= 0) throw fail("depth must be positive"); + } catch (NumberFormatException ex) { + throw fail("value " + args[i] + " is not a valid integer"); + } + } + case "--height" -> { + if (i + 1 == args.length) throw fail("missing value for parameter --height"); + try { + height = Integer.parseInt(args[++i]); + if (height <= 0) throw fail("height must be positive"); + } catch (NumberFormatException ex) { + throw fail("value " + args[i] + " is not a valid integer"); + } + } + case String str when !str.startsWith("-") -> example = switch (str) { + case "SIMPLE" -> Examples::getSimpleScene; + case "SPHERES" -> Examples::getSpheres; + case "SQUARES" -> Examples::getSquares; + case "LIGHT" -> Examples::getLight; + case "CORNELL" -> Examples::getCornellBox; + case "CORNELL_SMOKE" -> Examples::getCornellBoxSmoke; + default -> throw fail("unknown example " + str + ", expected one of SIMPLE, SPHERES, SQUARES, LIGHT, CORNELL or CORNELL_SMOKE"); + }; + default -> throw fail("unknown option " + args[i]); + } } + + 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, samples, depth); } - objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5))); - objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1)))); - objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5)))); - - var camera = SimpleCamera.builder() - .withImage(1200, 675) - .withPosition(new Vec3(13, 2, 3)) - .withTarget(new Vec3(0, 0, 0)) - .withFieldOfView(Math.toRadians(20)) - .withFocusDistance(10.0) - .withBlurAngle(Math.toRadians(0.6)) - .build(); + private static @NotNull RuntimeException fail(@NotNull String message) { + System.err.println(message); + System.exit(1); + return new RuntimeException(); + } - return new Example(new Scene(getSkyBox(), objects), camera); } - private static @NotNull Example getSquares() { - return new Example( - new Scene( - getSkyBox(), - new Parallelogram(new Vec3(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(1.0, 0.2, 0.2))), - new Parallelogram(new Vec3(-2, -2, 0), new Vec3(4, 0, 0), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 1.0, 0.2))), - new Parallelogram(new Vec3(3, -2, 1), new Vec3(0, 0, 4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 0.2, 1.0))), - new Parallelogram(new Vec3(-2, 3, 1), new Vec3(4, 0, 0), new Vec3(0, 0, 4), new LambertianMaterial(new Color(1.0, 0.5, 0.0))), - new Parallelogram(new Vec3(-2, -3, 5), new Vec3(4, 0, 0), new Vec3(0, 0, -4), new LambertianMaterial(new Color(0.2, 0.8, 0.8))) - ), - SimpleCamera.builder() - .withImage(400, 400) - .withFieldOfView(Math.toRadians(80)) - .withPosition(new Vec3(0, 0, 9)) - .withTarget(new Vec3(0, 0, 0)) - .build() - ); - } + private static class Examples { + public static @NotNull Example getSimpleScene(int height) { + if (height <= 0) height = 675; + return new Example( + new Scene( + getSkyBox(), + new Sphere(new Vec3(0, -100.5, -1.0), 100.0, new LambertianMaterial(new Color(0.8, 0.8, 0.0))), + new Sphere(new Vec3(0, 0, -1.2), 0.5, new LambertianMaterial(new Color(0.1, 0.2, 0.5))), + new Sphere(new Vec3(-1.0, 0, -1.2), 0.5, new DielectricMaterial(1.5)), + new Sphere(new Vec3(-1.0, 0, -1.2), 0.4, new DielectricMaterial(1 / 1.5)), + new Sphere(new Vec3(1.0, 0, -1.2), 0.5, new MetallicMaterial(new Color(0.8, 0.6, 0.2), 0.0)) + ), + SimpleCamera.builder() + .withImage(height * 16 / 9, height) + .build() + ); + } - private static @NotNull Example getLight() { - return new Example( - new Scene( - new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.2, 0.2, 0.2))), - new Sphere(new Vec3(0, 2, 0), 2, new LambertianMaterial(new Color(0.2, 0.2, 0.2))), - new Parallelogram(new Vec3(3, 1, -2), new Vec3(2, 0, 0), new Vec3(0, 2, 0), new DiffuseLight(new Color(4.0, 4.0, 4.0))), - new Sphere(new Vec3(0, 7, 0), 2, new DiffuseLight(new Color(4.0, 4.0, 4.0))) - ), - SimpleCamera.builder() - .withImage(400, 225) - .withFieldOfView(Math.toRadians(20)) - .withPosition(new Vec3(26, 3, 6)) - .withTarget(new Vec3(0, 2, 0)) - .build() - ); - } + public static @NotNull Example getSpheres(int height) { + if (height <= 0) height = 675; - private static @NotNull Example getCornellBox() { - var red = new LambertianMaterial(new Color(.65, .05, .05)); - var white = new LambertianMaterial(new Color(.73, .73, .73)); - var green = new LambertianMaterial(new Color(.12, .45, .15)); - var light = new DiffuseLight(new Color(15.0, 15.0, 15.0)); - - return new Example( - new Scene( - new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green), - new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red), - new Parallelogram(new Vec3(343, 554, 332), new Vec3(-130, 0, 0), new Vec3(0, 0, -105), light), - new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0 ,0), new Vec3(0, 0, 555), white), - new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0 ,0), new Vec3(0, 0, -555), white), - new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0 ,0), new Vec3(0, 555, 0), white), - Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)), - Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)) - ), - SimpleCamera.builder() - .withImage(600, 600) - .withFieldOfView(Math.toRadians(40)) - .withPosition(new Vec3(278, 278, -800)) - .withTarget(new Vec3(278, 278, 0)) - .build() - ); - } + var rng = new Random(1); + var objects = new ArrayList(); + objects.add(new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.5, 0.5, 0.5)))); - private static @NotNull Example getCornellBoxSmoke() { - var red = new LambertianMaterial(new Color(.65, .05, .05)); - var white = new LambertianMaterial(new Color(.73, .73, .73)); - var green = new LambertianMaterial(new Color(.12, .45, .15)); - var light = new DiffuseLight(new Color(7.0, 7.0, 7.0)); - - return new Example( - new Scene( - new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green), - new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red), - new Parallelogram(new Vec3(113, 554, 127), new Vec3(330, 0, 0), new Vec3(0, 0, 305), light), - new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0 ,0), new Vec3(0, 0, 555), white), - new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0 ,0), new Vec3(0, 0, -555), white), - new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0 ,0), new Vec3(0, 555, 0), white), - new ConstantMedium( - Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)), - 0.01, new IsotropicMaterial(Color.BLACK) - ), - new ConstantMedium( - Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)), - 0.01, new IsotropicMaterial(Color.WHITE) - ) - ), - SimpleCamera.builder() - .withImage(600, 600) - .withFieldOfView(Math.toRadians(40)) - .withPosition(new Vec3(278, 278, -800)) - .withTarget(new Vec3(278, 278, 0)) - .build() - ); - } + for (int a = -11; a < 11; a++) { + for (int b = -11; b < 11; b++) { + var center = new Vec3(a + 0.9 * rng.nextDouble(), 0.2, b + 0.9 * rng.nextDouble()); + if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue; - private static @NotNull Scene getSimpleScene() { - return new Scene( - getSkyBox(), - new Sphere(new Vec3(0, -100.5, -1.0), 100.0, new LambertianMaterial(new Color(0.8, 0.8, 0.0))), - new Sphere(new Vec3(0, 0, -1.2), 0.5, new LambertianMaterial(new Color(0.1, 0.2, 0.5))), - new Sphere(new Vec3(-1.0, 0, -1.2), 0.5, new DielectricMaterial(1.5)), - new Sphere(new Vec3(-1.0, 0, -1.2), 0.4, new DielectricMaterial(1 / 1.5)), - new Sphere(new Vec3(1.0, 0, -1.2), 0.5, new MetallicMaterial(new Color(0.8, 0.6, 0.2), 0.0)) - ); - } + Material material; + var rnd = rng.nextDouble(); + if (rnd < 0.8) { + // diffuse + var albedo = Color.multiply(Color.random(rng), Color.random(rng)); + material = new LambertianMaterial(albedo); + } else if (rnd < 0.95) { + // metal + var albedo = Color.random(rng, 0.5, 1.0); + var fuzz = rng.nextDouble() * 0.5; + material = new MetallicMaterial(albedo, fuzz); + } else { + // glass + material = new DielectricMaterial(1.5); + } + + objects.add(new Sphere(center, 0.2, material)); + } + } - private static @NotNull SkyBox getSkyBox() { - return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE); + objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5))); + objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1)))); + objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5)))); + + var camera = SimpleCamera.builder() + .withImage(height * 16 / 9, height) + .withPosition(new Vec3(13, 2, 3)) + .withTarget(new Vec3(0, 0, 0)) + .withFieldOfView(Math.toRadians(20)) + .withFocusDistance(10.0) + .withBlurAngle(Math.toRadians(0.6)) + .build(); + + return new Example(new Scene(getSkyBox(), objects), camera); + } + + public static @NotNull Example getSquares(int height) { + if (height <= 0) height = 600; + return new Example( + new Scene( + getSkyBox(), + new Parallelogram(new Vec3(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(1.0, 0.2, 0.2))), + new Parallelogram(new Vec3(-2, -2, 0), new Vec3(4, 0, 0), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 1.0, 0.2))), + new Parallelogram(new Vec3(3, -2, 1), new Vec3(0, 0, 4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 0.2, 1.0))), + new Parallelogram(new Vec3(-2, 3, 1), new Vec3(4, 0, 0), new Vec3(0, 0, 4), new LambertianMaterial(new Color(1.0, 0.5, 0.0))), + new Parallelogram(new Vec3(-2, -3, 5), new Vec3(4, 0, 0), new Vec3(0, 0, -4), new LambertianMaterial(new Color(0.2, 0.8, 0.8))) + ), + SimpleCamera.builder() + .withImage(height, height) + .withFieldOfView(Math.toRadians(80)) + .withPosition(new Vec3(0, 0, 9)) + .withTarget(new Vec3(0, 0, 0)) + .build() + ); + } + + public static @NotNull Example getLight(int height) { + if (height <= 0) height = 225; + return new Example( + new Scene( + new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.2, 0.2, 0.2))), + new Sphere(new Vec3(0, 2, 0), 2, new LambertianMaterial(new Color(0.2, 0.2, 0.2))), + new Parallelogram(new Vec3(3, 1, -2), new Vec3(2, 0, 0), new Vec3(0, 2, 0), new DiffuseLight(new Color(4.0, 4.0, 4.0))), + new Sphere(new Vec3(0, 7, 0), 2, new DiffuseLight(new Color(4.0, 4.0, 4.0))) + ), + SimpleCamera.builder() + .withImage(height * 16 / 9, height) + .withFieldOfView(Math.toRadians(20)) + .withPosition(new Vec3(26, 3, 6)) + .withTarget(new Vec3(0, 2, 0)) + .build() + ); + } + + public static @NotNull Example getCornellBox(int height) { + if (height <= 0) height = 600; + + var red = new LambertianMaterial(new Color(.65, .05, .05)); + var white = new LambertianMaterial(new Color(.73, .73, .73)); + var green = new LambertianMaterial(new Color(.12, .45, .15)); + var light = new DiffuseLight(new Color(15.0, 15.0, 15.0)); + + return new Example( + new Scene( + new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green), + new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red), + new Parallelogram(new Vec3(343, 554, 332), new Vec3(-130, 0, 0), new Vec3(0, 0, -105), light), + new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0 ,0), new Vec3(0, 0, 555), white), + new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0 ,0), new Vec3(0, 0, -555), white), + new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0 ,0), new Vec3(0, 555, 0), white), + Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)), + Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)) + ), + SimpleCamera.builder() + .withImage(height, height) + .withFieldOfView(Math.toRadians(40)) + .withPosition(new Vec3(278, 278, -800)) + .withTarget(new Vec3(278, 278, 0)) + .build() + ); + } + + public static @NotNull Example getCornellBoxSmoke(int height) { + if (height <= 0) height = 600; + var red = new LambertianMaterial(new Color(.65, .05, .05)); + var white = new LambertianMaterial(new Color(.73, .73, .73)); + var green = new LambertianMaterial(new Color(.12, .45, .15)); + var light = new DiffuseLight(new Color(7.0, 7.0, 7.0)); + + return new Example( + new Scene( + new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green), + new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red), + new Parallelogram(new Vec3(113, 554, 127), new Vec3(330, 0, 0), new Vec3(0, 0, 305), light), + new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0 ,0), new Vec3(0, 0, 555), white), + new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0 ,0), new Vec3(0, 0, -555), white), + new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0 ,0), new Vec3(0, 555, 0), white), + new ConstantMedium( + Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)), + 0.01, new IsotropicMaterial(Color.BLACK) + ), + new ConstantMedium( + Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)), + 0.01, new IsotropicMaterial(Color.WHITE) + ) + ), + SimpleCamera.builder() + .withImage(height, height) + .withFieldOfView(Math.toRadians(40)) + .withPosition(new Vec3(278, 278, -800)) + .withTarget(new Vec3(278, 278, 0)) + .build() + ); + } + + private static @NotNull SkyBox getSkyBox() { + return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE); + } } private record Example(@NotNull Scene scene, @NotNull Camera camera) {}