Compare commits

...

14 Commits

38 changed files with 1361 additions and 132 deletions

28
README.md Normal file
View File

@ -0,0 +1,28 @@
# raytracing
Based on the series <a href="https://raytracing.github.io"><cite>Ray Tracing in One Weekend</cite></a>.
## Scenes
### squares
![](./docs/squares.png)
```
java -jar raytracing.jar --samples 500 --height 1200 SQUARES
```
### cornell box
![](./docs/cornell_smoke.png)
```
java -jar raytracing.jar --samples 50000 --height 1200 CORNELL
```
### cornell box with smoke
![](./docs/cornell_smoke.png)
```
java -jar raytracing.jar --samples 50000 --height 600 CORNELL_SMOKE
```

View File

@ -1,5 +1,16 @@
plugins {
id("java")
id("application")
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(22)
}
}
application {
mainClass = "eu.jonahbauer.raytracing.Main"
}
dependencies {

BIN
docs/cornell_smoke.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

BIN
docs/squares.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

View File

@ -1,97 +1,307 @@
package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.material.DielectricMaterial;
import eu.jonahbauer.raytracing.material.LambertianMaterial;
import eu.jonahbauer.raytracing.material.Material;
import eu.jonahbauer.raytracing.material.MetallicMaterial;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.render.ImageFormat;
import eu.jonahbauer.raytracing.render.camera.Camera;
import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.canvas.Image;
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
import eu.jonahbauer.raytracing.render.material.*;
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer;
import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.Scene;
import eu.jonahbauer.raytracing.scene.Sphere;
import eu.jonahbauer.raytracing.scene.SkyBox;
import eu.jonahbauer.raytracing.scene.hittable2d.Parallelogram;
import eu.jonahbauer.raytracing.scene.hittable3d.ConstantMedium;
import eu.jonahbauer.raytracing.scene.hittable3d.Sphere;
import eu.jonahbauer.raytracing.scene.util.Hittables;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.IntFunction;
public class Main {
public static void main(String[] args) throws IOException {
var scene = getScene();
var camera = SimpleCamera.builder()
.withImage(1200, 675)
.withPosition(new Vec3(13, 2, 3))
.withTarget(new Vec3(0, 0, 0))
.withFieldOfView(Math.toRadians(20))
.withFocusDistance(10.0)
.withBlurAngle(Math.toRadians(0.6))
.build();
var config = Config.parse(args);
var example = config.example;
var scene = example.scene();
var camera = example.camera();
var renderer = SimpleRenderer.builder()
.withSamplesPerPixel(500)
.withMaxDepth(50)
.withIterative(true)
.withSamplesPerPixel(config.samples)
.withMaxDepth(config.depth)
.withIterative(config.iterative)
.build();
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
image.preview();
renderer.render(camera, scene, image);
ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png"));
}
private static @NotNull Scene getScene() {
var rng = new Random(1);
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))));
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
var center = new Vec3(a + 0.9 * rng.nextDouble(), 0.2, b + 0.9 * rng.nextDouble());
if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue;
Material material;
var rnd = rng.nextDouble();
if (rnd < 0.8) {
// diffuse
var albedo = Color.multiply(Color.random(rng), Color.random(rng));
material = new LambertianMaterial(albedo);
} else if (rnd < 0.95) {
// metal
var albedo = Color.random(rng, 0.5, 1.0);
var fuzz = rng.nextDouble() * 0.5;
material = new MetallicMaterial(albedo, fuzz);
} else {
// glass
material = new DielectricMaterial(1.5);
}
objects.add(new Sphere(center, 0.2, material));
}
Canvas canvas;
if (config.preview) {
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
image.preview();
canvas = image;
} else {
canvas = new Image(camera.getWidth(), camera.getHeight());
}
objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5)));
objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1))));
objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5))));
long time = System.nanoTime();
renderer.render(camera, scene, canvas);
System.out.printf("rendering finished after %dms", (System.nanoTime() - time) / 1_000_000);
return new Scene(objects);
ImageFormat.PNG.write(canvas, config.path);
}
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))
));
private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, int samples, int depth) {
public static @NotNull Config parse(@NotNull String @NotNull[] args) {
IntFunction<Example> example = null;
Path path = null;
boolean preview = true;
boolean iterative = false;
int samples = 1000;
int depth = 50;
int height = -1;
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case "--output" -> {
if (i + 1 == args.length) throw fail("missing value for parameter --output");
try {
path = Path.of(args[++i]);
} catch (InvalidPathException ex) {
throw fail("value " + args[i] + " is not a valid path");
}
}
case "--preview" -> preview = true;
case "--no-preview" -> preview = false;
case "--iterative" -> iterative = true;
case "--no-iterative" -> iterative = false;
case "--samples" -> {
if (i + 1 == args.length) throw fail("missing value for parameter --samples");
try {
samples = Integer.parseInt(args[++i]);
if (samples <= 0) throw fail("samples must be positive");
} catch (NumberFormatException ex) {
throw fail("value " + args[i] + " is not a valid integer");
}
}
case "--depth" -> {
if (i + 1 == args.length) throw fail("missing value for parameter --depth");
try {
depth = Integer.parseInt(args[++i]);
if (depth <= 0) throw fail("depth must be positive");
} catch (NumberFormatException ex) {
throw fail("value " + args[i] + " is not a valid integer");
}
}
case "--height" -> {
if (i + 1 == args.length) throw fail("missing value for parameter --height");
try {
height = Integer.parseInt(args[++i]);
if (height <= 0) throw fail("height must be positive");
} catch (NumberFormatException ex) {
throw fail("value " + args[i] + " is not a valid integer");
}
}
case String str when !str.startsWith("-") -> example = switch (str) {
case "SIMPLE" -> Examples::getSimpleScene;
case "SPHERES" -> Examples::getSpheres;
case "SQUARES" -> Examples::getSquares;
case "LIGHT" -> Examples::getLight;
case "CORNELL" -> Examples::getCornellBox;
case "CORNELL_SMOKE" -> Examples::getCornellBoxSmoke;
default -> throw fail("unknown example " + str + ", expected one of SIMPLE, SPHERES, SQUARES, LIGHT, CORNELL or CORNELL_SMOKE");
};
default -> throw fail("unknown option " + args[i]);
}
}
if (example == null) example = Examples::getCornellBoxSmoke;
if (path == null) path = Path.of("scene-" + System.currentTimeMillis() + ".png");
return new Config(example.apply(height), path, preview, iterative, samples, depth);
}
private static @NotNull RuntimeException fail(@NotNull String message) {
System.err.println(message);
System.exit(1);
return new RuntimeException();
}
}
private static class Examples {
public static @NotNull Example getSimpleScene(int height) {
if (height <= 0) height = 675;
return new Example(
new Scene(
getSkyBox(),
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))
),
SimpleCamera.builder()
.withImage(height * 16 / 9, height)
.build()
);
}
public static @NotNull Example getSpheres(int height) {
if (height <= 0) height = 675;
var rng = new Random(1);
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))));
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
var center = new Vec3(a + 0.9 * rng.nextDouble(), 0.2, b + 0.9 * rng.nextDouble());
if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue;
Material material;
var rnd = rng.nextDouble();
if (rnd < 0.8) {
// diffuse
var albedo = Color.multiply(Color.random(rng), Color.random(rng));
material = new LambertianMaterial(albedo);
} else if (rnd < 0.95) {
// metal
var albedo = Color.random(rng, 0.5, 1.0);
var fuzz = rng.nextDouble() * 0.5;
material = new MetallicMaterial(albedo, fuzz);
} else {
// glass
material = new DielectricMaterial(1.5);
}
objects.add(new Sphere(center, 0.2, material));
}
}
objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5)));
objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1))));
objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5))));
var camera = SimpleCamera.builder()
.withImage(height * 16 / 9, height)
.withPosition(new Vec3(13, 2, 3))
.withTarget(new Vec3(0, 0, 0))
.withFieldOfView(Math.toRadians(20))
.withFocusDistance(10.0)
.withBlurAngle(Math.toRadians(0.6))
.build();
return new Example(new Scene(getSkyBox(), objects), camera);
}
public static @NotNull Example getSquares(int height) {
if (height <= 0) height = 600;
return new Example(
new Scene(
getSkyBox(),
new Parallelogram(new Vec3(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(1.0, 0.2, 0.2))),
new Parallelogram(new Vec3(-2, -2, 0), new Vec3(4, 0, 0), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 1.0, 0.2))),
new Parallelogram(new Vec3(3, -2, 1), new Vec3(0, 0, 4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 0.2, 1.0))),
new Parallelogram(new Vec3(-2, 3, 1), new Vec3(4, 0, 0), new Vec3(0, 0, 4), new LambertianMaterial(new Color(1.0, 0.5, 0.0))),
new Parallelogram(new Vec3(-2, -3, 5), new Vec3(4, 0, 0), new Vec3(0, 0, -4), new LambertianMaterial(new Color(0.2, 0.8, 0.8)))
),
SimpleCamera.builder()
.withImage(height, height)
.withFieldOfView(Math.toRadians(80))
.withPosition(new Vec3(0, 0, 9))
.withTarget(new Vec3(0, 0, 0))
.build()
);
}
public static @NotNull Example getLight(int height) {
if (height <= 0) height = 225;
return new Example(
new Scene(
new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.2, 0.2, 0.2))),
new Sphere(new Vec3(0, 2, 0), 2, new LambertianMaterial(new Color(0.2, 0.2, 0.2))),
new Parallelogram(new Vec3(3, 1, -2), new Vec3(2, 0, 0), new Vec3(0, 2, 0), new DiffuseLight(new Color(4.0, 4.0, 4.0))),
new Sphere(new Vec3(0, 7, 0), 2, new DiffuseLight(new Color(4.0, 4.0, 4.0)))
),
SimpleCamera.builder()
.withImage(height * 16 / 9, height)
.withFieldOfView(Math.toRadians(20))
.withPosition(new Vec3(26, 3, 6))
.withTarget(new Vec3(0, 2, 0))
.build()
);
}
public static @NotNull Example getCornellBox(int height) {
if (height <= 0) height = 600;
var red = new LambertianMaterial(new Color(.65, .05, .05));
var white = new LambertianMaterial(new Color(.73, .73, .73));
var green = new LambertianMaterial(new Color(.12, .45, .15));
var light = new DiffuseLight(new Color(15.0, 15.0, 15.0));
return new Example(
new Scene(
new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green),
new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red),
new Parallelogram(new Vec3(343, 554, 332), new Vec3(-130, 0, 0), new Vec3(0, 0, -105), light),
new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0 ,0), new Vec3(0, 0, 555), white),
new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0 ,0), new Vec3(0, 0, -555), white),
new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0 ,0), new Vec3(0, 555, 0), white),
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65))
),
SimpleCamera.builder()
.withImage(height, height)
.withFieldOfView(Math.toRadians(40))
.withPosition(new Vec3(278, 278, -800))
.withTarget(new Vec3(278, 278, 0))
.build()
);
}
public static @NotNull Example getCornellBoxSmoke(int height) {
if (height <= 0) height = 600;
var red = new LambertianMaterial(new Color(.65, .05, .05));
var white = new LambertianMaterial(new Color(.73, .73, .73));
var green = new LambertianMaterial(new Color(.12, .45, .15));
var light = new DiffuseLight(new Color(7.0, 7.0, 7.0));
return new Example(
new Scene(
new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green),
new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red),
new Parallelogram(new Vec3(113, 554, 127), new Vec3(330, 0, 0), new Vec3(0, 0, 305), light),
new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0 ,0), new Vec3(0, 0, 555), white),
new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0 ,0), new Vec3(0, 0, -555), white),
new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0 ,0), new Vec3(0, 555, 0), white),
new ConstantMedium(
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
0.01, new IsotropicMaterial(Color.BLACK)
),
new ConstantMedium(
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)),
0.01, new IsotropicMaterial(Color.WHITE)
)
),
SimpleCamera.builder()
.withImage(height, height)
.withFieldOfView(Math.toRadians(40))
.withPosition(new Vec3(278, 278, -800))
.withTarget(new Vec3(278, 278, 0))
.build()
);
}
private static @NotNull SkyBox getSkyBox() {
return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE);
}
}
private record Example(@NotNull Scene scene, @NotNull Camera camera) {}
}

