Compare commits
7 Commits
c6c43ad895
...
4ea9ebe25a
Author | SHA1 | Date | |
---|---|---|---|
4ea9ebe25a | |||
1fe5731dcf | |||
5bf3108ab4 | |||
8773b04b0f | |||
b96c6db440 | |||
4dfeb133de | |||
579a08bbb3 |
@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
java -jar raytracing.jar --samples 1000 --height 1080 DIAGRAMM
|
||||||
```
|
```
|
Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 441 KiB |
BIN
docs/diagramm.png
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
docs/simple.png
Before Width: | Height: | Size: 881 KiB After Width: | Height: | Size: 881 KiB |
BIN
docs/squares.png
Before Width: | Height: | Size: 620 KiB After Width: | Height: | Size: 620 KiB |
8
src/main/java/eu/jonahbauer/raytracing/Example.java
Normal 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) {
|
||||||
|
}
|
257
src/main/java/eu/jonahbauer/raytracing/Examples.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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) {}
|
|
||||||
}
|
}
|
107
src/main/java/eu/jonahbauer/raytracing/math/AABB.java
Normal 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) { }
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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()) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|