193 lines
6.4 KiB
Java

package eu.jonahbauer.raytracing.render;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.Scene;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public final class Camera {
// image size
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;
// internal properties
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();
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
viewportU = viewportU.times(viewportWidth); // vector along the width of the viewport
viewportV = viewportV.times(viewportHeight); // vector along the height of the viewport
this.pixelU = viewportU.div(width);
this.pixelV = viewportV.div(height);
this.pixel00 = origin.plus(direction)
.minus(viewportU.div(2)).minus(viewportV.div(2))
.plus(pixelU.div(2)).plus(pixelV.div(2));
}
public @NotNull Image render(@NotNull Scene scene) {
var image = new Image(width, height);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
var r = 0d;
var g = 0d;
var b = 0d;
for (int i = 0; i < samplesPerPixel; i++) {
var ray = getRay(x, y);
var color = getColor(scene, ray);
r += color.r();
g += color.g();
b += color.b();
}
image.set(x, y, new Color(
Math.pow(r / samplesPerPixel, 1 / gamma),
Math.pow(g / samplesPerPixel, 1 / gamma),
Math.pow(b / samplesPerPixel, 1 / gamma)
));
}
}
return image;
}
private @NotNull Ray getRay(int x, int y) {
return new Ray(origin, getPixel(x, y).minus(origin));
}
private @NotNull Vec3 getPixel(int x, int y) {
Objects.checkIndex(x, width);
Objects.checkIndex(y, height);
double dx = x + Math.random() - 0.5;
double dy = y + Math.random() - 0.5;
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
}
private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray) {
return getColor(scene, ray, maxDepth);
}
private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray, int depth) {
if (depth <= 0) return Color.BLACK;
var optional = scene.hit(ray, new Range(0.001, Double.POSITIVE_INFINITY));
if (optional.isPresent()) {
var hit = optional.get();
var material = hit.material();
return material.scatter(ray, hit)
.map(scatter -> Color.multiply(scatter.attenuation(), getColor(scene, scatter.ray(), depth - 1)))
.orElse(Color.BLACK);
} else {
return getSkyboxColor(ray);
}
}
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(
Math.acos(ray.direction().withY(0).unit().times(ray.direction().unit())),
ray.direction().y()
);
return Color.lerp(Color.WHITE, Color.SKY, alt / Math.PI + 0.5);
}
// getters
public int width() {
return width;
}
public int height() {
return height;
}
public double viewportWidth() {
return viewportWidth;
}
public double viewportHeight() {
return viewportHeight;
}
public @NotNull Vec3 origin() {
return origin;
}
public @NotNull Vec3 direction() {
return direction;
}
}