View File

@ -3,8 +3,18 @@ package eu.jonahbauer.raytracing.math;
import org.jetbrains.annotations.NotNull;
public record BoundingBox(@NotNull Vec3 min, @NotNull Vec3 max) {
public BoundingBox {
var a = min;
var b = max;
min = Vec3.min(a, b);
max = Vec3.max(a, b);
}
public @NotNull Vec3 center() {
return Vec3.average(min, max, 2);
}
public @NotNull BoundingBox expand(@NotNull BoundingBox box) {
return new BoundingBox(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max));
}
}

View File

@ -0,0 +1,259 @@
package eu.jonahbauer.raytracing.math;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Predicate;
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://doi.org/10.1007/978-3-642-76298-7_3">
* Agate, M., Grimsdale, R.L., Lister, P.F. (1991).
* The HERO Algorithm for Ray-Tracing Octrees.
* In: Grimsdale, R.L., Straßer, W. (eds) Advances in Computer Graphics Hardware IV. Eurographic Seminars. Springer, Berlin, Heidelberg.</a>
*/
public void hit(@NotNull Ray ray, @NotNull Predicate<T> action) {
storage.hit(ray, action);
}
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 static sealed abstract class Storage<T> {
protected static final int LIST_SIZE_LIMIT = 32;
protected final @NotNull Vec3 center;
protected final double dimension;
public Storage(@NotNull Vec3 center, double dimension) {
this.center = Objects.requireNonNull(center);
this.dimension = dimension;
}
public abstract @NotNull Storage<T> add(@NotNull Entry<T> entry);
protected abstract boolean hit(@NotNull Ray ray, @NotNull Predicate<T> action);
protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<T> action) {
return hit(ray, action);
}
}
private static final class ListStorage<T> extends Storage<T> {
private final @NotNull List<Entry<T>> list = new ArrayList<>();
public ListStorage(@NotNull Vec3 center, double dimension) {
super(center, 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
protected boolean hit(@NotNull Ray ray, @NotNull Predicate<T> action) {
var hit = false;
for (Entry<T> entry : list) {
hit |= action.test(entry.object());
}
return hit;
}
}
private static final class NodeStorage<T> extends Storage<T> {
@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) {
super(center, 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);
}
@Override
protected boolean hit(@NotNull Ray ray, @NotNull Predicate<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 false;
return hit0(ray, vmask, tlmax, tumin, action);
}
@Override
protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<T> action) {
if (tmax < 0) return false;
// check for hit
var hit = false;
// process entries spanning multiple children
for (Entry<T> entry : list) {
hit |= action.test(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 = calculateMasklist(tmid);
// the first child to be hit by the ray assuming a ray with positive x, y and z coordinates
var childmask = (tmid[0] < tmin ? 1 : 0)
| (tmid[1] < tmin ? 2 : 0)
| (tmid[2] < tmin ? 4 : 0);
// the last child to be hit by the ray assuming a ray with positive x, y and z coordinates
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) {
// use vmask to nullify the assumption of a positive ray made for childmask
var child = octants[childmask ^ vmask];
// calculate t value for exit of child
double childTmax;
if (childmask == lastmask) {
// last child shares tmax
childTmax = tmax;
} else {
// determine next child
while ((masklist[i] & childmask) != 0) {
i++;
}
childmask = childmask | masklist[i];
// tmax of current child is the t value for the intersection with the plane dividing the current and next child
childTmax = tmid[Integer.numberOfTrailingZeros(masklist[i])];
}
// process child
var childHit = child != null && child.hit0(ray, vmask, childTmin, childTmax, action);
hit |= childHit;
// break after last child has been processed or a hit has been found
if (childTmax == tmax || childHit) break;
// tmin of next child is tmax of current child
childTmin = childTmax;
}
return hit;
}
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 static final int[][] MASKLISTS = new int[][] {
{1, 2, 4},
{1, 4, 2},
{4, 1, 2},
{2, 1, 4},
{2, 4, 1},
{4, 2, 1}
};
private static int @NotNull [] calculateMasklist(double @NotNull[] tmid) {
if (tmid[0] < tmid[1]) {
if (tmid[1] < tmid[2]) {
return MASKLISTS[0]; // {1, 2, 4}
} else if (tmid[0] < tmid[2]) {
return MASKLISTS[1]; // {1, 4, 2}
} else {
return MASKLISTS[2]; // {4, 1, 2}
}
} else {
if (tmid[0] < tmid[2]) {
return MASKLISTS[3]; // {2, 1, 4}
} else if (tmid[1] < tmid[2]) {
return MASKLISTS[4]; // {2, 4, 1}
} else {
return MASKLISTS[5]; // {4, 2, 1}
}
}
}
}
private record Entry<T>(@NotNull BoundingBox bbox, T object) { }
}

