Compare commits
3 Commits
d67f877428
...
18b9a52404
Author | SHA1 | Date | |
---|---|---|---|
18b9a52404 | |||
b40cc15a9f | |||
87a7fbfcff |
@ -5,69 +5,97 @@ import eu.jonahbauer.raytracing.material.LambertianMaterial;
|
|||||||
import eu.jonahbauer.raytracing.material.Material;
|
import eu.jonahbauer.raytracing.material.Material;
|
||||||
import eu.jonahbauer.raytracing.material.MetallicMaterial;
|
import eu.jonahbauer.raytracing.material.MetallicMaterial;
|
||||||
import eu.jonahbauer.raytracing.math.Vec3;
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.*;
|
||||||
import eu.jonahbauer.raytracing.render.Color;
|
import eu.jonahbauer.raytracing.render.Color;
|
||||||
import eu.jonahbauer.raytracing.render.ImageFormat;
|
import eu.jonahbauer.raytracing.render.canvas.BufferedImageCanvas;
|
||||||
import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
|
|
||||||
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
|
|
||||||
import eu.jonahbauer.raytracing.render.canvas.Image;
|
|
||||||
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer;
|
|
||||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||||
import eu.jonahbauer.raytracing.scene.Scene;
|
import eu.jonahbauer.raytracing.scene.Scene;
|
||||||
import eu.jonahbauer.raytracing.scene.Sphere;
|
import eu.jonahbauer.raytracing.scene.Sphere;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
var scene = getScene();
|
var scene = getScene();
|
||||||
|
var camera = Camera.builder()
|
||||||
var camera = SimpleCamera.builder()
|
// .withImage(1920, 1080)
|
||||||
.withImage(1200, 675)
|
.withImage(800, 450)
|
||||||
.withPosition(new Vec3(13, 2, 3))
|
.withPosition(new Vec3(13, 2, 3))
|
||||||
.withTarget(new Vec3(0, 0, 0))
|
.withTarget(new Vec3(0, 0, 0))
|
||||||
|
.withSamplesPerPixel(100)
|
||||||
|
.withMaxDepth(10)
|
||||||
.withFieldOfView(Math.toRadians(20))
|
.withFieldOfView(Math.toRadians(20))
|
||||||
.withFocusDistance(10.0)
|
|
||||||
.withBlurAngle(Math.toRadians(0.6))
|
.withBlurAngle(Math.toRadians(0.6))
|
||||||
|
.withFocusDistance(10.0)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var renderer = SimpleRenderer.builder()
|
var canvas = new BufferedImageCanvas(camera.width(), camera.height());
|
||||||
.withSamplesPerPixel(500)
|
preview(canvas);
|
||||||
.withMaxDepth(50)
|
|
||||||
.withIterative(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
|
camera.render(scene, canvas, Camera.RenderMode.SAMPLES);
|
||||||
image.preview();
|
ImageFormat.PNG.write(canvas, Path.of("scene-" + System.currentTimeMillis() + ".png"));
|
||||||
|
}
|
||||||
|
|
||||||
renderer.render(camera, scene, image);
|
private static Thread preview(@NotNull BufferedImageCanvas canvas) {
|
||||||
ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png"));
|
var frame = new JFrame();
|
||||||
|
frame.setSize(canvas.width(), canvas.height());
|
||||||
|
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||||
|
frame.setContentPane(new JPanel() {
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
g.drawImage(canvas.getImage(), 0, 0, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
frame.setResizable(false);
|
||||||
|
frame.setVisible(true);
|
||||||
|
|
||||||
|
var update = Thread.ofVirtual().start(() -> {
|
||||||
|
while (!Thread.interrupted()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
frame.repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frame.addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(WindowEvent e) {
|
||||||
|
update.interrupt();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return update;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NotNull Scene getScene() {
|
private static @NotNull Scene getScene() {
|
||||||
var rng = new Random(1);
|
|
||||||
var objects = new ArrayList<Hittable>();
|
var objects = new ArrayList<Hittable>();
|
||||||
objects.add(new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.5, 0.5, 0.5))));
|
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 a = -11; a < 11; a++) {
|
||||||
for (int b = -11; b < 11; b++) {
|
for (int b = -11; b < 11; b++) {
|
||||||
var center = new Vec3(a + 0.9 * rng.nextDouble(), 0.2, b + 0.9 * rng.nextDouble());
|
var center = new Vec3(a + 0.9 * Math.random(), 0.2, b + 0.9 * Math.random());
|
||||||
if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue;
|
if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue;
|
||||||
|
|
||||||
Material material;
|
Material material;
|
||||||
var rnd = rng.nextDouble();
|
var rnd = Math.random();
|
||||||
if (rnd < 0.8) {
|
if (rnd < 0.8) {
|
||||||
// diffuse
|
// diffuse
|
||||||
var albedo = Color.multiply(Color.random(rng), Color.random(rng));
|
var albedo = Color.multiply(Color.random(), Color.random());
|
||||||
material = new LambertianMaterial(albedo);
|
material = new LambertianMaterial(albedo);
|
||||||
} else if (rnd < 0.95) {
|
} else if (rnd < 0.95) {
|
||||||
// metal
|
// metal
|
||||||
var albedo = Color.random(rng, 0.5, 1.0);
|
var albedo = Color.random(0.5, 1.0);
|
||||||
var fuzz = rng.nextDouble() * 0.5;
|
var fuzz = Math.random() * 0.5;
|
||||||
material = new MetallicMaterial(albedo, fuzz);
|
material = new MetallicMaterial(albedo, fuzz);
|
||||||
} else {
|
} else {
|
||||||
// glass
|
// glass
|
||||||
@ -84,14 +112,4 @@ public class Main {
|
|||||||
|
|
||||||
return new Scene(objects);
|
return new Scene(objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NotNull Scene getSimpleScene() {
|
|
||||||
return new Scene(List.of(
|
|
||||||
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))
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,10 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.math;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public record BoundingBox(@NotNull Vec3 min, @NotNull Vec3 max) {
|
|
||||||
|
|
||||||
public @NotNull Vec3 center() {
|
|
||||||
return Vec3.average(min, max, 2);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,253 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.math;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public final class Octree<T> {
|
|
||||||
private final @NotNull NodeStorage<T> storage;
|
|
||||||
|
|
||||||
public Octree(@NotNull Vec3 center, double dimension) {
|
|
||||||
this.storage = new NodeStorage<>(center, dimension);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(@NotNull BoundingBox bbox, T object) {
|
|
||||||
storage.add(new Entry<>(bbox, object));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use HERO algorithms to find all elements that could possibly be hit by the given ray.
|
|
||||||
* @see <a href="https://diglib.eg.org/server/api/core/bitstreams/33fe8d58-1101-40ff-878a-79d689a4607d/content">The HERO Algorithm for Ray-Tracing Octrees</a>
|
|
||||||
*/
|
|
||||||
public void hit(@NotNull Ray ray, @NotNull Consumer<T> action) {
|
|
||||||
storage.hit(ray, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull String toString() {
|
|
||||||
return storage.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getOctantIndex(@NotNull Vec3 center, @NotNull Vec3 pos) {
|
|
||||||
return (pos.x() < center.x() ? 0 : 1)
|
|
||||||
| (pos.y() < center.y() ? 0 : 2)
|
|
||||||
| (pos.z() < center.z() ? 0 : 4);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed interface Storage<T> {
|
|
||||||
int LIST_SIZE_LIMIT = 32;
|
|
||||||
|
|
||||||
@NotNull Storage<T> add(@NotNull Entry<T> entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class ListStorage<T> implements Storage<T> {
|
|
||||||
private final @NotNull Vec3 center;
|
|
||||||
private final double dimension;
|
|
||||||
|
|
||||||
private final @NotNull List<Entry<T>> list = new ArrayList<>();
|
|
||||||
|
|
||||||
public ListStorage(@NotNull Vec3 center, double dimension) {
|
|
||||||
this.center = Objects.requireNonNull(center);
|
|
||||||
this.dimension = dimension;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Storage<T> add(@NotNull Entry<T> entry) {
|
|
||||||
if (list.size() >= LIST_SIZE_LIMIT) {
|
|
||||||
var node = new NodeStorage<T>(center, dimension);
|
|
||||||
list.forEach(node::add);
|
|
||||||
node.add(entry);
|
|
||||||
return node;
|
|
||||||
} else {
|
|
||||||
list.add(entry);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return list.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class NodeStorage<T> implements Storage<T> {
|
|
||||||
private final @NotNull Vec3 center;
|
|
||||||
private final double dimension;
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private final @Nullable Storage<T> @NotNull[] octants = new Storage[8];
|
|
||||||
private final @NotNull List<Entry<T>> list = new ArrayList<>(); // track elements spanning multiple octants separately
|
|
||||||
|
|
||||||
public NodeStorage(@NotNull Vec3 center, double dimension) {
|
|
||||||
this.center = Objects.requireNonNull(center);
|
|
||||||
this.dimension = dimension;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Storage<T> add(@NotNull Entry<T> entry) {
|
|
||||||
var index = getOctantIndex(center, entry.bbox().min());
|
|
||||||
if (index != getOctantIndex(center, entry.bbox().max())) {
|
|
||||||
list.add(entry);
|
|
||||||
} else {
|
|
||||||
var subnode = octants[index];
|
|
||||||
if (subnode == null) {
|
|
||||||
subnode = newOctant(index);
|
|
||||||
}
|
|
||||||
octants[index] = subnode.add(entry);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull Storage<T> newOctant(int index) {
|
|
||||||
var newSize = 0.5 * dimension;
|
|
||||||
var newCenter = this.center
|
|
||||||
.plus(new Vec3(
|
|
||||||
(index & 1) == 0 ? -newSize : newSize,
|
|
||||||
(index & 2) == 0 ? -newSize : newSize,
|
|
||||||
(index & 4) == 0 ? -newSize : newSize
|
|
||||||
));
|
|
||||||
return new ListStorage<>(newCenter, newSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void hit(@NotNull Ray ray, @NotNull Consumer<T> action) {
|
|
||||||
int vmask = (ray.direction().x() < 0 ? 1 : 0)
|
|
||||||
| (ray.direction().y() < 0 ? 2 : 0)
|
|
||||||
| (ray.direction().z() < 0 ? 4 : 0);
|
|
||||||
|
|
||||||
var min = center.minus(dimension, dimension, dimension);
|
|
||||||
var max = center.plus(dimension, dimension, dimension);
|
|
||||||
|
|
||||||
// calculate t values for intersection points of ray with planes through min
|
|
||||||
var tmin = calculatePlaneIntersections(min, ray);
|
|
||||||
// calculate t values for intersection points of ray with planes through max
|
|
||||||
var tmax = calculatePlaneIntersections(max, ray);
|
|
||||||
|
|
||||||
// determine range of t for which the ray is inside this voxel
|
|
||||||
double tlmax = Double.NEGATIVE_INFINITY; // lower limit maximum
|
|
||||||
double tumin = Double.POSITIVE_INFINITY; // upper limit minimum
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
// classify t values as lower or upper limit based on vmask
|
|
||||||
if ((vmask & (1 << i)) == 0) {
|
|
||||||
// min is lower limit and max is upper limit
|
|
||||||
tlmax = Math.max(tlmax, tmin[i]);
|
|
||||||
tumin = Math.min(tumin, tmax[i]);
|
|
||||||
} else {
|
|
||||||
// max is lower limit and min is upper limit
|
|
||||||
tlmax = Math.max(tlmax, tmax[i]);
|
|
||||||
tumin = Math.min(tumin, tmin[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hit = tlmax < tumin;
|
|
||||||
if (!hit) return;
|
|
||||||
|
|
||||||
hit0(ray, vmask, tlmax, tumin, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Consumer<T> action) {
|
|
||||||
if (tmax < 0) return;
|
|
||||||
for (Entry<T> entry : list) {
|
|
||||||
action.accept(entry.object());
|
|
||||||
}
|
|
||||||
|
|
||||||
// t values for intersection points of ray with planes through center
|
|
||||||
var tmid = calculatePlaneIntersections(center, ray);
|
|
||||||
// masks of planes in the order of intersection, e.g. [2, 1, 4] for a ray intersection y = center.y() then x = center.x() then z = center.z()
|
|
||||||
var masklist = calculateMastlist(tmid);
|
|
||||||
var childmask = (tmid[0] < tmin ? 1 : 0)
|
|
||||||
| (tmid[1] < tmin ? 2 : 0)
|
|
||||||
| (tmid[2] < tmin ? 4 : 0);
|
|
||||||
var lastmask = (tmid[0] < tmax ? 1 : 0)
|
|
||||||
| (tmid[1] < tmax ? 2 : 0)
|
|
||||||
| (tmid[2] < tmax ? 4 : 0);
|
|
||||||
|
|
||||||
var childTmin = tmin;
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
while (true) {
|
|
||||||
var child = octants[childmask ^ vmask];
|
|
||||||
double childTmax;
|
|
||||||
if (childmask == lastmask) {
|
|
||||||
childTmax = tmax;
|
|
||||||
} else {
|
|
||||||
while ((masklist[i] & childmask) != 0) {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
childmask = childmask | masklist[i];
|
|
||||||
childTmax = tmid[Integer.numberOfTrailingZeros(masklist[i])];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (child instanceof ListStorage<T> list) {
|
|
||||||
for (Entry<T> entry : list.list) {
|
|
||||||
action.accept(entry.object);
|
|
||||||
}
|
|
||||||
} else if (child instanceof NodeStorage<T> node) {
|
|
||||||
node.hit0(ray, vmask, childTmin, childTmax, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (childTmax == tmax) break;
|
|
||||||
childTmin = childTmax;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private double @NotNull [] calculatePlaneIntersections(@NotNull Vec3 position, @NotNull Ray ray) {
|
|
||||||
return new double[] {
|
|
||||||
(position.x() - ray.origin().x()) / ray.direction().x(),
|
|
||||||
(position.y() - ray.origin().y()) / ray.direction().y(),
|
|
||||||
(position.z() - ray.origin().z()) / ray.direction().z(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private int @NotNull [] calculateMastlist(double @NotNull[] tmid) {
|
|
||||||
var masklist = new int[3];
|
|
||||||
if (tmid[0] < tmid[1] && tmid[0] < tmid[2]) {
|
|
||||||
masklist[0] = 1;
|
|
||||||
if (tmid[1] < tmid[2]) {
|
|
||||||
masklist[1] = 2;
|
|
||||||
masklist[2] = 4;
|
|
||||||
} else {
|
|
||||||
masklist[1] = 4;
|
|
||||||
masklist[2] = 2;
|
|
||||||
}
|
|
||||||
} else if (tmid[1] < tmid[2]) {
|
|
||||||
masklist[0] = 2;
|
|
||||||
if (tmid[0] < tmid[2]) {
|
|
||||||
masklist[1] = 1;
|
|
||||||
masklist[2] = 4;
|
|
||||||
} else {
|
|
||||||
masklist[1] = 4;
|
|
||||||
masklist[2] = 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
masklist[0] = 4;
|
|
||||||
if (tmid[0] < tmid[1]) {
|
|
||||||
masklist[1] = 1;
|
|
||||||
masklist[2] = 2;
|
|
||||||
} else {
|
|
||||||
masklist[1] = 2;
|
|
||||||
masklist[2] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return masklist;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
var out = new StringBuilder("Octree centered on " + center + " with dimension " + dimension + "\n");
|
|
||||||
for (int i = 0; i < 8; i++) {
|
|
||||||
out.append(i == 7 ? "\\- [" : "|- [").append(i).append("]: ");
|
|
||||||
|
|
||||||
var prefix = i == 7 ? " " : "| ";
|
|
||||||
out.append(Objects.toString(octants[i]).lines().map(str -> prefix + str).collect(Collectors.joining("\n")).substring(8));
|
|
||||||
out.append("\n");
|
|
||||||
}
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record Entry<T>(@NotNull BoundingBox bbox, T object) { }
|
|
||||||
}
|
|
@ -6,14 +6,14 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public record Vec3(double x, double y, double z) {
|
public record Vec3(double x, double y, double z) {
|
||||||
public static final Vec3 ZERO = new Vec3(0, 0, 0);
|
public static final Vec3 ZERO = new Vec3(0, 0, 0);
|
||||||
public static final Vec3 MAX = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
|
|
||||||
public static final Vec3 MIN = new Vec3(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE);
|
|
||||||
public static final Vec3 UNIT_X = new Vec3(1, 0, 0);
|
public static final Vec3 UNIT_X = new Vec3(1, 0, 0);
|
||||||
public static final Vec3 UNIT_Y = new Vec3(0, 1, 0);
|
public static final Vec3 UNIT_Y = new Vec3(0, 1, 0);
|
||||||
public static final Vec3 UNIT_Z = new Vec3(0, 0, 1);
|
public static final Vec3 UNIT_Z = new Vec3(0, 0, 1);
|
||||||
|
|
||||||
public Vec3 {
|
public Vec3 {
|
||||||
assert Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z) : "x, y and z must be finite";
|
if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z)) {
|
||||||
|
throw new IllegalArgumentException("x, y and z must be finite");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Vec3 random() {
|
public static @NotNull Vec3 random() {
|
||||||
@ -54,38 +54,6 @@ public record Vec3(double x, double y, double z) {
|
|||||||
return a.minus(b).length();
|
return a.minus(b).length();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Vec3 average(@NotNull Vec3 current, @NotNull Vec3 next, int index) {
|
|
||||||
return new Vec3(
|
|
||||||
current.x() + (next.x() - current.x()) / index,
|
|
||||||
current.y() + (next.y() - current.y()) / index,
|
|
||||||
current.z() + (next.z() - current.z()) / index
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NotNull Vec3 max(@NotNull Vec3 a, @NotNull Vec3 b) {
|
|
||||||
return new Vec3(
|
|
||||||
Math.max(a.x(), b.x()),
|
|
||||||
Math.max(a.y(), b.y()),
|
|
||||||
Math.max(a.z(), b.z())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NotNull Vec3 min(@NotNull Vec3 a, @NotNull Vec3 b) {
|
|
||||||
return new Vec3(
|
|
||||||
Math.min(a.x(), b.x()),
|
|
||||||
Math.min(a.y(), b.y()),
|
|
||||||
Math.min(a.z(), b.z())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Vec3 plus(double x, double y, double z) {
|
|
||||||
return new Vec3(this.x + x, this.y + y, this.z + z);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Vec3 minus(double x, double y, double z) {
|
|
||||||
return new Vec3(this.x - x, this.y - y, this.z - z);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Vec3 plus(@NotNull Vec3 b) {
|
public @NotNull Vec3 plus(@NotNull Vec3 b) {
|
||||||
return new Vec3(this.x + b.x, this.y + b.y, this.z + b.z);
|
return new Vec3(this.x + b.x, this.y + b.y, this.z + b.z);
|
||||||
}
|
}
|
||||||
|
312
src/main/java/eu/jonahbauer/raytracing/render/Camera.java
Normal file
312
src/main/java/eu/jonahbauer/raytracing/render/Camera.java
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
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.render.canvas.Canvas;
|
||||||
|
import eu.jonahbauer.raytracing.render.canvas.Image;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Scene;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.LongStream;
|
||||||
|
|
||||||
|
public final class Camera {
|
||||||
|
// image size
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
|
||||||
|
// camera position and orientation
|
||||||
|
private final @NotNull Vec3 origin;
|
||||||
|
|
||||||
|
// rendering
|
||||||
|
private final int samplesPerPixel;
|
||||||
|
private final int maxDepth;
|
||||||
|
private final double gamma;
|
||||||
|
private final double blurRadius;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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 viewportHeight = 2 * Math.tan(0.5 * builder.fov) * builder.focusDistance;
|
||||||
|
var viewportWidth = viewportHeight * ((double) width / height);
|
||||||
|
|
||||||
|
this.origin = builder.position;
|
||||||
|
var direction = (builder.direction == null ? builder.target.minus(builder.position).unit() : builder.direction);
|
||||||
|
|
||||||
|
this.samplesPerPixel = builder.samplePerPixel;
|
||||||
|
this.maxDepth = builder.maxDepth;
|
||||||
|
this.gamma = builder.gamma;
|
||||||
|
this.blurRadius = Math.tan(0.5 * builder.blurAngle) * builder.focusDistance;
|
||||||
|
|
||||||
|
// 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.times(builder.focusDistance))
|
||||||
|
.minus(u.times(0.5 * viewportWidth)).minus(v.times(0.5 * viewportHeight))
|
||||||
|
.plus(pixelU.div(2)).plus(pixelV.div(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Image render(@NotNull Scene scene) {
|
||||||
|
var image = new Image(width, height);
|
||||||
|
render(scene, image);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void render(@NotNull Scene scene, @NotNull Canvas canvas) {
|
||||||
|
render(scene, canvas, RenderMode.SEQUENTIEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void render(@NotNull Scene scene, @NotNull Canvas canvas, @NotNull RenderMode mode) {
|
||||||
|
if (canvas.width() != width || canvas.height() != height) throw new IllegalArgumentException();
|
||||||
|
|
||||||
|
var progress = new AtomicInteger();
|
||||||
|
mode.render(this, scene, canvas, () -> {
|
||||||
|
var val = progress.incrementAndGet();
|
||||||
|
if (val % 1000 == 0) System.out.println(val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull Ray getRay(int x, int y) {
|
||||||
|
var origin = this.origin;
|
||||||
|
if (blurRadius > 0) {
|
||||||
|
double bu, bv;
|
||||||
|
do {
|
||||||
|
bu = 2 * Math.random() - 1;
|
||||||
|
bv = 2 * Math.random() - 1;
|
||||||
|
} while (bu * bu + bv * bv >= 1);
|
||||||
|
|
||||||
|
origin = origin.plus(u.times(blurRadius * bu)).plus(v.times(blurRadius * bv));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int width() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int height() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RenderMode {
|
||||||
|
SEQUENTIEL {
|
||||||
|
@Override
|
||||||
|
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas, @NotNull Runnable onProgressUpdate) {
|
||||||
|
for (int y = 0; y < camera.height; y++) {
|
||||||
|
for (int x = 0; x < camera.width; x++) {
|
||||||
|
Color color = Color.BLACK;
|
||||||
|
for (int i = 1; i <= camera.samplesPerPixel; i++) {
|
||||||
|
var ray = camera.getRay(x, y);
|
||||||
|
var c = camera.getColor(scene, ray);
|
||||||
|
color = Color.average(color, c, i);
|
||||||
|
}
|
||||||
|
canvas.set(x, y, color);
|
||||||
|
onProgressUpdate.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PARALLEL {
|
||||||
|
@Override
|
||||||
|
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas, @NotNull Runnable onProgressUpdate) {
|
||||||
|
coordinates(camera.width, camera.height).parallel().forEach(pos -> {
|
||||||
|
var x = (int) pos;
|
||||||
|
var y = (int) (pos >> 32);
|
||||||
|
Color color = Color.BLACK;
|
||||||
|
for (int i = 1; i <= camera.samplesPerPixel; i++) {
|
||||||
|
var ray = camera.getRay(x, y);
|
||||||
|
var c = camera.getColor(scene, ray);
|
||||||
|
color = Color.average(color, c, i);
|
||||||
|
}
|
||||||
|
canvas.set(x, y, color);
|
||||||
|
onProgressUpdate.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SAMPLES {
|
||||||
|
@Override
|
||||||
|
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas, @NotNull Runnable onProgressUpdate) {
|
||||||
|
for (int i = 1; i <= camera.samplesPerPixel; i++) {
|
||||||
|
var sample = i;
|
||||||
|
coordinates(camera.width, camera.height).forEach(pos -> {
|
||||||
|
var x = (int) pos;
|
||||||
|
var y = (int) (pos >> 32);
|
||||||
|
var ray = camera.getRay(x, y);
|
||||||
|
var color = Color.average(canvas.get(x, y), camera.getColor(scene, ray), sample);
|
||||||
|
canvas.set(x, y, color);
|
||||||
|
});
|
||||||
|
onProgressUpdate.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
|
private static LongStream coordinates(int width, int height) {
|
||||||
|
return IntStream.range(0, height)
|
||||||
|
.mapToObj(y -> IntStream.range(0, width).mapToLong(x -> (long) y << 32 | x))
|
||||||
|
.flatMapToLong(Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas, @NotNull Runnable onProgressUpdate);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 double focusDistance = 10;
|
||||||
|
private double blurAngle = 0.0;
|
||||||
|
|
||||||
|
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 @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 withFocusDistance(double focusDistance) {
|
||||||
|
if (focusDistance <= 0 || !Double.isFinite(focusDistance)) throw new IllegalArgumentException("focus distance must be positive");
|
||||||
|
this.focusDistance = focusDistance;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Builder withBlurAngle(double angle) {
|
||||||
|
if (angle < 0 || angle >= Math.PI || !Double.isFinite(angle)) throw new IllegalArgumentException("blur-angle must be in the range [0, π)");
|
||||||
|
this.blurAngle = angle;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,6 @@ package eu.jonahbauer.raytracing.render;
|
|||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public record Color(double r, double g, double b) {
|
public record Color(double r, double g, double b) {
|
||||||
public static final @NotNull Color BLACK = new Color(0.0, 0.0, 0.0);
|
public static final @NotNull Color BLACK = new Color(0.0, 0.0, 0.0);
|
||||||
public static final @NotNull Color WHITE = new Color(1.0, 1.0, 1.0);
|
public static final @NotNull Color WHITE = new Color(1.0, 1.0, 1.0);
|
||||||
@ -26,16 +24,16 @@ public record Color(double r, double g, double b) {
|
|||||||
return new Color(a.r() * b.r(), a.g() * b.g(), a.b() * b.b());
|
return new Color(a.r() * b.r(), a.g() * b.g(), a.b() * b.b());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Color random(@NotNull Random random) {
|
public static @NotNull Color random() {
|
||||||
return new Color(random.nextDouble(), random.nextDouble(), random.nextDouble());
|
return new Color(Math.random(), Math.random(), Math.random());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Color random(@NotNull Random random, double min, double max) {
|
public static @NotNull Color random(double min, double max) {
|
||||||
var span = max - min;
|
var span = max - min;
|
||||||
return new Color(
|
return new Color(
|
||||||
Math.fma(random.nextDouble(), span, min),
|
Math.fma(Math.random(), span, min),
|
||||||
Math.fma(random.nextDouble(), span, min),
|
Math.fma(Math.random(), span, min),
|
||||||
Math.fma(random.nextDouble(), span, min)
|
Math.fma(Math.random(), span, min)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,24 +45,6 @@ public record Color(double r, double g, double b) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Color gamma(@NotNull Color color, double gamma) {
|
|
||||||
if (gamma == 1.0) {
|
|
||||||
return color;
|
|
||||||
} else if (gamma == 2.0) {
|
|
||||||
return new Color(
|
|
||||||
Math.sqrt(color.r()),
|
|
||||||
Math.sqrt(color.g()),
|
|
||||||
Math.sqrt(color.b())
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return new Color(
|
|
||||||
Math.pow(color.r(), 1 / gamma),
|
|
||||||
Math.pow(color.g(), 1 / gamma),
|
|
||||||
Math.pow(color.b(), 1 / gamma)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Color {
|
public Color {
|
||||||
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1) {
|
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1) {
|
||||||
throw new IllegalArgumentException("r, g and b must be in the range 0 to 1");
|
throw new IllegalArgumentException("r, g and b must be in the range 0 to 1");
|
||||||
|
@ -17,9 +17,9 @@ public enum ImageFormat {
|
|||||||
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
|
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
|
||||||
try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) {
|
try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) {
|
||||||
writer.write("P3\n");
|
writer.write("P3\n");
|
||||||
writer.write(String.valueOf(image.getWidth()));
|
writer.write(String.valueOf(image.width()));
|
||||||
writer.write(" ");
|
writer.write(" ");
|
||||||
writer.write(String.valueOf(image.getHeight()));
|
writer.write(String.valueOf(image.height()));
|
||||||
writer.write("\n255\n");
|
writer.write("\n255\n");
|
||||||
|
|
||||||
var it = image.pixels().iterator();
|
var it = image.pixels().iterator();
|
||||||
@ -60,8 +60,8 @@ public enum ImageFormat {
|
|||||||
var ihdr = new DataOutputStream(crc)
|
var ihdr = new DataOutputStream(crc)
|
||||||
) {
|
) {
|
||||||
ihdr.writeInt(IHDR_TYPE);
|
ihdr.writeInt(IHDR_TYPE);
|
||||||
ihdr.writeInt(image.getWidth()); // image width
|
ihdr.writeInt(image.width()); // image width
|
||||||
ihdr.writeInt(image.getHeight()); // image height
|
ihdr.writeInt(image.height()); // image height
|
||||||
ihdr.writeByte(8); // bit depth
|
ihdr.writeByte(8); // bit depth
|
||||||
ihdr.writeByte(2); // color type
|
ihdr.writeByte(2); // color type
|
||||||
ihdr.writeByte(0); // compression method
|
ihdr.writeByte(0); // compression method
|
||||||
@ -82,7 +82,7 @@ public enum ImageFormat {
|
|||||||
|
|
||||||
try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) {
|
try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) {
|
||||||
var pixels = image.pixels().iterator();
|
var pixels = image.pixels().iterator();
|
||||||
for (int i = 0; pixels.hasNext(); i = (i + 1) % image.getWidth()) {
|
for (int i = 0; pixels.hasNext(); i = (i + 1) % image.width()) {
|
||||||
if (i == 0) deflate.writeByte(0); // filter type
|
if (i == 0) deflate.writeByte(0); // filter type
|
||||||
var pixel = pixels.next();
|
var pixel = pixels.next();
|
||||||
deflate.writeByte(pixel.red());
|
deflate.writeByte(pixel.red());
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.render.camera;
|
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.math.Ray;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public interface Camera {
|
|
||||||
/**
|
|
||||||
* {@return the width of this camera in pixels}
|
|
||||||
*/
|
|
||||||
int getWidth();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the height of this camera in pixels}
|
|
||||||
*/
|
|
||||||
int getHeight();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Casts a ray through the given pixel.
|
|
||||||
* @return a new ray
|
|
||||||
*/
|
|
||||||
@NotNull Ray cast(int x, int y);
|
|
||||||
}
|
|
@ -1,193 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.render.camera;
|
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.math.Ray;
|
|
||||||
import eu.jonahbauer.raytracing.math.Vec3;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public final class SimpleCamera implements Camera {
|
|
||||||
// image size
|
|
||||||
private final int width;
|
|
||||||
private final int height;
|
|
||||||
|
|
||||||
// camera position and orientation
|
|
||||||
private final @NotNull Vec3 origin;
|
|
||||||
|
|
||||||
// rendering
|
|
||||||
private final double blurRadius;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
public static @NotNull Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NotNull Camera withDefaults() {
|
|
||||||
return new Builder().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SimpleCamera(@NotNull Builder builder) {
|
|
||||||
this.width = builder.imageWidth;
|
|
||||||
this.height = builder.imageHeight;
|
|
||||||
|
|
||||||
var viewportHeight = 2 * Math.tan(0.5 * builder.fov) * builder.focusDistance;
|
|
||||||
var viewportWidth = viewportHeight * ((double) width / height);
|
|
||||||
|
|
||||||
this.origin = builder.position;
|
|
||||||
var direction = (builder.direction == null ? builder.target.minus(builder.position).unit() : builder.direction);
|
|
||||||
|
|
||||||
this.blurRadius = Math.tan(0.5 * builder.blurAngle) * builder.focusDistance;
|
|
||||||
|
|
||||||
// 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.times(builder.focusDistance))
|
|
||||||
.minus(u.times(0.5 * viewportWidth)).minus(v.times(0.5 * viewportHeight))
|
|
||||||
.plus(pixelU.div(2)).plus(pixelV.div(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public int getWidth() {
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public int getHeight() {
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
public @NotNull Ray cast(int x, int y) {
|
|
||||||
Objects.checkIndex(x, width);
|
|
||||||
Objects.checkIndex(y, height);
|
|
||||||
|
|
||||||
var origin = getRayOrigin();
|
|
||||||
var target = getRayTarget(x, y);
|
|
||||||
return new Ray(origin, target.minus(origin));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the origin for a ray cast by this camera} The ray origin is randomized within a disk of
|
|
||||||
* radius {@link #blurRadius} centered on the camera position and perpendicular to the direction to simulate depth
|
|
||||||
* of field.
|
|
||||||
*/
|
|
||||||
private @NotNull Vec3 getRayOrigin() {
|
|
||||||
if (blurRadius <= 0) return origin;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
var du = 2 * Math.random() - 1;
|
|
||||||
var dv = 2 * Math.random() - 1;
|
|
||||||
if (du * du + dv * dv >= 1) continue;
|
|
||||||
|
|
||||||
var ru = blurRadius * du;
|
|
||||||
var rv = blurRadius * dv;
|
|
||||||
|
|
||||||
return new Vec3(
|
|
||||||
origin.x() + ru * u.x() + rv * v.x(),
|
|
||||||
origin.y() + ru * u.y() + rv * v.y(),
|
|
||||||
origin.z() + ru * u.z() + rv * v.z()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the target vector for a ray through the given pixel} The position is randomized within the pixel.
|
|
||||||
*/
|
|
||||||
private @NotNull Vec3 getRayTarget(int x, int y) {
|
|
||||||
double dx = x + Math.random() - 0.5;
|
|
||||||
double dy = y + Math.random() - 0.5;
|
|
||||||
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 double focusDistance = 10;
|
|
||||||
private double blurAngle = 0.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 @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 withFocusDistance(double focusDistance) {
|
|
||||||
if (focusDistance <= 0 || !Double.isFinite(focusDistance)) throw new IllegalArgumentException("focus distance must be positive");
|
|
||||||
this.focusDistance = focusDistance;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Builder withBlurAngle(double angle) {
|
|
||||||
if (angle < 0 || angle >= Math.PI || !Double.isFinite(angle)) throw new IllegalArgumentException("blur-angle must be in the range [0, π)");
|
|
||||||
this.blurAngle = angle;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull SimpleCamera build() {
|
|
||||||
return new SimpleCamera(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,26 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.canvas;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.render.Color;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
public class BufferedImageCanvas extends Image {
|
||||||
|
private final @NotNull BufferedImage image;
|
||||||
|
|
||||||
|
public BufferedImageCanvas(int width, int height) {
|
||||||
|
super(width, height);
|
||||||
|
this.image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull BufferedImage getImage() {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void set(int x, int y, @NotNull Color color) {
|
||||||
|
super.set(x, y, color);
|
||||||
|
var rgb = color.red() << 16 | color.green() << 8 | color.blue();
|
||||||
|
image.setRGB(x, y, rgb);
|
||||||
|
}
|
||||||
|
}
|
@ -8,15 +8,15 @@ import java.util.stream.IntStream;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public interface Canvas {
|
public interface Canvas {
|
||||||
int getWidth();
|
int width();
|
||||||
int getHeight();
|
int height();
|
||||||
|
|
||||||
void set(int x, int y, @NotNull Color color);
|
void set(int x, int y, @NotNull Color color);
|
||||||
@NotNull Color get(int x, int y);
|
@NotNull Color get(int x, int y);
|
||||||
|
|
||||||
default @NotNull Stream<Color> pixels() {
|
default @NotNull Stream<Color> pixels() {
|
||||||
return IntStream.range(0, getHeight())
|
return IntStream.range(0, height())
|
||||||
.mapToObj(y -> IntStream.range(0, getWidth()).mapToObj(x -> get(x, y)))
|
.mapToObj(y -> IntStream.range(0, width()).mapToObj(x -> get(x, y)))
|
||||||
.flatMap(Function.identity());
|
.flatMap(Function.identity());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class Image implements Canvas {
|
public class Image implements Canvas {
|
||||||
private final int width;
|
private final int width;
|
||||||
private final int height;
|
private final int height;
|
||||||
|
|
||||||
@ -22,12 +22,12 @@ public final class Image implements Canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getWidth() {
|
public int width() {
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getHeight() {
|
public int height() {
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.render.canvas;
|
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.render.Color;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.event.WindowAdapter;
|
|
||||||
import java.awt.event.WindowEvent;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
|
|
||||||
public final class LiveCanvas implements Canvas {
|
|
||||||
private final @NotNull Canvas delegate;
|
|
||||||
private final @NotNull BufferedImage image;
|
|
||||||
|
|
||||||
public LiveCanvas(@NotNull Canvas delegate) {
|
|
||||||
this.delegate = delegate;
|
|
||||||
this.image = new BufferedImage(delegate.getWidth(), delegate.getHeight(), BufferedImage.TYPE_INT_RGB);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getWidth() {
|
|
||||||
return delegate.getWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getHeight() {
|
|
||||||
return delegate.getHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void set(int x, int y, @NotNull Color color) {
|
|
||||||
delegate.set(x, y, color);
|
|
||||||
var rgb = color.red() << 16 | color.green() << 8 | color.blue();
|
|
||||||
image.setRGB(x, y, rgb);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Color get(int x, int y) {
|
|
||||||
return delegate.get(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Thread preview() {
|
|
||||||
var frame = new JFrame();
|
|
||||||
frame.setSize(getWidth(), getHeight());
|
|
||||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
|
||||||
frame.setContentPane(new JPanel() {
|
|
||||||
@Override
|
|
||||||
protected void paintComponent(Graphics g) {
|
|
||||||
g.drawImage(image, 0, 0, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
frame.setResizable(false);
|
|
||||||
frame.setVisible(true);
|
|
||||||
|
|
||||||
var update = Thread.ofVirtual().start(() -> {
|
|
||||||
while (!Thread.interrupted()) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(1000);
|
|
||||||
} catch (InterruptedException ex) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
frame.repaint();
|
|
||||||
}
|
|
||||||
frame.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
frame.addWindowListener(new WindowAdapter() {
|
|
||||||
@Override
|
|
||||||
public void windowClosing(WindowEvent e) {
|
|
||||||
update.interrupt();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.render.renderer;
|
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.render.camera.Camera;
|
|
||||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
|
||||||
import eu.jonahbauer.raytracing.render.canvas.Image;
|
|
||||||
import eu.jonahbauer.raytracing.scene.Scene;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public interface Renderer {
|
|
||||||
default @NotNull Image render(@NotNull Camera camera, @NotNull Scene scene) {
|
|
||||||
var image = new Image(camera.getWidth(), camera.getHeight());
|
|
||||||
render(camera, scene, image);
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}.
|
|
||||||
*/
|
|
||||||
void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas);
|
|
||||||
}
|
|
@ -1,166 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.render.renderer;
|
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.math.Range;
|
|
||||||
import eu.jonahbauer.raytracing.math.Ray;
|
|
||||||
import eu.jonahbauer.raytracing.render.Color;
|
|
||||||
import eu.jonahbauer.raytracing.render.camera.Camera;
|
|
||||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
|
||||||
import eu.jonahbauer.raytracing.scene.Scene;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
import java.util.stream.LongStream;
|
|
||||||
|
|
||||||
public final class SimpleRenderer implements Renderer {
|
|
||||||
private final int samplesPerPixel;
|
|
||||||
private final int maxDepth;
|
|
||||||
private final double gamma;
|
|
||||||
|
|
||||||
private final boolean parallel;
|
|
||||||
private final boolean iterative;
|
|
||||||
|
|
||||||
public static @NotNull Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @NotNull Renderer withDefaults() {
|
|
||||||
return new Builder().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SimpleRenderer(@NotNull Builder builder) {
|
|
||||||
this.samplesPerPixel = builder.samplesPerPixel;
|
|
||||||
this.maxDepth = builder.maxDepth;
|
|
||||||
this.gamma = builder.gamma;
|
|
||||||
|
|
||||||
this.parallel = builder.parallel;
|
|
||||||
this.iterative = builder.iterative;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
|
|
||||||
if (canvas.getWidth() != camera.getWidth() || canvas.getHeight() != camera.getHeight()) {
|
|
||||||
throw new IllegalArgumentException("sizes of camera and canvas are different");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iterative) {
|
|
||||||
// render one sample after the other
|
|
||||||
for (int i = 1 ; i <= samplesPerPixel; i++) {
|
|
||||||
var sample = i;
|
|
||||||
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> {
|
|
||||||
var y = (int) (pixel >> 32);
|
|
||||||
var x = (int) pixel;
|
|
||||||
var ray = camera.cast(x, y);
|
|
||||||
var c = getColor(scene, ray);
|
|
||||||
canvas.set(x, y, Color.average(canvas.get(x, y), c, sample));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// apply gamma correction
|
|
||||||
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> {
|
|
||||||
var y = (int) (pixel >> 32);
|
|
||||||
var x = (int) pixel;
|
|
||||||
canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// render one pixel after the other
|
|
||||||
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> {
|
|
||||||
var y = (int) (pixel >> 32);
|
|
||||||
var x = (int) pixel;
|
|
||||||
|
|
||||||
var color = Color.BLACK;
|
|
||||||
for (int i = 1; i <= samplesPerPixel; i++) {
|
|
||||||
var ray = camera.cast(x, y);
|
|
||||||
var c = getColor(scene, ray);
|
|
||||||
color = Color.average(color, c, i);
|
|
||||||
}
|
|
||||||
canvas.set(x, y, Color.gamma(color, gamma));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the color of the given ray in the given scene}
|
|
||||||
*/
|
|
||||||
private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray) {
|
|
||||||
return getColor0(scene, ray, maxDepth);
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NotNull Color getColor0(@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(), getColor0(scene, scatter.ray(), depth - 1)))
|
|
||||||
.orElse(Color.BLACK);
|
|
||||||
} else {
|
|
||||||
return getSkyboxColor(ray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return a stream of the pixels in a canvas with the given size} The pixels {@code x} and {@code y} coordinate
|
|
||||||
* are encoded in the longs lower and upper 32 bits respectively.
|
|
||||||
*/
|
|
||||||
private static @NotNull LongStream getPixelStream(int width, int height, boolean parallel) {
|
|
||||||
var stream = IntStream.range(0, height)
|
|
||||||
.mapToObj(y -> IntStream.range(0, width).mapToLong(x -> (long) y << 32 | x))
|
|
||||||
.flatMapToLong(Function.identity());
|
|
||||||
return parallel ? stream.parallel() : stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@return the color of the skybox for a given ray} The skybox color is a linear gradient based on the altitude of
|
|
||||||
* the ray above the horizon with {@link Color#SKY} at the top and {@link Color#WHITE} at the bottom.
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
private int samplesPerPixel = 100;
|
|
||||||
private int maxDepth = 10;
|
|
||||||
private double gamma = 2.0;
|
|
||||||
private boolean parallel = true;
|
|
||||||
private boolean iterative = false;
|
|
||||||
|
|
||||||
public @NotNull Builder withSamplesPerPixel(int samples) {
|
|
||||||
if (samples <= 0) throw new IllegalArgumentException("samples must be positive");
|
|
||||||
this.samplesPerPixel = 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 Builder withParallel(boolean parallel) {
|
|
||||||
this.parallel = parallel;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Builder withIterative(boolean iterative) {
|
|
||||||
this.iterative = iterative;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull SimpleRenderer build() {
|
|
||||||
return new SimpleRenderer(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
package eu.jonahbauer.raytracing.scene;
|
package eu.jonahbauer.raytracing.scene;
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
|
||||||
import eu.jonahbauer.raytracing.math.Range;
|
import eu.jonahbauer.raytracing.math.Range;
|
||||||
import eu.jonahbauer.raytracing.math.Ray;
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@ -15,8 +14,4 @@ public interface Hittable {
|
|||||||
* @param ray a ray
|
* @param ray a ray
|
||||||
*/
|
*/
|
||||||
@NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range);
|
@NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range);
|
||||||
|
|
||||||
default @NotNull Optional<BoundingBox> getBoundingBox() {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,80 +1,31 @@
|
|||||||
package eu.jonahbauer.raytracing.scene;
|
package eu.jonahbauer.raytracing.scene;
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.math.Octree;
|
|
||||||
import eu.jonahbauer.raytracing.math.Range;
|
import eu.jonahbauer.raytracing.math.Range;
|
||||||
import eu.jonahbauer.raytracing.math.Ray;
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
import eu.jonahbauer.raytracing.math.Vec3;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public final class Scene implements Hittable {
|
public record Scene(@NotNull List<@NotNull Hittable> objects) implements Hittable {
|
||||||
private final @NotNull Octree<@NotNull Hittable> octree;
|
|
||||||
private final @NotNull List<@NotNull Hittable> list;
|
|
||||||
|
|
||||||
public Scene(@NotNull List<? extends @NotNull Hittable> objects) {
|
public Scene {
|
||||||
this.octree = newOctree(objects);
|
objects = List.copyOf(objects);
|
||||||
this.list = new ArrayList<>();
|
}
|
||||||
|
|
||||||
for (Hittable object : objects) {
|
public Scene(@NotNull Hittable @NotNull ... objects) {
|
||||||
var bbox = object.getBoundingBox();
|
this(List.of(objects));
|
||||||
if (bbox.isPresent()) {
|
|
||||||
octree.add(bbox.get(), object);
|
|
||||||
} else {
|
|
||||||
list.add(object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
||||||
var state = new State();
|
var result = (HitResult) null;
|
||||||
state.range = range;
|
for (var object : objects) {
|
||||||
|
var r = object.hit(ray, range);
|
||||||
octree.hit(ray, object -> hit(state, ray, object));
|
if (r.isPresent() && range.surrounds(r.get().t())) {
|
||||||
list.forEach(object -> hit(state, ray, object));
|
result = r.get();
|
||||||
|
range = new Range(range.min(), result.t());
|
||||||
return Optional.ofNullable(state.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object) {
|
|
||||||
var r = object.hit(ray, state.range);
|
|
||||||
if (r.isPresent() && state.range.surrounds(r.get().t())) {
|
|
||||||
state.result = r.get();
|
|
||||||
state.range = new Range(state.range.min(), state.result.t());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @NotNull Octree<Hittable> newOctree(@NotNull List<? extends Hittable> objects) {
|
|
||||||
Vec3 center = Vec3.ZERO, max = Vec3.MIN, min = Vec3.MAX;
|
|
||||||
|
|
||||||
int i = 1;
|
|
||||||
for (Hittable object : objects) {
|
|
||||||
var bbox = object.getBoundingBox();
|
|
||||||
if (bbox.isPresent()) {
|
|
||||||
center = Vec3.average(center, bbox.get().center(), i++);
|
|
||||||
max = Vec3.max(max, bbox.get().max());
|
|
||||||
min = Vec3.min(min, bbox.get().min());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return Optional.ofNullable(result);
|
||||||
var dimension = Arrays.stream(new double[] {
|
|
||||||
Math.abs(max.x() - center.x()),
|
|
||||||
Math.abs(max.y() - center.y()),
|
|
||||||
Math.abs(max.z() - center.z()),
|
|
||||||
Math.abs(min.x() - center.x()),
|
|
||||||
Math.abs(min.y() - center.y()),
|
|
||||||
Math.abs(min.z() - center.z())
|
|
||||||
}).max().orElse(10);
|
|
||||||
|
|
||||||
return new Octree<Hittable>(center, dimension);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class State {
|
|
||||||
HitResult result;
|
|
||||||
Range range;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package eu.jonahbauer.raytracing.scene;
|
package eu.jonahbauer.raytracing.scene;
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.material.Material;
|
import eu.jonahbauer.raytracing.material.Material;
|
||||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
|
||||||
import eu.jonahbauer.raytracing.math.Range;
|
import eu.jonahbauer.raytracing.math.Range;
|
||||||
import eu.jonahbauer.raytracing.math.Ray;
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
import eu.jonahbauer.raytracing.math.Vec3;
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
@ -45,14 +44,6 @@ public record Sphere(@NotNull Vec3 center, double radius, @NotNull Material mate
|
|||||||
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.times(-1), material, frontFace));
|
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.times(-1), material, frontFace));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
|
||||||
return Optional.of(new BoundingBox(
|
|
||||||
center.minus(radius, radius, radius),
|
|
||||||
center.plus(radius, radius, radius)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Sphere withCenter(@NotNull Vec3 center) {
|
public @NotNull Sphere withCenter(@NotNull Vec3 center) {
|
||||||
return new Sphere(center, radius, material);
|
return new Sphere(center, radius, material);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.jonahbauer.raytracing.render;
|
package eu.jonahbauer.raytracing.render;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.render.canvas.Image;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
@ -23,12 +24,12 @@ class ImageTest {
|
|||||||
var g = (double) y / (image.height() - 1);
|
var g = (double) y / (image.height() - 1);
|
||||||
var b = 0;
|
var b = 0;
|
||||||
|
|
||||||
image.set(x, y, r, g, b);
|
image.set(x, y, new Color(r, g, b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println(dir);
|
System.out.println(dir);
|
||||||
ImageIO.write(image, dir.resolve("img.ppm"));
|
ImageFormat.PPM.write(image, dir.resolve("img.ppm"));
|
||||||
|
|
||||||
String expected;
|
String expected;
|
||||||
String actual;
|
String actual;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user