From d89d15f1a498140b55f67b54f197bce10fe77442 Mon Sep 17 00:00:00 2001 From: jbb01 <32650546+jbb01@users.noreply.github.com> Date: Sat, 3 Aug 2024 22:58:13 +0200 Subject: [PATCH] add Camera.Builder --- .../java/eu/jonahbauer/raytracing/Main.java | 12 +- .../jonahbauer/raytracing/render/Camera.java | 180 ++++++++++-------- 2 files changed, 109 insertions(+), 83 deletions(-) diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java index 3056b10..dab2326 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Main.java +++ b/src/main/java/eu/jonahbauer/raytracing/Main.java @@ -22,11 +22,13 @@ public class Main { new Sphere(-1, 0, - 1, 0.4, new DielectricMaterial(1 / 1.5)), new Sphere(1, 0, - 1, 0.5, new MetallicMaterial(new Color(0.8, 0.6, 0.2), 1.0)) ); - var camera = new Camera( - 800, 450, - 16d / 9 * 2, 2d, - Vec3.ZERO.plus(Vec3.UNIT_Z).plus(Vec3.UNIT_Y).minus(Vec3.UNIT_X), new Vec3(0.25, -0.5, - 1) - ); + + var camera = Camera.builder() + .withImage(800, 450) + .withPosition(new Vec3(-2, 2, 1)) + .withTarget(new Vec3(0, 0, -1)) + .withFieldOfView(Math.toRadians(20)) + .build(); var image = camera.render(scene); ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png")); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/Camera.java b/src/main/java/eu/jonahbauer/raytracing/render/Camera.java index 58e9e40..2a88475 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/Camera.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/Camera.java @@ -5,6 +5,7 @@ import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.scene.Scene; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Objects; @@ -13,77 +14,57 @@ public final class Camera { private final int width; private final int height; - // viewport size - private final double viewportWidth; - private final double viewportHeight; - // camera position and orientation private final @NotNull Vec3 origin; - private final @NotNull Vec3 direction; // rendering - private final int samplesPerPixel = 100; - private final int maxDepth = 10; - private final double gamma = 2.0; + private final int samplesPerPixel; + private final int maxDepth; + private final double gamma; // internal properties + private final @NotNull Vec3 u; + private final @NotNull Vec3 v; + private final @NotNull Vec3 pixelU; private final @NotNull Vec3 pixelV; private final @NotNull Vec3 pixel00; - /** - * Creates a new camera with the given dimensions at the origin facing towards positive z with a focal length of 1. - * @param height the image height - * @param viewportHeight the viewport height - * @param aspectRatio the aspect ratio - */ - public Camera(int height, double viewportHeight, double aspectRatio) { - this((int) (height * aspectRatio), height, viewportHeight * aspectRatio, viewportHeight); + public static @NotNull Builder builder() { + return new Builder(); } - /** - * Creates a new camera with the given dimensions at the origin facing towards positive z with a focal length of 1. - * @param width the image width - * @param height the image height - * @param viewportWidth the viewport width - * @param viewportHeight the viewport height - */ - public Camera(int width, int height, double viewportWidth, double viewportHeight) { - this(width, height, viewportWidth, viewportHeight, Vec3.ZERO, Vec3.UNIT_Z.neg()); + public Camera() { + this(new Builder()); } - public Camera( - int width, int height, - double viewportWidth, double viewportHeight, - @NotNull Vec3 origin, @NotNull Vec3 direction - ) { - if (width <= 0) throw new IllegalArgumentException("width must be positive"); - if (height <= 0) throw new IllegalArgumentException("height must be positive"); - if (viewportWidth <= 0 || !Double.isFinite(viewportWidth)) throw new IllegalArgumentException("viewportWidth must be positive"); - if (viewportHeight <= 0 || !Double.isFinite(viewportHeight)) throw new IllegalArgumentException("viewportHeight must be positive"); - - this.width = width; - this.height = height; - this.viewportWidth = viewportWidth; - this.viewportHeight = viewportHeight; - this.origin = Objects.requireNonNull(origin, "origin"); - this.direction = Objects.requireNonNull(direction, "direction"); - - // project direction onto xz-plane - var d = direction.unit(); - var dXZ = direction.withY(0).unit(); + private Camera(@NotNull Builder builder) { + this.width = builder.imageWidth; + this.height = builder.imageHeight; - var viewportU = new Vec3(- dXZ.z(), 0, dXZ.x()); // perpendicular to dXZ in xz-plane - var viewportV = d.cross(viewportU); // perpendicular to viewportU and direction + var viewportHeight = 2 * Math.tan(0.5 * builder.fov); + var viewportWidth = viewportHeight * ((double) width / height); - viewportU = viewportU.times(viewportWidth); // vector along the width of the viewport - viewportV = viewportV.times(viewportHeight); // vector along the height of the viewport + this.origin = builder.position; + var direction = (builder.direction == null ? builder.target.minus(builder.position).unit() : builder.direction); - this.pixelU = viewportU.div(width); - this.pixelV = viewportV.div(height); + this.samplesPerPixel = builder.samplePerPixel; + this.maxDepth = builder.maxDepth; + this.gamma = builder.gamma; + + // project direction the horizontal plane + var dXZ = direction.withY(0).unit(); + this.u = Vec3.rotate( + new Vec3(- dXZ.z(), 0, dXZ.x()), // perpendicular to d in horizontal plane + direction, builder.rotation + ); + this.v = direction.cross(u); // perpendicular to viewportU and direction + + this.pixelU = u.times(viewportWidth / width); + this.pixelV = v.times(viewportHeight / height); this.pixel00 = origin.plus(direction) - .minus(viewportU.div(2)).minus(viewportV.div(2)) + .minus(u.times(0.5 * viewportWidth)).minus(v.times(0.5 * viewportHeight)) .plus(pixelU.div(2)).plus(pixelV.div(2)); } @@ -147,14 +128,6 @@ public final class Camera { } } - private static @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 static @NotNull Color getSkyboxColor(@NotNull Ray ray) { // altitude from -pi/2 to pi/2 var alt = Math.copySign( @@ -164,29 +137,80 @@ public final class Camera { return Color.lerp(Color.WHITE, Color.SKY, alt / Math.PI + 0.5); } - // getters + public static class Builder { + private int imageWidth = 1920; + private int imageHeight = 1080; - public int width() { - return width; - } + private @NotNull Vec3 position = Vec3.ZERO; + private @Nullable Vec3 direction = Vec3.UNIT_Z.neg(); + private @Nullable Vec3 target = null; + private double rotation = 0.0; - public int height() { - return height; - } + private double fov = 0.5 * Math.PI; - public double viewportWidth() { - return viewportWidth; - } + private int samplePerPixel = 100; + private int maxDepth = 10; + private double gamma = 2.0; - public double viewportHeight() { - return viewportHeight; - } + private Builder() {} - public @NotNull Vec3 origin() { - return origin; - } + public @NotNull Builder withImage(int width, int height) { + if (width <= 0 || height <= 0) throw new IllegalArgumentException("width and height must be positive"); + this.imageWidth = width; + this.imageHeight = height; + return this; + } + + public @NotNull Builder withPosition(@NotNull Vec3 position) { + this.position = Objects.requireNonNull(position); + return this; + } + + public @NotNull Builder withDirection(@NotNull Vec3 direction) { + this.direction = Objects.requireNonNull(direction).unit(); + this.target = null; + return this; + } + + public @NotNull Builder withTarget(@NotNull Vec3 target) { + this.target = Objects.requireNonNull(target); + this.direction = null; + return this; + } + + public @NotNull Builder withRotation(double rotation) { + if (!Double.isFinite(rotation)) throw new IllegalArgumentException("rotation must be finite"); + this.rotation = rotation; + return this; + } + + public @NotNull Builder withFieldOfView(double fov) { + if (fov <= 0 || fov >= Math.PI || !Double.isFinite(fov)) throw new IllegalArgumentException("fov must be in the range (0, π)"); + this.fov = fov; + return this; + } + + public @NotNull Builder withSamplesPerPixel(int samples) { + if (samples <= 0) throw new IllegalArgumentException("samples must be positive"); + this.samplePerPixel = samples; + return this; + } + + public @NotNull Builder withMaxDepth(int depth) { + if (depth <= 0) throw new IllegalArgumentException("depth must be positive"); + this.maxDepth = depth; + return this; + } + + public @NotNull Builder withGamma(double gamma) { + if (gamma <= 0 || !Double.isFinite(gamma)) throw new IllegalArgumentException("gamma must be positive"); + this.gamma = gamma; + return this; + } + + public @NotNull Camera build() { + return new Camera(this); + } - public @NotNull Vec3 direction() { - return direction; } }