View File

@ -11,7 +11,6 @@ public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
}
public @NotNull Vec3 at(double t) {
if (t < 0) throw new IllegalArgumentException("t must not be negative");
return new Vec3(
origin().x() + t * direction.x(),
origin().y() + t * direction.y(),

View File

@ -6,6 +6,8 @@ import java.util.Optional;
public record Vec3(double x, double y, double z) {
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_Y = new Vec3(0, 1, 0);
public static final Vec3 UNIT_Z = new Vec3(0, 0, 1);
@ -52,6 +54,38 @@ public record Vec3(double x, double y, double z) {
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) {
return new Vec3(this.x + b.x, this.y + b.y, this.z + b.z);
}

View File

@ -7,7 +7,6 @@ import java.util.Random;
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 WHITE = new Color(1.0, 1.0, 1.0);
public static final @NotNull Color SKY = new Color(0.5, 0.7, 1.0);
public static final @NotNull Color RED = new Color(1.0, 0.0, 0.0);
public static final @NotNull Color GREEN = new Color(0.0, 1.0, 0.0);
public static final @NotNull Color BLUE = new Color(0.0, 0.0, 1.0);
@ -26,6 +25,10 @@ public record Color(double r, double g, double b) {
return new Color(a.r() * b.r(), a.g() * b.g(), a.b() * b.b());
}
public static @NotNull Color add(@NotNull Color a, @NotNull Color b) {
return new Color(a.r() + b.r(), a.g() + b.g(), a.b() + b.b());
}
public static @NotNull Color random(@NotNull Random random) {
return new Color(random.nextDouble(), random.nextDouble(), random.nextDouble());
}
@ -65,25 +68,25 @@ public record Color(double r, double g, double b) {
}
}
public Color {
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");
}
}
public Color {}
public Color(int red, int green, int blue) {
this(red / 255f, green / 255f, blue / 255f);
}
public int red() {
return (int) (255.99 * r);
return toInt(r);
}
public int green() {
return (int) (255.99 * g);
return toInt(g);
}
public int blue() {
return (int) (255.99 * b);
return toInt(b);
}
private static int toInt(double value) {
return Math.max(0, Math.min(255, (int) (255.99 * value)));
}
}

