add Camera.Builder

main
jbb01 6 months ago
parent 94231f6a5b
commit d89d15f1a4

@ -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"));

@ -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);
}
/**
* 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(
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();
public static @NotNull Builder builder() {
return new Builder();
}
public Camera() {
this(new Builder());
}
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;
private @NotNull Vec3 position = Vec3.ZERO;
private @Nullable Vec3 direction = Vec3.UNIT_Z.neg();
private @Nullable Vec3 target = null;
private double rotation = 0.0;
private double fov = 0.5 * Math.PI;
private int samplePerPixel = 100;
private int maxDepth = 10;
private double gamma = 2.0;
private Builder() {}
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 int width() {
return width;
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 int height() {
return height;
public @NotNull Builder withSamplesPerPixel(int samples) {
if (samples <= 0) throw new IllegalArgumentException("samples must be positive");
this.samplePerPixel = samples;
return this;
}
public double viewportWidth() {
return viewportWidth;
public @NotNull Builder withMaxDepth(int depth) {
if (depth <= 0) throw new IllegalArgumentException("depth must be positive");
this.maxDepth = depth;
return this;
}
public double viewportHeight() {
return viewportHeight;
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 Vec3 origin() {
return origin;
public @NotNull Camera build() {
return new Camera(this);
}
public @NotNull Vec3 direction() {
return direction;
}
}

Loading…
Cancel
Save