Compare commits

...

7 Commits

39 changed files with 872 additions and 726 deletions

View File

@ -31,4 +31,12 @@ java -jar raytracing.jar --samples 50000 --height 1200 CORNELL
``` ```
java -jar raytracing.jar --samples 50000 --height 600 CORNELL_SMOKE 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; 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.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.Canvas;
import eu.jonahbauer.raytracing.render.canvas.Image; import eu.jonahbauer.raytracing.render.canvas.Image;
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas; import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
import eu.jonahbauer.raytracing.render.material.*;
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer; 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 org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Random;
import java.util.function.IntFunction; import java.util.function.IntFunction;
public class Main { public class Main {
@ -37,6 +23,7 @@ public class Main {
.withSamplesPerPixel(config.samples) .withSamplesPerPixel(config.samples)
.withMaxDepth(config.depth) .withMaxDepth(config.depth)
.withIterative(config.iterative) .withIterative(config.iterative)
.withParallel(config.parallel)
.build(); .build();
Canvas canvas; Canvas canvas;
@ -55,13 +42,13 @@ public class Main {
ImageFormat.PNG.write(canvas, config.path); 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) { public static @NotNull Config parse(@NotNull String @NotNull[] args) {
IntFunction<Example> example = null; IntFunction<Example> example = null;
Path path = null; Path path = null;
boolean preview = true; boolean preview = true;
boolean iterative = false; boolean iterative = false;
boolean parallel = false;
int samples = 1000; int samples = 1000;
int depth = 50; int depth = 50;
int height = -1; int height = -1;
@ -80,6 +67,8 @@ public class Main {
case "--no-preview" -> preview = false; case "--no-preview" -> preview = false;
case "--iterative" -> iterative = true; case "--iterative" -> iterative = true;
case "--no-iterative" -> iterative = false; case "--no-iterative" -> iterative = false;
case "--parallel" -> parallel = true;
case "--no-parallel" -> parallel = false;
case "--samples" -> { case "--samples" -> {
if (i + 1 == args.length) throw fail("missing value for parameter --samples"); if (i + 1 == args.length) throw fail("missing value for parameter --samples");
try { try {
@ -107,22 +96,20 @@ public class Main {
throw fail("value " + args[i] + " is not a valid integer"); throw fail("value " + args[i] + " is not a valid integer");
} }
} }
case String str when !str.startsWith("-") -> example = switch (str) { case String str when !str.startsWith("-") -> {
case "SIMPLE" -> Examples::getSimpleScene; try {
case "SPHERES" -> Examples::getSpheres; example = Examples.getByName(str);
case "SQUARES" -> Examples::getSquares; } catch (IllegalArgumentException ex) {
case "LIGHT" -> Examples::getLight; throw fail(ex.getMessage());
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]); default -> throw fail("unknown option " + args[i]);
} }
} }
if (example == null) example = Examples::getCornellBoxSmoke; if (example == null) example = Examples::getCornellBoxSmoke;
if (path == null) path = Path.of("scene-" + System.currentTimeMillis() + ".png"); 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) { private static @NotNull RuntimeException fail(@NotNull String message) {
@ -130,178 +117,5 @@ public class Main {
System.exit(1); System.exit(1);
return new RuntimeException(); 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) { public boolean surrounds(double value) {
return min < value && value < max; 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() 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 org.jetbrains.annotations.NotNull;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public record Vec3(double x, double y, double z) { public record Vec3(double x, double y, double z) {
public static final Vec3 ZERO = new Vec3(0, 0, 0); public static final Vec3 ZERO = new Vec3(0, 0, 0);
@ -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"; assert Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z) : "x, y and z must be finite";
} }
public static @NotNull Vec3 random() { public static @NotNull Vec3 random(@NotNull RandomGenerator random) {
return random(false); return random(random, false);
} }
public static @NotNull Vec3 random(boolean unit) { public static @NotNull Vec3 random(@NotNull RandomGenerator random, boolean unit) {
var random = new Vec3( var vec = new Vec3(
2 * Math.random() - 1, 2 * random.nextDouble() - 1,
2 * Math.random() - 1, 2 * random.nextDouble() - 1,
2 * Math.random() - 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) { 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); 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) { public @NotNull Vec3 cross(@NotNull Vec3 b) {
return new Vec3( return new Vec3(
this.y() * b.z() - b.y() * this.z(), this.y() * b.z() - b.y() * this.z(),

View File

@ -1,6 +1,7 @@
package eu.jonahbauer.raytracing.render; package eu.jonahbauer.raytracing.render;
import eu.jonahbauer.raytracing.render.canvas.Canvas; import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.canvas.Image;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.*; import java.io.*;
@ -10,6 +11,7 @@ import java.nio.file.Path;
import java.util.zip.CRC32; import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream; import java.util.zip.CheckedOutputStream;
import java.util.zip.DeflaterOutputStream; import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public enum ImageFormat { public enum ImageFormat {
PPM { PPM {
@ -41,6 +43,7 @@ public enum ImageFormat {
private static final int IHDR_TYPE = 0x49484452; private static final int IHDR_TYPE = 0x49484452;
private static final int IDAT_TYPE = 0x49444154; private static final int IDAT_TYPE = 0x49444154;
private static final int IEND_TYPE = 0x49454E44; private static final int IEND_TYPE = 0x49454E44;
private static final int IEND_CRC = 0xAE426082;
@Override @Override
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException { public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
@ -92,7 +95,7 @@ public enum ImageFormat {
} }
var bytes = baos.toByteArray(); var bytes = baos.toByteArray();
data.writeInt(bytes.length); data.writeInt(bytes.length - 4); // don't include type in length
data.write(bytes); data.write(bytes);
data.writeInt((int) crc.getChecksum().getValue()); data.writeInt((int) crc.getChecksum().getValue());
} }
@ -101,7 +104,7 @@ public enum ImageFormat {
private void writeIEND(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException { private void writeIEND(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
data.writeInt(0); data.writeInt(0);
data.writeInt(IEND_TYPE); data.writeInt(IEND_TYPE);
data.writeInt(0); data.writeInt(IEND_CRC);
} }
private static class NoCloseDataOutputStream extends DataOutputStream { 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 eu.jonahbauer.raytracing.math.Ray;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.random.RandomGenerator;
public interface Camera { public interface Camera {
/** /**
* {@return the width of this camera in pixels} * {@return the width of this camera in pixels}
@ -18,5 +20,5 @@ public interface Camera {
* Casts a ray through the given pixel. * Casts a ray through the given pixel.
* @return a new ray * @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 org.jetbrains.annotations.Nullable;
import java.util.Objects; import java.util.Objects;
import java.util.random.RandomGenerator;
public final class SimpleCamera implements Camera { public final class SimpleCamera implements Camera {
// image size // image size
@ -79,12 +80,12 @@ public final class SimpleCamera implements Camera {
/** /**
* {@inheritDoc} * {@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(x, width);
Objects.checkIndex(y, height); Objects.checkIndex(y, height);
var origin = getRayOrigin(); var origin = getRayOrigin(random);
var target = getRayTarget(x, y); var target = getRayTarget(x, y, random);
return new Ray(origin, target.minus(origin)); 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 * radius {@link #blurRadius} centered on the camera position and perpendicular to the direction to simulate depth
* of field. * of field.
*/ */
private @NotNull Vec3 getRayOrigin() { private @NotNull Vec3 getRayOrigin(@NotNull RandomGenerator random) {
if (blurRadius <= 0) return origin; if (blurRadius <= 0) return origin;
while (true) { while (true) {
var du = 2 * Math.random() - 1; var du = 2 * random.nextDouble() - 1;
var dv = 2 * Math.random() - 1; var dv = 2 * random.nextDouble() - 1;
if (du * du + dv * dv >= 1) continue; if (du * du + dv * dv >= 1) continue;
var ru = blurRadius * du; 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. * {@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) { private @NotNull Vec3 getRayTarget(int x, int y, @NotNull RandomGenerator random) {
double dx = x + Math.random() - 0.5; double dx = x + random.nextDouble() - 0.5;
double dy = y + Math.random() - 0.5; double dy = y + random.nextDouble() - 0.5;
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy)); 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 eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional; 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 @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 ri = hit.frontFace() ? (1 / refractionIndex) : refractionIndex;
var cosTheta = Math.min(- ray.direction().unit().times(hit.normal()), 1.0); var cosTheta = Math.min(- ray.direction().unit().times(hit.normal()), 1.0);
var reflectance = reflectance(cosTheta); 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)) var newDirection = (reflect ? Optional.<Vec3>empty() : Vec3.refract(ray.direction(), hit.normal(), ri))
.orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal())); .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) { private double reflectance(double cos) {

View File

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

View File

@ -7,10 +7,11 @@ import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public record IsotropicMaterial(@NotNull Color albedo) implements Material{ public record IsotropicMaterial(@NotNull Color albedo) implements Material{
@Override @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.of(new ScatterResult(new Ray(hit.position(), Vec3.random(true)), albedo())); 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.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public record LambertianMaterial(@NotNull Color albedo) implements Material { public record LambertianMaterial(@NotNull Color albedo) implements Material {
public LambertianMaterial { public LambertianMaterial {
@ -15,8 +16,8 @@ public record LambertianMaterial(@NotNull Color albedo) implements Material {
} }
@Override @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 = hit.normal().plus(Vec3.random(true)); var newDirection = hit.normal().plus(Vec3.random(random, true));
if (newDirection.isNearZero()) newDirection = hit.normal(); if (newDirection.isNearZero()) newDirection = hit.normal();
var scattered = new Ray(hit.position(), newDirection); 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.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public interface Material { 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) { default @NotNull Color emitted(@NotNull HitResult hit) {
return Color.BLACK; return Color.BLACK;

View File

@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Material { 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 @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()); var newDirection = Vec3.reflect(ray.direction(), hit.normal());
if (fuzz > 0) { 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)); 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 eu.jonahbauer.raytracing.scene.Scene;
import org.jetbrains.annotations.NotNull; 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.IntStream;
import java.util.stream.LongStream;
public final class SimpleRenderer implements Renderer { public final class SimpleRenderer implements Renderer {
private final int samplesPerPixel; private final int samplesPerPixel;
@ -44,36 +45,40 @@ public final class SimpleRenderer implements Renderer {
} }
if (iterative) { if (iterative) {
var random = new Random();
// render one sample after the other // render one sample after the other
for (int i = 1 ; i <= samplesPerPixel; i++) { for (int i = 1 ; i <= samplesPerPixel; i++) {
var sample = i; var sample = i;
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
var y = (int) (pixel >> 32); for (int x = 0; x < camera.getWidth(); x++) {
var x = (int) pixel; var ray = camera.cast(x, y, random);
var ray = camera.cast(x, y); var c = getColor(scene, ray, random);
var c = getColor(scene, ray); canvas.set(x, y, Color.average(canvas.get(x, y), c, sample));
canvas.set(x, y, Color.average(canvas.get(x, y), c, sample)); }
}); });
} }
// apply gamma correction // apply gamma correction
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
var y = (int) (pixel >> 32); for (int x = 0; x < camera.getWidth(); x++) {
var x = (int) pixel; canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma));
canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma)); }
}); });
} else { } else {
var splittable = new SplittableRandom();
// render one pixel after the other // render one pixel after the other
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
var y = (int) (pixel >> 32); var random = splittable.split();
var x = (int) pixel; for (int x = 0; x < camera.getWidth(); x++) {
var color = Color.BLACK;
var color = Color.BLACK; for (int i = 1; i <= samplesPerPixel; i++) {
for (int i = 1; i <= samplesPerPixel; i++) { var ray = camera.cast(x, y, random);
var ray = camera.cast(x, y); var c = getColor(scene, ray, random);
var c = getColor(scene, ray); color = Color.average(color, c, i);
color = Color.average(color, c, i); }
canvas.set(x, y, Color.gamma(color, gamma));
} }
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} * {@return the color of the given ray in the given scene}
*/ */
private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray) { private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray, @NotNull RandomGenerator random) {
return getColor0(scene, ray, maxDepth); 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 color = Color.BLACK;
var attenuation = Color.WHITE; var attenuation = Color.WHITE;
@ -99,7 +104,7 @@ public final class SimpleRenderer implements Renderer {
var hit = optional.get(); var hit = optional.get();
var material = hit.material(); var material = hit.material();
var emitted = material.emitted(hit); 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)); color = Color.add(color, Color.multiply(attenuation, emitted));
if (scatter.isEmpty()) break; 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 * {@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. * are encoded in the longs lower and upper 32 bits respectively.
*/ */
private static @NotNull LongStream getPixelStream(int width, int height, boolean parallel) { private static @NotNull IntStream getScanlineStream(int height, boolean parallel) {
var stream = IntStream.range(0, height) var stream = IntStream.range(0, height);
.mapToObj(y -> IntStream.range(0, width).mapToLong(x -> (long) y << 32 | x))
.flatMapToLong(Function.identity());
return parallel ? stream.parallel() : stream; return parallel ? stream.parallel() : stream;
} }

View File

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

View File

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

View File

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

View File

@ -46,19 +46,13 @@ public abstract class Hittable2D implements Hittable {
var position = ray.at(t); var position = ray.at(t);
var p = position.minus(origin); 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; var frontFace = denominator < 0;
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace)); return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace));
} }
protected boolean isInterior(@NotNull Vec3 p) { protected abstract boolean isInterior(double alpha, double beta);
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

@ -1,16 +1,16 @@
package eu.jonahbauer.raytracing.scene.hittable2d; 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.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Parallelogram extends Hittable2D { 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) { public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material); super(origin, u, v, material);
this.bbox = new AABB(origin, origin.plus(u).plus(v));
} }
@Override @Override
@ -19,9 +19,7 @@ public final class Parallelogram extends Hittable2D {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
var a = origin; return bbox;
var b = origin.plus(u).plus(v);
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
} }
} }

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; 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.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.material.Material;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Triangle extends Hittable2D { 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) { public Triangle(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
super(origin, u, v, material); super(origin, u, v, material);
this.bbox = new AABB(origin, origin.plus(u).plus(v));
} }
@Override @Override
@ -19,9 +19,7 @@ public final class Triangle extends Hittable2D {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
var a = origin; return bbox;
var b = origin.plus(u).plus(v);
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
} }
} }

View File

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

View File

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

View File

@ -1,49 +1,47 @@
package eu.jonahbauer.raytracing.scene.transform; 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.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class RotateY extends Transform { public final class RotateY extends Transform {
private final double cos; private final double cos;
private final double sin; private final double sin;
private final @NotNull Optional<BoundingBox> bbox; private final @NotNull AABB bbox;
public RotateY(@NotNull Hittable object, double angle) { public RotateY(@NotNull Hittable object, double angle) {
super(object); super(object);
this.cos = Math.cos(angle); this.cos = Math.cos(angle);
this.sin = Math.sin(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);
for (int i = 0; i < 2; i++) { var min = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
for (int j = 0; j < 2; j++) { var max = new Vec3(- Double.MAX_VALUE, - Double.MAX_VALUE, - Double.MAX_VALUE);
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; for (int i = 0; i < 2; i++) {
var newz = -sin * x + cos * z; 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 temp = new Vec3(newx, y, newz); var newx = cos * x + sin * z;
var newz = -sin * x + cos * z;
min = Vec3.min(min, temp); var temp = new Vec3(newx, y, newz);
max = Vec3.max(max, temp);
} min = Vec3.min(min, temp);
max = Vec3.max(max, temp);
} }
} }
}
return new BoundingBox(min, max); this.bbox = new AABB(min, max);
});
} }
@Override @Override
@ -85,7 +83,7 @@ public final class RotateY extends Transform {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; return bbox;
} }
} }