View File

@ -42,15 +42,19 @@ public final class LiveCanvas implements Canvas {
public @NotNull Thread preview() {
var frame = new JFrame();
frame.setSize(getWidth(), getHeight());
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setContentPane(new JPanel() {
{
setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
}
@Override
protected void paintComponent(Graphics g) {
g.drawImage(image, 0, 0, null);
}
});
frame.setResizable(false);
frame.pack();
frame.setVisible(true);
var update = Thread.ofVirtual().start(() -> {

View File

@ -1,4 +1,4 @@
package eu.jonahbauer.raytracing.material;
package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;

View File

@ -0,0 +1,20 @@
package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public record DiffuseLight(@NotNull Color emit) implements Material {
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
return Optional.empty();
}
@Override
public @NotNull Color emitted(@NotNull HitResult hit) {
return emit;
}
}

View File

@ -0,0 +1,16 @@
package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public record IsotropicMaterial(@NotNull Color albedo) implements Material{
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
return Optional.of(new ScatterResult(new Ray(hit.position(), Vec3.random(true)), albedo()));
}
}

View File

@ -1,4 +1,4 @@
package eu.jonahbauer.raytracing.material;
package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;

View File

@ -1,4 +1,4 @@
package eu.jonahbauer.raytracing.material;
package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.Color;
@ -12,6 +12,10 @@ public interface Material {
@NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit);
default @NotNull Color emitted(@NotNull HitResult hit) {
return Color.BLACK;
}
record ScatterResult(@NotNull Ray ray, @NotNull Color attenuation) {
public ScatterResult {
Objects.requireNonNull(ray, "ray");

View File

@ -1,4 +1,4 @@
package eu.jonahbauer.raytracing.material;
package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;

View File

@ -86,18 +86,28 @@ public final class SimpleRenderer implements Renderer {
}
private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth) {
if (depth <= 0) return Color.BLACK;
var color = Color.BLACK;
var attenuation = Color.WHITE;
while (depth-- > 0) {
var optional = scene.hit(ray, new Range(0.001, Double.POSITIVE_INFINITY));
if (optional.isEmpty()) {
color = Color.add(color, Color.multiply(attenuation, scene.getBackgroundColor(ray)));
break;
}
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);
var emitted = material.emitted(hit);
var scatter = material.scatter(ray, hit);
color = Color.add(color, Color.multiply(attenuation, emitted));
if (scatter.isEmpty()) break;
attenuation = Color.multiply(attenuation, scatter.get().attenuation());
ray = scatter.get().ray();
}
return color;
}
/**
@ -111,19 +121,6 @@ public final class SimpleRenderer implements Renderer {
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;

View File

@ -1,7 +1,7 @@
package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.material.Material;
import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
@ -14,7 +14,6 @@ public record HitResult(
boolean frontFace
) implements Comparable<HitResult> {
public HitResult {
if (t < 0 || !Double.isFinite(t)) throw new IllegalArgumentException("t must be non-negative");
Objects.requireNonNull(position, "position");
normal = normal.unit();
}

View File

@ -3,6 +3,9 @@ package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.transform.RotateY;
import eu.jonahbauer.raytracing.scene.transform.Translate;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
@ -19,4 +22,12 @@ public interface Hittable {
default @NotNull Optional<BoundingBox> getBoundingBox() {
return Optional.empty();
}
default @NotNull Hittable translate(@NotNull Vec3 offset) {
return new Translate(this, offset);
}
default @NotNull Hittable rotateY(double angle) {
return new RotateY(this, angle);
}
}

View File

@ -1,31 +1,64 @@
package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.scene.util.HittableCollection;
import eu.jonahbauer.raytracing.scene.util.HittableList;
import eu.jonahbauer.raytracing.scene.util.HittableOctree;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public record Scene(@NotNull List<@NotNull Hittable> objects) implements Hittable {
public final class Scene extends HittableCollection {
private final @NotNull HittableOctree octree;
private final @NotNull HittableList list;
private final @NotNull SkyBox background;
public Scene {
objects = List.copyOf(objects);
public Scene(@NotNull List<? extends @NotNull Hittable> objects) {
this(Color.BLACK, objects);
}
public Scene(@NotNull Hittable @NotNull ... objects) {
public Scene(@NotNull Color background, @NotNull List<? extends @NotNull Hittable> objects) {
this(SkyBox.solid(background), objects);
}
public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects) {
var bounded = new ArrayList<Hittable>();
var unbounded = new ArrayList<Hittable>();
objects.forEach(object -> {
if (object.getBoundingBox().isPresent()) {
bounded.add(object);
} else {
unbounded.add(object);
}
});
this.octree = new HittableOctree(bounded);
this.list = new HittableList(unbounded);
this.background = background;
}
public Scene(@NotNull Hittable @NotNull... objects) {
this(List.of(objects));
}
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
var result = (HitResult) null;
for (var object : objects) {
var r = object.hit(ray, range);
if (r.isPresent() && range.surrounds(r.get().t())) {
result = r.get();
range = new Range(range.min(), result.t());
}
}
return Optional.ofNullable(result);
public Scene(@NotNull Color background, @NotNull Hittable @NotNull... objects) {
this(background, List.of(objects));
}
public Scene(@NotNull SkyBox background, @NotNull Hittable @NotNull... objects) {
this(background, List.of(objects));
}
@Override
public void hit(@NotNull Ray ray, @NotNull State state) {
octree.hit(ray, state);
list.hit(ray, state);
}
public @NotNull Color getBackgroundColor(@NotNull Ray ray) {
return background.getColor(ray);
}
}

View File

@ -0,0 +1,25 @@
package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.Color;
import org.jetbrains.annotations.NotNull;
@FunctionalInterface
public interface SkyBox {
@NotNull Color getColor(@NotNull Ray ray);
static @NotNull SkyBox gradient(@NotNull Color top, @NotNull Color bottom) {
return 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(bottom, top, alt / Math.PI + 0.5);
};
}
static @NotNull SkyBox solid(@NotNull Color color) {
return _ -> color;
}
}

View File

@ -0,0 +1,26 @@
package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Ellipse extends Hittable2D {
public Ellipse(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material);
}
@Override
protected boolean isInterior(double alpha, double beta) {
return alpha * alpha + beta * beta < 1;
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
var a = origin.minus(u).minus(v);
var b = origin.plus(u).plus(v);
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
}
}

View File

@ -0,0 +1,64 @@
package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
public abstract class Hittable2D implements Hittable {
protected final @NotNull Vec3 origin;
protected final @NotNull Vec3 u;
protected final @NotNull Vec3 v;
private final @NotNull Material material;
// internal
private final @NotNull Vec3 normal;
private final double d;
private final @NotNull Vec3 w;
protected Hittable2D(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
this.origin = Objects.requireNonNull(origin);
this.u = Objects.requireNonNull(u);
this.v = Objects.requireNonNull(v);
this.material = Objects.requireNonNull(material);
var n = u.cross(v);
if (n.squared() < 1e-8) throw new IllegalArgumentException();
this.normal = n.unit();
this.d = origin.times(normal);
this.w = n.div(n.squared());
}
@Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
var denominator = ray.direction().times(normal);
if (Math.abs(denominator) < 1e-8) return Optional.empty(); // parallel
var t = (d - ray.origin().times(normal)) / denominator;
if (!range.surrounds(t)) return Optional.empty();
var position = ray.at(t);
var p = position.minus(origin);
if (!isInterior(p)) return Optional.empty();
var frontFace = denominator < 0;
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace));
}
protected boolean isInterior(@NotNull Vec3 p) {
var alpha = w.times(p.cross(v));
var beta = w.times(u.cross(p));
return isInterior(alpha, beta);
}
protected boolean isInterior(double alpha, double beta) {
return false;
}
}

View File

@ -0,0 +1,27 @@
package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Parallelogram extends Hittable2D {
public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material);
}
@Override
protected boolean isInterior(double alpha, double beta) {
return 0 <= alpha && alpha < 1 && 0 <= beta && beta < 1;
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
var a = origin;
var b = origin.plus(u).plus(v);
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
}
}

View File

@ -0,0 +1,16 @@
package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull;
public final class Plane extends Hittable2D {
public Plane(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material);
}
@Override
protected boolean isInterior(@NotNull Vec3 p) {
return true;
}
}

View File

@ -0,0 +1,27 @@
package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Triangle extends Hittable2D {
public Triangle(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material);
}
@Override
protected boolean isInterior(double alpha, double beta) {
return 0 <= alpha && 0 <= beta && alpha + beta <= 1;
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
var a = origin;
var b = origin.plus(u).plus(v);
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
}
}

View File

@ -0,0 +1,42 @@
package eu.jonahbauer.raytracing.scene.hittable3d;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.IsotropicMaterial;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNull IsotropicMaterial material) implements Hittable {
@Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
var hit1 = boundary.hit(ray, Range.UNIVERSE);
if (hit1.isEmpty()) return Optional.empty();
var hit2 = boundary.hit(ray, new Range(hit1.get().t() + 0.0001, Double.POSITIVE_INFINITY));
if (hit2.isEmpty()) return Optional.empty();
var tmin = Math.max(range.min(), hit1.get().t());
var tmax = Math.min(range.max(), hit2.get().t());
if (tmin >= tmax) return Optional.empty();
if (tmin < 0) tmin = 0;
var length = ray.direction().length();
var distance = length * (tmax - tmin);
var hitDistance = - Math.log(Math.random()) / density;
if (hitDistance > distance) return Optional.empty();
var t = tmin + hitDistance / length;
return Optional.of(new HitResult(t, ray.at(t), Vec3.UNIT_X, material, true)); // arbitrary normal and frontFace
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
return boundary().getBoundingBox();
}
}

View File

@ -1,10 +1,12 @@
package eu.jonahbauer.raytracing.scene;
package eu.jonahbauer.raytracing.scene.hittable3d;
import eu.jonahbauer.raytracing.material.Material;
import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;

View File

@ -0,0 +1,91 @@
package eu.jonahbauer.raytracing.scene.transform;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class RotateY extends Transform {
private final double cos;
private final double sin;
private final @NotNull Optional<BoundingBox> bbox;
public RotateY(@NotNull Hittable object, double angle) {
super(object);
this.cos = Math.cos(angle);
this.sin = Math.sin(angle);
this.bbox = object.getBoundingBox().map(bbox -> {
var min = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
var max = new Vec3(- Double.MAX_VALUE, - Double.MAX_VALUE, - Double.MAX_VALUE);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
var x = i * bbox.max().x() + (1 - i) * bbox.min().x();
var y = j * bbox.max().y() + (1 - j) * bbox.min().y();
var z = k * bbox.max().z() + (1 - k) * bbox.min().z();
var newx = cos * x + sin * z;
var newz = -sin * x + cos * z;
var temp = new Vec3(newx, y, newz);
min = Vec3.min(min, temp);
max = Vec3.max(max, temp);
}
}
}
return new BoundingBox(min, max);
});
}
@Override
protected @NotNull Ray transform(@NotNull Ray ray) {
var origin = ray.origin();
var direction = ray.direction();
var newOrigin = new Vec3(
cos * origin.x() - sin * origin.z(),
origin.y(),
sin * origin.x() + cos * origin.z()
);
var newDirection = new Vec3(
cos * direction.x() - sin * direction.z(),
direction.y(),
sin * direction.x() + cos * direction.z()
);
return new Ray(newOrigin, newDirection);
}
@Override
protected @NotNull HitResult transform(@NotNull HitResult result) {
var position = result.position();
var newPosition = new Vec3(
cos * position.x() + sin * position.z(),
position.y(),
- sin * position.x() + cos * position.z()
);
var normal = result.normal();
var newNormal = new Vec3(
cos * normal.x() + sin * normal.z(),
normal.y(),
-sin * normal.x() + cos * normal.z()
);
return new HitResult(result.t(), newPosition, newNormal, result.material(), result.frontFace());
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
return bbox;
}
}

View File

@ -0,0 +1,27 @@
package eu.jonahbauer.raytracing.scene.transform;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
public abstract class Transform implements Hittable {
protected final @NotNull Hittable object;
protected Transform(@NotNull Hittable object) {
this.object = Objects.requireNonNull(object);
}
protected abstract @NotNull Ray transform(@NotNull Ray ray);
protected abstract @NotNull HitResult transform(@NotNull HitResult result);
@Override
public final @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
return object.hit(transform(ray), range).map(this::transform);
}
}

View File

@ -0,0 +1,46 @@
package eu.jonahbauer.raytracing.scene.transform;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Translate extends Transform {
private final @NotNull Vec3 offset;
private final @NotNull Optional<BoundingBox> bbox;
public Translate(@NotNull Hittable object, @NotNull Vec3 offset) {
super(object);
this.offset = offset;
this.bbox = object.getBoundingBox().map(bbox -> new BoundingBox(
bbox.min().plus(offset),
bbox.max().plus(offset)
));
}
@Override
protected @NotNull Ray transform(@NotNull Ray ray) {
return new Ray(ray.origin().minus(offset), ray.direction());
}
@Override
protected @NotNull HitResult transform(@NotNull HitResult result) {
return new HitResult(
result.t(),
result.position().plus(offset),
result.normal(),
result.material(),
result.frontFace()
);
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
return bbox;
}
}

View File

@ -0,0 +1,65 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
public abstract class HittableCollection implements Hittable {
@Override
public final @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
var state = new State(range);
hit(ray, state);
return state.getResult();
}
public abstract void hit(@NotNull Ray ray, @NotNull State state);
protected static @NotNull Optional<BoundingBox> getBoundingBox(@NotNull Collection<? extends @NotNull Hittable> objects) {
var bbox = new BoundingBox(Vec3.ZERO, Vec3.ZERO);
for (var object : objects) {
var b = object.getBoundingBox();
if (b.isPresent()) {
bbox = bbox.expand(b.get());
} else {
bbox = null;
break;
}
}
return Optional.ofNullable(bbox);
}
protected static boolean hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object) {
var r = object.hit(ray, state.range);
if (r.isPresent()) {
if (state.range.surrounds(r.get().t())){
state.result = r.get();
state.range = new Range(state.range.min(), state.result.t());
}
return true;
} else {
return false;
}
}
public static class State {
private @NotNull Range range;
private HitResult result;
private State(@NotNull Range range) {
this.range = Objects.requireNonNull(range);
}
private @NotNull Optional<HitResult> getResult() {
return Optional.ofNullable(result);
}
}
}

View File

@ -0,0 +1,33 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
public final class HittableList extends HittableCollection {
private final @NotNull List<Hittable> objects;
private final @NotNull Optional<BoundingBox> bbox;
public HittableList(@NotNull List<? extends @NotNull Hittable> objects) {
this.objects = List.copyOf(objects);
this.bbox = getBoundingBox(this.objects);
}
public HittableList(@NotNull Hittable @NotNull... objects) {
this(List.of(objects));
}
@Override
public void hit(@NotNull Ray ray, @NotNull State state) {
objects.forEach(object -> hit(state, ray, object));
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
return bbox;
}
}

View File

@ -0,0 +1,61 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.*;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
public final class HittableOctree extends HittableCollection {
private final @NotNull Octree<Hittable> objects;
private final @NotNull Optional<BoundingBox> bbox;
public HittableOctree(@NotNull List<? extends @NotNull Hittable> objects) {
var result = newOctree(objects);
this.objects = result.getKey();
this.bbox = Optional.of(result.getValue());
}
public HittableOctree(@NotNull Hittable @NotNull... objects) {
this(List.of(objects));
}
@Override
public void hit(@NotNull Ray ray, @NotNull State state) {
objects.hit(ray, object -> hit(state, ray, object));
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
return bbox;
}
private static @NotNull Entry<@NotNull Octree<Hittable>, @NotNull BoundingBox> newOctree(@NotNull List<? extends Hittable> objects) {
Vec3 center = Vec3.ZERO, max = Vec3.MIN, min = Vec3.MAX;
int i = 1;
for (var object : objects) {
var bbox = object.getBoundingBox().orElseThrow();
center = Vec3.average(center, bbox.center(), i++);
max = Vec3.max(max, bbox.max());
min = Vec3.min(min, bbox.min());
}
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);
var out = new Octree<Hittable>(center, dimension);
objects.forEach(object -> out.add(object.getBoundingBox().get(), object));
return Map.entry(out, new BoundingBox(min, max));
}
}

View File

@ -0,0 +1,35 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.hittable2d.Parallelogram;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
public final class Hittables {
private Hittables() {
throw new UnsupportedOperationException();
}
public static @NotNull Hittable box(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Material material) {
var sides = new ArrayList<Hittable>();
var min = Vec3.min(a, b);
var max = Vec3.max(a, b);
var dx = new Vec3(max.x() - min.x(), 0, 0);
var dy = new Vec3(0, max.y() - min.y(), 0);
var dz = new Vec3(0, 0, max.z() - min.z());
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), max.z()), dx, dy, material)); // front
sides.add(new Parallelogram(new Vec3(max.x(), min.y(), max.z()), dz.neg(), dy, material)); // right
sides.add(new Parallelogram(new Vec3(max.x(), min.y(), min.z()), dx.neg(), dy, material)); // back
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), min.z()), dz, dy, material)); // left
sides.add(new Parallelogram(new Vec3(min.x(), max.y(), max.z()), dx, dz.neg(), material)); // top
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), min.z()), dx, dz, material)); // bottom
return new HittableList(sides);
}
}

View File

@ -1,5 +1,7 @@
package eu.jonahbauer.raytracing.render;
package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.render.ImageFormat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -17,18 +19,18 @@ class ImageTest {
void test(@TempDir Path dir) throws IOException {
var image = new Image(256, 256);
for (var y = 0; y < image.height(); y++) {
for (var x = 0; x < image.width(); x++) {
var r = (double) x / (image.width() - 1);
var g = (double) y / (image.height() - 1);
for (var y = 0; y < image.getHeight(); y++) {
for (var x = 0; x < image.getWidth(); x++) {
var r = (double) x / (image.getWidth() - 1);
var g = (double) y / (image.getHeight() - 1);
var b = 0;
image.set(x, y, r, g, b);
image.set(x, y, new Color(r, g, b));
}
}
System.out.println(dir);
ImageIO.write(image, dir.resolve("img.ppm"));
ImageFormat.PPM.write(image, dir.resolve("img.ppm"));
String expected;
String actual;

View File

@ -1,8 +1,10 @@
package eu.jonahbauer.raytracing.scene;
package eu.jonahbauer.raytracing.scene.hittable3d;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.render.material.LambertianMaterial;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@ -13,7 +15,7 @@ class SphereTest {
void hit() {
var center = new Vec3(1, 2, 3);
var radius = 5;
var sphere = new Sphere(center, radius);
var sphere = new Sphere(center, radius, new LambertianMaterial(Color.WHITE));
var origin = new Vec3(6, 7, 8);
var direction = new Vec3(-1, -1, -1);