|
|
|
@ -7,22 +7,24 @@ import eu.jonahbauer.raytracing.scene.Scene;
|
|
|
|
|
import org.jetbrains.annotations.NotNull;
|
|
|
|
|
|
|
|
|
|
import java.util.Objects;
|
|
|
|
|
import java.util.stream.IntStream;
|
|
|
|
|
import java.util.stream.Stream;
|
|
|
|
|
|
|
|
|
|
public record Camera(
|
|
|
|
|
int width, int height,
|
|
|
|
|
double viewportWidth, double viewportHeight,
|
|
|
|
|
@NotNull Vec3 origin, @NotNull Vec3 direction
|
|
|
|
|
) {
|
|
|
|
|
public Camera {
|
|
|
|
|
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");
|
|
|
|
|
Objects.requireNonNull(origin, "origin");
|
|
|
|
|
Objects.requireNonNull(direction, "direction");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
@ -45,27 +47,76 @@ public record Camera(
|
|
|
|
|
this(width, height, viewportWidth, viewportHeight, Vec3.ZERO, Vec3.UNIT_Z);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
pixels().forEach(pixel -> {
|
|
|
|
|
var x = pixel.x();
|
|
|
|
|
var y = pixel.y();
|
|
|
|
|
var ray = pixel.ray();
|
|
|
|
|
|
|
|
|
|
var result = scene.hit(ray, Range.NON_NEGATIVE);
|
|
|
|
|
if (result.isPresent()) {
|
|
|
|
|
var normal = result.get().normal();
|
|
|
|
|
image.set(x, y, getNormalColor(normal));
|
|
|
|
|
} else {
|
|
|
|
|
image.set(x, y, scene.getSkyboxColor(ray));
|
|
|
|
|
var image = new Image(width, height);
|
|
|
|
|
|
|
|
|
|
for (int y = 0; y < height; y++) {
|
|
|
|
|
var ray = getRay(x, y);
|
|
|
|
|
var color = getColor(scene, ray);
|
|
|
|
|
image.set(x, y, color);
|
|
|
|
|
for (int x = 0; x < width; x++) {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private @NotNull Color getNormalColor(@NotNull Vec3 normal) {
|
|
|
|
|
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);
|
|
|
|
|
return pixel00.plus(pixelU.times(x)).plus(pixelV.times(y));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray) {
|
|
|
|
|
var result = scene.hit(ray, Range.NON_NEGATIVE);
|
|
|
|
|
if (result.isPresent()) {
|
|
|
|
|
var normal = result.get().normal();
|
|
|
|
|
return getNormalColor(normal);
|
|
|
|
|
} 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),
|
|
|
|
@ -73,31 +124,38 @@ public record Camera(
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public @NotNull Stream<Pixel> pixels() {
|
|
|
|
|
// project direction onto xz-plane
|
|
|
|
|
var d = direction.unit();
|
|
|
|
|
var dXZ = direction.withY(0).unit();
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
// getters
|
|
|
|
|
|
|
|
|
|
viewportU = viewportU.times(viewportWidth); // vector along the width of the viewport
|
|
|
|
|
viewportV = viewportV.times(- viewportHeight); // vector along the height of the viewport
|
|
|
|
|
public int width() {
|
|
|
|
|
return width;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pixelU = viewportU.div(width);
|
|
|
|
|
var pixelV = viewportV.div(height);
|
|
|
|
|
public int height() {
|
|
|
|
|
return height;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pixel00 = origin.plus(direction)
|
|
|
|
|
.minus(viewportU.div(2)).minus(viewportV.div(2))
|
|
|
|
|
.plus(pixelU.div(2)).plus(pixelV.div(2));
|
|
|
|
|
public double viewportWidth() {
|
|
|
|
|
return viewportWidth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return IntStream.range(0, width * height).mapToObj(i -> {
|
|
|
|
|
var y = i / width;
|
|
|
|
|
var x = i % width;
|
|
|
|
|
var pixel = pixel00.plus(pixelU.times(x)).plus(pixelV.times(y));
|
|
|
|
|
return new Pixel(x, y, new Ray(origin, pixel.minus(origin)));
|
|
|
|
|
});
|
|
|
|
|
public double viewportHeight() {
|
|
|
|
|
return viewportHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public record Pixel(int x, int y, @NotNull Ray ray) {}
|
|
|
|
|
public @NotNull Vec3 origin() {
|
|
|
|
|
return origin;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public @NotNull Vec3 direction() {
|
|
|
|
|
return direction;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|