Compare commits

...

7 Commits

39 changed files with 872 additions and 726 deletions

View File

@ -32,3 +32,11 @@ java -jar raytracing.jar --samples 50000 --height 1200 CORNELL
```
java -jar raytracing.jar --samples 50000 --height 600 CORNELL_SMOKE
```
### diagramm
![](./docs/diagramm.png)
```
java -jar raytracing.jar --samples 1000 --height 1080 DIAGRAMM
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

After

Width:  |  Height:  |  Size: 441 KiB

BIN
docs/diagramm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 KiB

After

Width:  |  Height:  |  Size: 881 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

After

Width:  |  Height:  |  Size: 620 KiB

View File

@ -0,0 +1,8 @@
package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.render.camera.Camera;
import eu.jonahbauer.raytracing.scene.Scene;
import org.jetbrains.annotations.NotNull;
public record Example(@NotNull Scene scene, @NotNull Camera camera) {
}

View File

@ -0,0 +1,257 @@
package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
import eu.jonahbauer.raytracing.render.material.*;
import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.Scene;
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.util.*;
import java.util.function.IntFunction;
public class Examples {
private static final Map<String, IntFunction<Example>> REGISTRY = new HashMap<>();
private static void register(@NotNull String name, @NotNull IntFunction<Example> example) {
REGISTRY.put(name, example);
}
static {
register("SIMPLE", Examples::getSimpleScene);
register("SPHERES", Examples::getSpheres);
register("SQUARES", Examples::getSquares);
register("LIGHT", Examples::getLight);
register("CORNELL", Examples::getCornellBox);
register("CORNELL_SMOKE", Examples::getCornellBoxSmoke);
register("DIAGRAMM", Examples::getDiagramm);
}
public static @NotNull IntFunction<Example> getByName(@NotNull String name) {
var out = REGISTRY.get(name);
if (out == null) throw new IllegalArgumentException("unknown example " + name + ", expected one of " + REGISTRY.keySet());
return out;
}
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()
);
}
public static @NotNull Example getDiagramm(int height) {
if (height <= 0) height = 450;
record Partei(String name, Color color, double stimmen) { }
var data = List.of(
new Partei("CDU", new Color(0x00, 0x4B, 0x76), 18.9),
new Partei("SPD", new Color(0xC0, 0x00, 0x3C), 25.7),
new Partei("AfD", new Color(0x80, 0xCD, 0xEC), 10.3),
new Partei("FDP", new Color(0xF7, 0xBB, 0x3D), 11.5),
new Partei("DIE LINKE", new Color(0x5F, 0x31, 0x6E), 4.9),
new Partei("GRÜNE", new Color(0x00, 0x85, 0x4A), 14.8),
new Partei("CSU", new Color(0x00, 0x77, 0xB6), 5.2)
);
var white = new LambertianMaterial(new Color(.99, .99, .99));
var count = data.size();
var size = 75d;
var spacing = 50d;
var x = (count + 1) * spacing + count * size + 1000;
var y = 500 + 1000;
var z = 2 * spacing + size + 1000;
var objects = new ArrayList<Hittable>();
objects.add(new Parallelogram(new Vec3(0, 0, 0), new Vec3(x, 0, 0), new Vec3(0, y, 0), white));
objects.add(new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 0, z), new Vec3(x, 0, 0), white));
objects.add(new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, y, 0), new Vec3(0, 0, z), white));
for (int i = 0; i < data.size(); i++) {
var partei = data.get(i);
objects.add(Hittables.box(
new Vec3((i + 1) * spacing + i * size, 0, spacing),
new Vec3((i + 1) * spacing + (i + 1) * size, partei.stimmen() * 15, spacing + size),
new DielectricMaterial(1.5, partei.color())
));
}
return new Example(
new Scene(new Color(1.25, 1.25, 1.25), objects),
SimpleCamera.builder()
.withImage(height * 16 / 9, height)
.withPosition(new Vec3(700, 250, 800))
.withTarget(new Vec3(500, 200, 0))
.withFieldOfView(Math.toRadians(40))
.build()
);
}
private static @NotNull SkyBox getSkyBox() {
return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE);
}
}

View File

@ -1,29 +1,15 @@
package eu.jonahbauer.raytracing;
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.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.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.Random;
import java.util.function.IntFunction;
public class Main {
@ -37,6 +23,7 @@ public class Main {
.withSamplesPerPixel(config.samples)
.withMaxDepth(config.depth)
.withIterative(config.iterative)
.withParallel(config.parallel)
.build();
Canvas canvas;
@ -55,13 +42,13 @@ public class Main {
ImageFormat.PNG.write(canvas, config.path);
}
private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, int samples, int depth) {
private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, boolean parallel, 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;
boolean parallel = false;
int samples = 1000;
int depth = 50;
int height = -1;
@ -80,6 +67,8 @@ public class Main {
case "--no-preview" -> preview = false;
case "--iterative" -> iterative = true;
case "--no-iterative" -> iterative = false;
case "--parallel" -> parallel = true;
case "--no-parallel" -> parallel = false;
case "--samples" -> {
if (i + 1 == args.length) throw fail("missing value for parameter --samples");
try {
@ -107,22 +96,20 @@ public class Main {
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");
};
case String str when !str.startsWith("-") -> {
try {
example = Examples.getByName(str);
} catch (IllegalArgumentException ex) {
throw fail(ex.getMessage());
}
}
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);
return new Config(example.apply(height), path, preview, iterative, parallel, samples, depth);
}
private static @NotNull RuntimeException fail(@NotNull String message) {
@ -130,178 +117,5 @@ public class Main {
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

@ -0,0 +1,107 @@
package eu.jonahbauer.raytracing.math;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
/**
* An axis-aligned bounding box.
*/
public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
public static final AABB UNIVERSE = new AABB(Vec3.MIN, Vec3.MAX);
public static final AABB EMPTY = new AABB(Vec3.ZERO, Vec3.ZERO);
public static final Comparator<AABB> X_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::x));
public static final Comparator<AABB> Y_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::y));
public static final Comparator<AABB> Z_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::z));
public AABB {
var a = min;
var b = max;
min = Vec3.min(a, b);
max = Vec3.max(a, b);
}
public AABB(@NotNull Range x, @NotNull Range y, @NotNull Range z) {
this(new Vec3(x.min(), y.min(), z.min()), new Vec3(x.max(), y.max(), z.max()));
}
public static @NotNull Optional<AABB> getBoundingBox(@NotNull List<? extends @NotNull Hittable> objects) {
var bbox = (AABB) null;
for (var object : objects) {
bbox = bbox == null ? object.getBoundingBox() : bbox.expand(object.getBoundingBox());
}
return Optional.ofNullable(bbox);
}
public @NotNull Range x() {
return new Range(min.x(), max.x());
}
public @NotNull Range y() {
return new Range(min.y(), max.y());
}
public @NotNull Range z() {
return new Range(min.z(), max.z());
}
public @NotNull Vec3 center() {
return Vec3.average(min, max, 2);
}
public @NotNull AABB expand(@NotNull AABB box) {
return new AABB(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max));
}
public boolean hit(@NotNull Ray ray) {
return intersect(ray).isPresent();
}
public @NotNull Optional<Range> intersect(@NotNull Ray ray) {
if (this == UNIVERSE) return Optional.of(Range.UNIVERSE);
if (this == EMPTY) return Optional.empty();
int vmask = ray.vmask();
var origin = ray.origin();
var invDirection = ray.direction().inv();
// calculate t values for intersection points of ray with planes through min
var tmin = intersect(min(), origin, invDirection);
// calculate t values for intersection points of ray with planes through max
var tmax = intersect(max(), origin, invDirection);
// 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]);
}
}
return tlmax < tumin ? Optional.of(new Range(tlmax, tumin)) : Optional.empty();
}
public static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Ray ray) {
return intersect(corner, ray.origin(), ray.direction().inv());
}
private static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Vec3 origin, @NotNull Vec3 invDirection) {
return new double[] {
(corner.x() - origin.x()) * invDirection.x(),
(corner.y() - origin.y()) * invDirection.y(),
(corner.z() - origin.z()) * invDirection.z(),
};
}
}

View File

@ -1,20 +0,0 @@
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

@ -1,259 +0,0 @@
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

@ -16,4 +16,8 @@ public record Range(double min, double max) {
public boolean surrounds(double value) {
return min < value && value < max;
}
public double size() {
return max - min;
}
}

View File

@ -17,4 +17,10 @@ public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
origin().z() + t * direction.z()
);
}
public int vmask() {
return (direction().x() < 0 ? 1 : 0)
| (direction().y() < 0 ? 2 : 0)
| (direction().z() < 0 ? 4 : 0);
}
}

View File

@ -3,6 +3,7 @@ package eu.jonahbauer.raytracing.math;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.random.RandomGenerator;
public record Vec3(double x, double y, double z) {
public static final Vec3 ZERO = new Vec3(0, 0, 0);
@ -16,17 +17,17 @@ public record Vec3(double x, double y, double z) {
assert Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z) : "x, y and z must be finite";
}
public static @NotNull Vec3 random() {
return random(false);
public static @NotNull Vec3 random(@NotNull RandomGenerator random) {
return random(random, false);
}
public static @NotNull Vec3 random(boolean unit) {
var random = new Vec3(
2 * Math.random() - 1,
2 * Math.random() - 1,
2 * Math.random() - 1
public static @NotNull Vec3 random(@NotNull RandomGenerator random, boolean unit) {
var vec = new Vec3(
2 * random.nextDouble() - 1,
2 * random.nextDouble() - 1,
2 * random.nextDouble() - 1
);
return unit ? random.unit() : random;
return unit ? vec.unit() : vec;
}
public static @NotNull Vec3 reflect(@NotNull Vec3 vec, @NotNull Vec3 normal) {
@ -106,6 +107,10 @@ public record Vec3(double x, double y, double z) {
return new Vec3(-x, -y, -z);
}
public @NotNull Vec3 inv() {
return new Vec3(1 / x, 1 / y, 1 / z);
}
public @NotNull Vec3 cross(@NotNull Vec3 b) {
return new Vec3(
this.y() * b.z() - b.y() * this.z(),

View File

@ -1,6 +1,7 @@
package eu.jonahbauer.raytracing.render;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.canvas.Image;
import org.jetbrains.annotations.NotNull;
import java.io.*;
@ -10,6 +11,7 @@ import java.nio.file.Path;
import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public enum ImageFormat {
PPM {
@ -41,6 +43,7 @@ public enum ImageFormat {
private static final int IHDR_TYPE = 0x49484452;
private static final int IDAT_TYPE = 0x49444154;
private static final int IEND_TYPE = 0x49454E44;
private static final int IEND_CRC = 0xAE426082;
@Override
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
@ -92,7 +95,7 @@ public enum ImageFormat {
}
var bytes = baos.toByteArray();
data.writeInt(bytes.length);
data.writeInt(bytes.length - 4); // don't include type in length
data.write(bytes);
data.writeInt((int) crc.getChecksum().getValue());
}
@ -101,7 +104,7 @@ public enum ImageFormat {
private void writeIEND(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
data.writeInt(0);
data.writeInt(IEND_TYPE);
data.writeInt(0);
data.writeInt(IEND_CRC);
}
private static class NoCloseDataOutputStream extends DataOutputStream {

View File

@ -3,6 +3,8 @@ package eu.jonahbauer.raytracing.render.camera;
import eu.jonahbauer.raytracing.math.Ray;
import org.jetbrains.annotations.NotNull;
import java.util.random.RandomGenerator;
public interface Camera {
/**
* {@return the width of this camera in pixels}
@ -18,5 +20,5 @@ public interface Camera {
* Casts a ray through the given pixel.
* @return a new ray
*/
@NotNull Ray cast(int x, int y);
@NotNull Ray cast(int x, int y, @NotNull RandomGenerator random);
}

View File

@ -6,6 +6,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.random.RandomGenerator;
public final class SimpleCamera implements Camera {
// image size
@ -79,12 +80,12 @@ public final class SimpleCamera implements Camera {
/**
* {@inheritDoc}
*/
public @NotNull Ray cast(int x, int y) {
public @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random) {
Objects.checkIndex(x, width);
Objects.checkIndex(y, height);
var origin = getRayOrigin();
var target = getRayTarget(x, y);
var origin = getRayOrigin(random);
var target = getRayTarget(x, y, random);
return new Ray(origin, target.minus(origin));
}
@ -93,12 +94,12 @@ public final class SimpleCamera implements Camera {
* radius {@link #blurRadius} centered on the camera position and perpendicular to the direction to simulate depth
* of field.
*/
private @NotNull Vec3 getRayOrigin() {
private @NotNull Vec3 getRayOrigin(@NotNull RandomGenerator random) {
if (blurRadius <= 0) return origin;
while (true) {
var du = 2 * Math.random() - 1;
var dv = 2 * Math.random() - 1;
var du = 2 * random.nextDouble() - 1;
var dv = 2 * random.nextDouble() - 1;
if (du * du + dv * dv >= 1) continue;
var ru = blurRadius * du;
@ -115,9 +116,9 @@ public final class SimpleCamera implements Camera {
/**
* {@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;
private @NotNull Vec3 getRayTarget(int x, int y, @NotNull RandomGenerator random) {
double dx = x + random.nextDouble() - 0.5;
double dy = y + random.nextDouble() - 0.5;
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
}

View File

@ -6,21 +6,31 @@ import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
import java.util.random.RandomGenerator;
public record DielectricMaterial(double refractionIndex, @NotNull Color albedo) implements Material {
public DielectricMaterial(double refractionIndex) {
this(refractionIndex, Color.WHITE);
}
public DielectricMaterial {
Objects.requireNonNull(albedo, "albedo");
}
public record DielectricMaterial(double refractionIndex) implements Material {
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var ri = hit.frontFace() ? (1 / refractionIndex) : refractionIndex;
var cosTheta = Math.min(- ray.direction().unit().times(hit.normal()), 1.0);
var reflectance = reflectance(cosTheta);
var reflect = reflectance > Math.random();
var reflect = reflectance > random.nextDouble();
var newDirection = (reflect ? Optional.<Vec3>empty() : Vec3.refract(ray.direction(), hit.normal(), ri))
.orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal()));
return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), Color.WHITE));
return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), albedo));
}
private double reflectance(double cos) {

View File

@ -6,10 +6,11 @@ import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.random.RandomGenerator;
public record DiffuseLight(@NotNull Color emit) implements Material {
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
return Optional.empty();
}

View File

@ -7,10 +7,11 @@ import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.random.RandomGenerator;
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()));
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
return Optional.of(new ScatterResult(new Ray(hit.position(), Vec3.random(random, true)), albedo()));
}
}

View File

@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
import java.util.random.RandomGenerator;
public record LambertianMaterial(@NotNull Color albedo) implements Material {
public LambertianMaterial {
@ -15,8 +16,8 @@ public record LambertianMaterial(@NotNull Color albedo) implements Material {
}
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
var newDirection = hit.normal().plus(Vec3.random(true));
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var newDirection = hit.normal().plus(Vec3.random(random, true));
if (newDirection.isNearZero()) newDirection = hit.normal();
var scattered = new Ray(hit.position(), newDirection);

View File

@ -7,10 +7,11 @@ import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
import java.util.random.RandomGenerator;
public interface Material {
@NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit);
@NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random);
default @NotNull Color emitted(@NotNull HitResult hit) {
return Color.BLACK;

View File

@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
import java.util.random.RandomGenerator;
public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Material {
@ -21,10 +22,10 @@ public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Ma
}
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var newDirection = Vec3.reflect(ray.direction(), hit.normal());
if (fuzz > 0) {
newDirection = newDirection.unit().plus(Vec3.random(true).times(fuzz));
newDirection = newDirection.unit().plus(Vec3.random(random, true).times(fuzz));
}
return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), albedo));
}

View File

@ -8,9 +8,10 @@ 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.Random;
import java.util.SplittableRandom;
import java.util.random.RandomGenerator;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
public final class SimpleRenderer implements Renderer {
private final int samplesPerPixel;
@ -44,36 +45,40 @@ public final class SimpleRenderer implements Renderer {
}
if (iterative) {
var random = new Random();
// 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);
getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
for (int x = 0; x < camera.getWidth(); x++) {
var ray = camera.cast(x, y, random);
var c = getColor(scene, ray, random);
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;
getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
for (int x = 0; x < camera.getWidth(); x++) {
canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma));
}
});
} else {
var splittable = new SplittableRandom();
// render one pixel after the other
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> {
var y = (int) (pixel >> 32);
var x = (int) pixel;
getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
var random = splittable.split();
for (int x = 0; x < camera.getWidth(); x++) {
var color = Color.BLACK;
for (int i = 1; i <= samplesPerPixel; i++) {
var ray = camera.cast(x, y);
var c = getColor(scene, ray);
var ray = camera.cast(x, y, random);
var c = getColor(scene, ray, random);
color = Color.average(color, c, i);
}
canvas.set(x, y, Color.gamma(color, gamma));
}
});
}
}
@ -81,11 +86,11 @@ public final class SimpleRenderer implements Renderer {
/**
* {@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 getColor(@NotNull Scene scene, @NotNull Ray ray, @NotNull RandomGenerator random) {
return getColor0(scene, ray, maxDepth, random);
}
private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth) {
private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) {
var color = Color.BLACK;
var attenuation = Color.WHITE;
@ -99,7 +104,7 @@ public final class SimpleRenderer implements Renderer {
var hit = optional.get();
var material = hit.material();
var emitted = material.emitted(hit);
var scatter = material.scatter(ray, hit);
var scatter = material.scatter(ray, hit, random);
color = Color.add(color, Color.multiply(attenuation, emitted));
if (scatter.isEmpty()) break;
@ -114,10 +119,8 @@ public final class SimpleRenderer implements Renderer {
* {@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());
private static @NotNull IntStream getScanlineStream(int height, boolean parallel) {
var stream = IntStream.range(0, height);
return parallel ? stream.parallel() : stream;
}

View File

@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
@ -19,9 +19,7 @@ public interface Hittable {
*/
@NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range);
default @NotNull Optional<BoundingBox> getBoundingBox() {
return Optional.empty();
}
@NotNull AABB getBoundingBox();
default @NotNull Hittable translate(@NotNull Vec3 offset) {
return new Translate(this, offset);

View File

@ -1,18 +1,17 @@
package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree;
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.Objects;
public final class Scene extends HittableCollection {
private final @NotNull HittableOctree octree;
private final @NotNull HittableList list;
private final @NotNull HittableCollection objects;
private final @NotNull SkyBox background;
public Scene(@NotNull List<? extends @NotNull Hittable> objects) {
@ -24,20 +23,8 @@ public final class Scene extends HittableCollection {
}
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;
this.objects = new HittableBinaryTree(objects);
this.background = Objects.requireNonNull(background);
}
public Scene(@NotNull Hittable @NotNull... objects) {
@ -54,8 +41,12 @@ public final class Scene extends HittableCollection {
@Override
public void hit(@NotNull Ray ray, @NotNull State state) {
octree.hit(ray, state);
list.hit(ray, state);
objects.hit(ray, state);
}
@Override
public @NotNull AABB getBoundingBox() {
return objects.getBoundingBox();
}
public @NotNull Color getBackgroundColor(@NotNull Ray ray) {

View File

@ -1,15 +1,16 @@
package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
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 {
private final @NotNull AABB bbox;
public Ellipse(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material);
this.bbox = new AABB(origin.minus(u).minus(v), origin.plus(u).plus(v));
}
@Override
@ -18,9 +19,7 @@ public final class Ellipse extends Hittable2D {
}
@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)));
public @NotNull AABB getBoundingBox() {
return bbox;
}
}

View File

@ -46,19 +46,13 @@ public abstract class Hittable2D implements Hittable {
var position = ray.at(t);
var p = position.minus(origin);
if (!isInterior(p)) return Optional.empty();
var alpha = w.times(p.cross(v));
var beta = w.times(u.cross(p));
if (!isInterior(alpha, beta)) 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;
}
protected abstract boolean isInterior(double alpha, double beta);
}

View File

@ -1,16 +1,16 @@
package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
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 {
private final @NotNull AABB bbox;
public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material);
this.bbox = new AABB(origin, origin.plus(u).plus(v));
}
@Override
@ -19,9 +19,7 @@ public final class Parallelogram extends Hittable2D {
}
@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)));
public @NotNull AABB getBoundingBox() {
return bbox;
}
}

View File

@ -1,16 +0,0 @@
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

@ -1,16 +1,16 @@
package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
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 {
private final @NotNull AABB bbox;
public Triangle(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material);
this.bbox = new AABB(origin, origin.plus(u).plus(v));
}
@Override
@ -19,9 +19,7 @@ public final class Triangle extends Hittable2D {
}
@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)));
public @NotNull AABB getBoundingBox() {
return bbox;
}
}

View File

@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.scene.hittable3d;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
@ -36,7 +36,7 @@ public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNul
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
public @NotNull AABB getBoundingBox() {
return boundary().getBoundingBox();
}
}

View File

@ -1,7 +1,7 @@
package eu.jonahbauer.raytracing.scene.hittable3d;
import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
@ -12,21 +12,28 @@ import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
public record Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) implements Hittable {
public final class Sphere implements Hittable {
private final @NotNull Vec3 center;
private final double radius;
private final @NotNull Material material;
public Sphere {
Objects.requireNonNull(center, "center");
Objects.requireNonNull(material, "material");
private final @NotNull AABB bbox;
public Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) {
this.center = Objects.requireNonNull(center, "center");
this.material = Objects.requireNonNull(material, "material");
if (radius <= 0 || !Double.isFinite(radius)) throw new IllegalArgumentException("radius must be positive");
}
this.radius = radius;
public Sphere(double x, double y, double z, double r, @NotNull Material material) {
this(new Vec3(x, y, z), r, material);
this.bbox = new AABB(
center.minus(radius, radius, radius),
center.plus(radius, radius, radius)
);
}
@Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
var oc = ray.origin().minus(center());
var oc = ray.origin().minus(center);
var a = ray.direction().squared();
var h = ray.direction().times(oc);
@ -48,22 +55,7 @@ public record Sphere(@NotNull Vec3 center, double radius, @NotNull Material mate
}
@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) {
return new Sphere(center, radius, material);
}
public @NotNull Sphere withCenter(double x, double y, double z) {
return withCenter(new Vec3(x, y, z));
}
public @NotNull Sphere withRadius(double radius) {
return new Sphere(center, radius, material);
public @NotNull AABB getBoundingBox() {
return bbox;
}
}

View File

@ -1,26 +1,25 @@
package eu.jonahbauer.raytracing.scene.transform;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
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;
private final @NotNull AABB 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 bbox = object.getBoundingBox();
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);
@ -42,8 +41,7 @@ public final class RotateY extends Transform {
}
}
return new BoundingBox(min, max);
});
this.bbox = new AABB(min, max);
}
@Override
@ -85,7 +83,7 @@ public final class RotateY extends Transform {
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
public @NotNull AABB getBoundingBox() {
return bbox;
}
}

View File

@ -1,26 +1,25 @@
package eu.jonahbauer.raytracing.scene.transform;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
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;
private final @NotNull AABB bbox;
public Translate(@NotNull Hittable object, @NotNull Vec3 offset) {
super(object);
this.offset = offset;
this.bbox = object.getBoundingBox().map(bbox -> new BoundingBox(
var bbox = object.getBoundingBox();
this.bbox = new AABB(
bbox.min().plus(offset),
bbox.max().plus(offset)
));
);
}
@Override
@ -40,7 +39,7 @@ public final class Translate extends Transform {
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
public @NotNull AABB getBoundingBox() {
return bbox;
}
}

View File

@ -0,0 +1,67 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Comparator;
import java.util.List;
public final class HittableBinaryTree extends HittableCollection {
private final @Nullable Hittable left;
private final @Nullable Hittable right;
private final @NotNull AABB bbox;
public HittableBinaryTree(@NotNull List<? extends @NotNull Hittable> objects) {
bbox = AABB.getBoundingBox(objects).orElse(AABB.EMPTY);
if (objects.isEmpty()) {
left = null;
right = null;
} else if (objects.size() == 1) {
left = objects.getFirst();
right = null;
} else if (objects.size() == 2) {
left = objects.getFirst();
right = objects.getLast();
} else {
var x = bbox.x().size();
var y = bbox.y().size();
var z = bbox.z().size();
Comparator<AABB> comparator;
if (x > y && x > z) {
comparator = AABB.X_AXIS;
} else if (y > z) {
comparator = AABB.Y_AXIS;
} else {
comparator = AABB.Z_AXIS;
}
var sorted = objects.stream().sorted(Comparator.comparing(Hittable::getBoundingBox, comparator)).toList();
var size = sorted.size();
left = new HittableBinaryTree(sorted.subList(0, size / 2));
right = new HittableBinaryTree(sorted.subList(size / 2, size));
}
}
@Override
public void hit(@NotNull Ray ray, @NotNull State state) {
if (!bbox.hit(ray)) return;
if (left instanceof HittableCollection coll) {
coll.hit(ray, state);
} else if (left != null) {
hit(state, ray, left);
}
if (right instanceof HittableCollection coll) {
coll.hit(ray, state);
} else if (right != null) {
hit(state, ray, right);
}
}
@Override
public @NotNull AABB getBoundingBox() {
return bbox;
}
}

View File

@ -1,14 +1,11 @@
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;
@ -23,20 +20,6 @@ public abstract class HittableCollection implements Hittable {
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()) {

View File

@ -1,20 +1,19 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.AABB;
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;
private final @NotNull AABB bbox;
public HittableList(@NotNull List<? extends @NotNull Hittable> objects) {
this.objects = List.copyOf(objects);
this.bbox = getBoundingBox(this.objects);
this.bbox = AABB.getBoundingBox(this.objects).orElse(AABB.EMPTY);
}
public HittableList(@NotNull Hittable @NotNull... objects) {
@ -27,7 +26,7 @@ public final class HittableList extends HittableCollection {
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
public @NotNull AABB getBoundingBox() {
return bbox;
}
}

View File

@ -1,61 +1,253 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.*;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.IntStream;
public final class HittableOctree extends HittableCollection {
private final @NotNull Octree<Hittable> objects;
private final @NotNull Optional<BoundingBox> bbox;
private static final int LIST_SIZE_LIMIT = 16;
private final @Nullable Storage storage;
private final @NotNull AABB bbox;
public HittableOctree(@NotNull List<? extends @NotNull Hittable> objects) {
var result = newOctree(objects);
this.objects = result.getKey();
this.bbox = Optional.of(result.getValue());
bbox = AABB.getBoundingBox(objects).orElse(AABB.EMPTY);
storage = newStorage(bbox, objects);
}
public HittableOctree(@NotNull Hittable @NotNull... objects) {
this(List.of(objects));
private static @NotNull AABB[] getBoundingBoxes(@NotNull AABB aabb, @NotNull Vec3 center) {
return new AABB[] {
new AABB(new Range(aabb.x().min(), center.x()), new Range(aabb.y().min(), center.y()), new Range(aabb.z().min(), center.z())),
new AABB(new Range(center.x(), aabb.x().max()), new Range(aabb.y().min(), center.y()), new Range(aabb.z().min(), center.z())),
new AABB(new Range(aabb.x().min(), center.x()), new Range(center.y(), aabb.y().max()), new Range(aabb.z().min(), center.z())),
new AABB(new Range(center.x(), aabb.x().max()), new Range(center.y(), aabb.y().max()), new Range(aabb.z().min(), center.z())),
new AABB(new Range(aabb.x().min(), center.x()), new Range(aabb.y().min(), center.y()), new Range(center.z(), aabb.z().max())),
new AABB(new Range(center.x(), aabb.x().max()), new Range(aabb.y().min(), center.y()), new Range(center.z(), aabb.z().max())),
new AABB(new Range(aabb.x().min(), center.x()), new Range(center.y(), aabb.y().max()), new Range(center.z(), aabb.z().max())),
new AABB(new Range(center.x(), aabb.x().max()), new Range(center.y(), aabb.y().max()), new Range(center.z(), aabb.z().max())),
};
}
private static @Nullable Storage newStorage(@NotNull AABB aabb, @NotNull List<? extends @NotNull Hittable> objects) {
if (objects.isEmpty()) return null;
if (objects.size() < LIST_SIZE_LIMIT) {
return new ListStorage(aabb, objects);
} else {
var center = aabb.center();
var octants = (List<Hittable>[]) new List<?>[8];
for (int i = 0; i < 8; i++) octants[i] = new ArrayList<>();
var bboxes = getBoundingBoxes(aabb, center);
var list = new ArrayList<Hittable>();
for (var object : objects) {
var bbox = object.getBoundingBox();
var imin = getOctantIndex(center, bbox.min());
var imax = getOctantIndex(center, bbox.max());
if (imin == imax) {
octants[imin].add(object);
} else {
list.add(object);
}
}
return new NodeStorage(aabb, center, list, IntStream.range(0, 8).mapToObj(i -> newStorage(bboxes[i], octants[i])).toArray(Storage[]::new));
}
}
@Override
public void hit(@NotNull Ray ray, @NotNull State state) {
objects.hit(ray, object -> hit(state, ray, object));
hit(ray, object -> hit(state, ray, object));
}
@Override
public @NotNull Optional<BoundingBox> getBoundingBox() {
public @NotNull AABB 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());
/**
* 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>
*/
private void hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> action) {
if (storage != null) storage.hit(ray, action);
}
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);
private 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);
var out = new Octree<Hittable>(center, dimension);
objects.forEach(object -> out.add(object.getBoundingBox().get(), object));
return Map.entry(out, new BoundingBox(min, max));
}
private abstract static sealed class Storage {
protected final @NotNull AABB bbox;
public Storage(@NotNull AABB bbox) {
this.bbox = Objects.requireNonNull(bbox);
}
protected boolean hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> action) {
var range = bbox.intersect(ray);
if (range.isEmpty()) return false;
int vmask = ray.vmask();
return hit0(ray, vmask, range.get().min(), range.get().max(), action);
}
protected abstract boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<? super Hittable> action);
}
private static final class ListStorage extends Storage {
private final @NotNull List<Hittable> list;
public ListStorage(@NotNull AABB bbox, @NotNull List<? extends @NotNull Hittable> entries) {
super(bbox);
this.list = List.copyOf(entries);
}
@Override
protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<? super Hittable> action) {
var hit = false;
for (Hittable hittable : list) {
hit |= action.test(hittable);
}
return hit;
}
}
private static final class NodeStorage extends Storage {
private final @Nullable Storage @NotNull[] octants;
private final @NotNull Vec3 center;
private final int degenerate;
private final @NotNull List<Hittable> list; // track elements spanning multiple octants separately
public NodeStorage(@NotNull AABB bbox, @NotNull Vec3 center, @NotNull List<? extends @NotNull Hittable> list, @Nullable Storage @NotNull[] octants) {
super(bbox);
this.octants = octants;
this.center = center;
this.list = List.copyOf(list);
int count = 0;
int degenerate = 0;
for (int i = 0; i < octants.length; i++) {
if (octants[i] != null) {
count++;
degenerate = i;
}
}
this.degenerate = count == 1 ? degenerate : -1;
}
@Override
protected boolean hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> action) {
if (degenerate >= 0 && list.isEmpty()) {
return octants[degenerate].hit(ray, action);
} else {
return super.hit(ray, action);
}
}
@Override
protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<? super Hittable> action) {
if (tmax < 0) return false;
// check for hit
var hit = false;
// process entries spanning multiple children
for (Hittable object : list) {
hit |= action.test(object);
}
// t values for intersection points of ray with planes through center
var tmid = AABB.intersect(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 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}
}
}
}
}
}