View File

@ -1,26 +1,25 @@
package eu.jonahbauer.raytracing.scene.transform; 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.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public final class Translate extends Transform { public final class Translate extends Transform {
private final @NotNull Vec3 offset; private final @NotNull Vec3 offset;
private final @NotNull AABB bbox;
private final @NotNull Optional<BoundingBox> bbox;
public Translate(@NotNull Hittable object, @NotNull Vec3 offset) { public Translate(@NotNull Hittable object, @NotNull Vec3 offset) {
super(object); super(object);
this.offset = offset; this.offset = offset;
this.bbox = object.getBoundingBox().map(bbox -> new BoundingBox(
var bbox = object.getBoundingBox();
this.bbox = new AABB(
bbox.min().plus(offset), bbox.min().plus(offset),
bbox.max().plus(offset) bbox.max().plus(offset)
)); );
} }
@Override @Override
@ -40,7 +39,7 @@ public final class Translate extends Transform {
} }
@Override @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; 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; package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.BoundingBox;
import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -23,20 +20,6 @@ public abstract class HittableCollection implements Hittable {
public abstract void hit(@NotNull Ray ray, @NotNull State state); 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) { protected static boolean hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object) {
var r = object.hit(ray, state.range); var r = object.hit(ray, state.range);
if (r.isPresent()) { if (r.isPresent()) {

View File

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

View File

@ -1,61 +1,253 @@
package eu.jonahbauer.raytracing.scene.util; 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 eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays; import java.util.*;
import java.util.List; import java.util.function.Predicate;
import java.util.Map; import java.util.stream.IntStream;
import java.util.Map.Entry;
import java.util.Optional;
public final class HittableOctree extends HittableCollection { public final class HittableOctree extends HittableCollection {
private final @NotNull Octree<Hittable> objects; private static final int LIST_SIZE_LIMIT = 16;
private final @NotNull Optional<BoundingBox> bbox;
private final @Nullable Storage storage;
private final @NotNull AABB bbox;
public HittableOctree(@NotNull List<? extends @NotNull Hittable> objects) { public HittableOctree(@NotNull List<? extends @NotNull Hittable> objects) {
var result = newOctree(objects); bbox = AABB.getBoundingBox(objects).orElse(AABB.EMPTY);
this.objects = result.getKey(); storage = newStorage(bbox, objects);
this.bbox = Optional.of(result.getValue());
} }
public HittableOctree(@NotNull Hittable @NotNull... objects) { private static @NotNull AABB[] getBoundingBoxes(@NotNull AABB aabb, @NotNull Vec3 center) {
this(List.of(objects)); 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 @Override
public void hit(@NotNull Ray ray, @NotNull State state) { 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 @Override
public @NotNull Optional<BoundingBox> getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; 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; * 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);
}
int i = 1; private static int getOctantIndex(@NotNull Vec3 center, @NotNull Vec3 pos) {
for (var object : objects) { return (pos.x() < center.x() ? 0 : 1)
var bbox = object.getBoundingBox().orElseThrow(); | (pos.y() < center.y() ? 0 : 2)
center = Vec3.average(center, bbox.center(), i++); | (pos.z() < center.z() ? 0 : 4);
max = Vec3.max(max, bbox.max());
min = Vec3.min(min, bbox.min()); }
private abstract static sealed class Storage {
protected final @NotNull AABB bbox;
public Storage(@NotNull AABB bbox) {
this.bbox = Objects.requireNonNull(bbox);
} }
var dimension = Arrays.stream(new double[] { protected boolean hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> action) {
Math.abs(max.x() - center.x()), var range = bbox.intersect(ray);
Math.abs(max.y() - center.y()), if (range.isEmpty()) return false;
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); int vmask = ray.vmask();
objects.forEach(object -> out.add(object.getBoundingBox().get(), object)); return hit0(ray, vmask, range.get().min(), range.get().max(), action);
return Map.entry(out, new BoundingBox(min, max)); }
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}
}
}
}
} }
} }