Compare commits
14 Commits
d67f877428
...
e18b2ba7e5
Author | SHA1 | Date | |
---|---|---|---|
e18b2ba7e5 | |||
e4e241a314 | |||
360fb2c990 | |||
c002b8215a | |||
580d8eca12 | |||
27e3fc0990 | |||
ebbf711403 | |||
3be855cffd | |||
36de714f46 | |||
8ea894cd3e | |||
b5acbd1b6c | |||
86b6f1891c | |||
7a5526f987 | |||
137c0b2190 |
28
README.md
Normal file
28
README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# raytracing
|
||||
|
||||
Based on the series <a href="https://raytracing.github.io"><cite>Ray Tracing in One Weekend</cite></a>.
|
||||
|
||||
## Scenes
|
||||
|
||||
### squares
|
||||

|
||||
|
||||
```
|
||||
java -jar raytracing.jar --samples 500 --height 1200 SQUARES
|
||||
```
|
||||
|
||||
### cornell box
|
||||
|
||||

|
||||
|
||||
```
|
||||
java -jar raytracing.jar --samples 50000 --height 1200 CORNELL
|
||||
```
|
||||
|
||||
### cornell box with smoke
|
||||
|
||||

|
||||
|
||||
```
|
||||
java -jar raytracing.jar --samples 50000 --height 600 CORNELL_SMOKE
|
||||
```
|
@ -1,5 +1,16 @@
|
||||
plugins {
|
||||
id("java")
|
||||
id("application")
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(22)
|
||||
}
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = "eu.jonahbauer.raytracing.Main"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
BIN
docs/cornell_smoke.png
Normal file
BIN
docs/cornell_smoke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 441 KiB |
BIN
docs/squares.png
Normal file
BIN
docs/squares.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 620 KiB |
@ -1,97 +1,307 @@
|
||||
package eu.jonahbauer.raytracing;
|
||||
|
||||
import eu.jonahbauer.raytracing.material.DielectricMaterial;
|
||||
import eu.jonahbauer.raytracing.material.LambertianMaterial;
|
||||
import eu.jonahbauer.raytracing.material.Material;
|
||||
import eu.jonahbauer.raytracing.material.MetallicMaterial;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.ImageFormat;
|
||||
import eu.jonahbauer.raytracing.render.camera.Camera;
|
||||
import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
|
||||
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
|
||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||
import eu.jonahbauer.raytracing.render.canvas.Image;
|
||||
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
|
||||
import eu.jonahbauer.raytracing.render.material.*;
|
||||
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import eu.jonahbauer.raytracing.scene.Scene;
|
||||
import eu.jonahbauer.raytracing.scene.Sphere;
|
||||
import eu.jonahbauer.raytracing.scene.SkyBox;
|
||||
import eu.jonahbauer.raytracing.scene.hittable2d.Parallelogram;
|
||||
import eu.jonahbauer.raytracing.scene.hittable3d.ConstantMedium;
|
||||
import eu.jonahbauer.raytracing.scene.hittable3d.Sphere;
|
||||
import eu.jonahbauer.raytracing.scene.util.Hittables;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) throws IOException {
|
||||
var scene = getScene();
|
||||
|
||||
var camera = SimpleCamera.builder()
|
||||
.withImage(1200, 675)
|
||||
.withPosition(new Vec3(13, 2, 3))
|
||||
.withTarget(new Vec3(0, 0, 0))
|
||||
.withFieldOfView(Math.toRadians(20))
|
||||
.withFocusDistance(10.0)
|
||||
.withBlurAngle(Math.toRadians(0.6))
|
||||
.build();
|
||||
var config = Config.parse(args);
|
||||
var example = config.example;
|
||||
var scene = example.scene();
|
||||
var camera = example.camera();
|
||||
|
||||
var renderer = SimpleRenderer.builder()
|
||||
.withSamplesPerPixel(500)
|
||||
.withMaxDepth(50)
|
||||
.withIterative(true)
|
||||
.withSamplesPerPixel(config.samples)
|
||||
.withMaxDepth(config.depth)
|
||||
.withIterative(config.iterative)
|
||||
.build();
|
||||
|
||||
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
|
||||
image.preview();
|
||||
|
||||
renderer.render(camera, scene, image);
|
||||
ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png"));
|
||||
}
|
||||
|
||||
private static @NotNull Scene getScene() {
|
||||
var rng = new Random(1);
|
||||
var objects = new ArrayList<Hittable>();
|
||||
objects.add(new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.5, 0.5, 0.5))));
|
||||
|
||||
for (int a = -11; a < 11; a++) {
|
||||
for (int b = -11; b < 11; b++) {
|
||||
var center = new Vec3(a + 0.9 * rng.nextDouble(), 0.2, b + 0.9 * rng.nextDouble());
|
||||
if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue;
|
||||
|
||||
Material material;
|
||||
var rnd = rng.nextDouble();
|
||||
if (rnd < 0.8) {
|
||||
// diffuse
|
||||
var albedo = Color.multiply(Color.random(rng), Color.random(rng));
|
||||
material = new LambertianMaterial(albedo);
|
||||
} else if (rnd < 0.95) {
|
||||
// metal
|
||||
var albedo = Color.random(rng, 0.5, 1.0);
|
||||
var fuzz = rng.nextDouble() * 0.5;
|
||||
material = new MetallicMaterial(albedo, fuzz);
|
||||
} else {
|
||||
// glass
|
||||
material = new DielectricMaterial(1.5);
|
||||
}
|
||||
|
||||
objects.add(new Sphere(center, 0.2, material));
|
||||
}
|
||||
Canvas canvas;
|
||||
if (config.preview) {
|
||||
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
|
||||
image.preview();
|
||||
canvas = image;
|
||||
} else {
|
||||
canvas = new Image(camera.getWidth(), camera.getHeight());
|
||||
}
|
||||
|
||||
objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5)));
|
||||
objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1))));
|
||||
objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5))));
|
||||
long time = System.nanoTime();
|
||||
renderer.render(camera, scene, canvas);
|
||||
System.out.printf("rendering finished after %dms", (System.nanoTime() - time) / 1_000_000);
|
||||
|
||||
return new Scene(objects);
|
||||
ImageFormat.PNG.write(canvas, config.path);
|
||||
}
|
||||
|
||||
private static @NotNull Scene getSimpleScene() {
|
||||
return new Scene(List.of(
|
||||
new Sphere(new Vec3(0, -100.5, -1.0), 100.0, new LambertianMaterial(new Color(0.8, 0.8, 0.0))),
|
||||
new Sphere(new Vec3(0, 0, -1.2), 0.5, new LambertianMaterial(new Color(0.1, 0.2, 0.5))),
|
||||
new Sphere(new Vec3(-1.0, 0, -1.2), 0.5, new DielectricMaterial(1.5)),
|
||||
new Sphere(new Vec3(-1.0, 0, -1.2), 0.4, new DielectricMaterial(1 / 1.5)),
|
||||
new Sphere(new Vec3(1.0, 0, -1.2), 0.5, new MetallicMaterial(new Color(0.8, 0.6, 0.2), 0.0))
|
||||
));
|
||||
private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, int samples, int depth) {
|
||||
|
||||
public static @NotNull Config parse(@NotNull String @NotNull[] args) {
|
||||
IntFunction<Example> example = null;
|
||||
Path path = null;
|
||||
boolean preview = true;
|
||||
boolean iterative = false;
|
||||
int samples = 1000;
|
||||
int depth = 50;
|
||||
int height = -1;
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case "--output" -> {
|
||||
if (i + 1 == args.length) throw fail("missing value for parameter --output");
|
||||
try {
|
||||
path = Path.of(args[++i]);
|
||||
} catch (InvalidPathException ex) {
|
||||
throw fail("value " + args[i] + " is not a valid path");
|
||||
}
|
||||
}
|
||||
case "--preview" -> preview = true;
|
||||
case "--no-preview" -> preview = false;
|
||||
case "--iterative" -> iterative = true;
|
||||
case "--no-iterative" -> iterative = false;
|
||||
case "--samples" -> {
|
||||
if (i + 1 == args.length) throw fail("missing value for parameter --samples");
|
||||
try {
|
||||
samples = Integer.parseInt(args[++i]);
|
||||
if (samples <= 0) throw fail("samples must be positive");
|
||||
} catch (NumberFormatException ex) {
|
||||
throw fail("value " + args[i] + " is not a valid integer");
|
||||
}
|
||||
}
|
||||
case "--depth" -> {
|
||||
if (i + 1 == args.length) throw fail("missing value for parameter --depth");
|
||||
try {
|
||||
depth = Integer.parseInt(args[++i]);
|
||||
if (depth <= 0) throw fail("depth must be positive");
|
||||
} catch (NumberFormatException ex) {
|
||||
throw fail("value " + args[i] + " is not a valid integer");
|
||||
}
|
||||
}
|
||||
case "--height" -> {
|
||||
if (i + 1 == args.length) throw fail("missing value for parameter --height");
|
||||
try {
|
||||
height = Integer.parseInt(args[++i]);
|
||||
if (height <= 0) throw fail("height must be positive");
|
||||
} catch (NumberFormatException ex) {
|
||||
throw fail("value " + args[i] + " is not a valid integer");
|
||||
}
|
||||
}
|
||||
case String str when !str.startsWith("-") -> example = switch (str) {
|
||||
case "SIMPLE" -> Examples::getSimpleScene;
|
||||
case "SPHERES" -> Examples::getSpheres;
|
||||
case "SQUARES" -> Examples::getSquares;
|
||||
case "LIGHT" -> Examples::getLight;
|
||||
case "CORNELL" -> Examples::getCornellBox;
|
||||
case "CORNELL_SMOKE" -> Examples::getCornellBoxSmoke;
|
||||
default -> throw fail("unknown example " + str + ", expected one of SIMPLE, SPHERES, SQUARES, LIGHT, CORNELL or CORNELL_SMOKE");
|
||||
};
|
||||
default -> throw fail("unknown option " + args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (example == null) example = Examples::getCornellBoxSmoke;
|
||||
if (path == null) path = Path.of("scene-" + System.currentTimeMillis() + ".png");
|
||||
return new Config(example.apply(height), path, preview, iterative, samples, depth);
|
||||
}
|
||||
|
||||
private static @NotNull RuntimeException fail(@NotNull String message) {
|
||||
System.err.println(message);
|
||||
System.exit(1);
|
||||
return new RuntimeException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Examples {
|
||||
public static @NotNull Example getSimpleScene(int height) {
|
||||
if (height <= 0) height = 675;
|
||||
return new Example(
|
||||
new Scene(
|
||||
getSkyBox(),
|
||||
new Sphere(new Vec3(0, -100.5, -1.0), 100.0, new LambertianMaterial(new Color(0.8, 0.8, 0.0))),
|
||||
new Sphere(new Vec3(0, 0, -1.2), 0.5, new LambertianMaterial(new Color(0.1, 0.2, 0.5))),
|
||||
new Sphere(new Vec3(-1.0, 0, -1.2), 0.5, new DielectricMaterial(1.5)),
|
||||
new Sphere(new Vec3(-1.0, 0, -1.2), 0.4, new DielectricMaterial(1 / 1.5)),
|
||||
new Sphere(new Vec3(1.0, 0, -1.2), 0.5, new MetallicMaterial(new Color(0.8, 0.6, 0.2), 0.0))
|
||||
),
|
||||
SimpleCamera.builder()
|
||||
.withImage(height * 16 / 9, height)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Example getSpheres(int height) {
|
||||
if (height <= 0) height = 675;
|
||||
|
||||
var rng = new Random(1);
|
||||
var objects = new ArrayList<Hittable>();
|
||||
objects.add(new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.5, 0.5, 0.5))));
|
||||
|
||||
for (int a = -11; a < 11; a++) {
|
||||
for (int b = -11; b < 11; b++) {
|
||||
var center = new Vec3(a + 0.9 * rng.nextDouble(), 0.2, b + 0.9 * rng.nextDouble());
|
||||
if (Vec3.distance(center, new Vec3(4, 0.2, 0)) <= 0.9) continue;
|
||||
|
||||
Material material;
|
||||
var rnd = rng.nextDouble();
|
||||
if (rnd < 0.8) {
|
||||
// diffuse
|
||||
var albedo = Color.multiply(Color.random(rng), Color.random(rng));
|
||||
material = new LambertianMaterial(albedo);
|
||||
} else if (rnd < 0.95) {
|
||||
// metal
|
||||
var albedo = Color.random(rng, 0.5, 1.0);
|
||||
var fuzz = rng.nextDouble() * 0.5;
|
||||
material = new MetallicMaterial(albedo, fuzz);
|
||||
} else {
|
||||
// glass
|
||||
material = new DielectricMaterial(1.5);
|
||||
}
|
||||
|
||||
objects.add(new Sphere(center, 0.2, material));
|
||||
}
|
||||
}
|
||||
|
||||
objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5)));
|
||||
objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1))));
|
||||
objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5))));
|
||||
|
||||
var camera = SimpleCamera.builder()
|
||||
.withImage(height * 16 / 9, height)
|
||||
.withPosition(new Vec3(13, 2, 3))
|
||||
.withTarget(new Vec3(0, 0, 0))
|
||||
.withFieldOfView(Math.toRadians(20))
|
||||
.withFocusDistance(10.0)
|
||||
.withBlurAngle(Math.toRadians(0.6))
|
||||
.build();
|
||||
|
||||
return new Example(new Scene(getSkyBox(), objects), camera);
|
||||
}
|
||||
|
||||
public static @NotNull Example getSquares(int height) {
|
||||
if (height <= 0) height = 600;
|
||||
return new Example(
|
||||
new Scene(
|
||||
getSkyBox(),
|
||||
new Parallelogram(new Vec3(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(1.0, 0.2, 0.2))),
|
||||
new Parallelogram(new Vec3(-2, -2, 0), new Vec3(4, 0, 0), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 1.0, 0.2))),
|
||||
new Parallelogram(new Vec3(3, -2, 1), new Vec3(0, 0, 4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 0.2, 1.0))),
|
||||
new Parallelogram(new Vec3(-2, 3, 1), new Vec3(4, 0, 0), new Vec3(0, 0, 4), new LambertianMaterial(new Color(1.0, 0.5, 0.0))),
|
||||
new Parallelogram(new Vec3(-2, -3, 5), new Vec3(4, 0, 0), new Vec3(0, 0, -4), new LambertianMaterial(new Color(0.2, 0.8, 0.8)))
|
||||
),
|
||||
SimpleCamera.builder()
|
||||
.withImage(height, height)
|
||||
.withFieldOfView(Math.toRadians(80))
|
||||
.withPosition(new Vec3(0, 0, 9))
|
||||
.withTarget(new Vec3(0, 0, 0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Example getLight(int height) {
|
||||
if (height <= 0) height = 225;
|
||||
return new Example(
|
||||
new Scene(
|
||||
new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.2, 0.2, 0.2))),
|
||||
new Sphere(new Vec3(0, 2, 0), 2, new LambertianMaterial(new Color(0.2, 0.2, 0.2))),
|
||||
new Parallelogram(new Vec3(3, 1, -2), new Vec3(2, 0, 0), new Vec3(0, 2, 0), new DiffuseLight(new Color(4.0, 4.0, 4.0))),
|
||||
new Sphere(new Vec3(0, 7, 0), 2, new DiffuseLight(new Color(4.0, 4.0, 4.0)))
|
||||
),
|
||||
SimpleCamera.builder()
|
||||
.withImage(height * 16 / 9, height)
|
||||
.withFieldOfView(Math.toRadians(20))
|
||||
.withPosition(new Vec3(26, 3, 6))
|
||||
.withTarget(new Vec3(0, 2, 0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Example getCornellBox(int height) {
|
||||
if (height <= 0) height = 600;
|
||||
|
||||
var red = new LambertianMaterial(new Color(.65, .05, .05));
|
||||
var white = new LambertianMaterial(new Color(.73, .73, .73));
|
||||
var green = new LambertianMaterial(new Color(.12, .45, .15));
|
||||
var light = new DiffuseLight(new Color(15.0, 15.0, 15.0));
|
||||
|
||||
return new Example(
|
||||
new Scene(
|
||||
new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green),
|
||||
new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red),
|
||||
new Parallelogram(new Vec3(343, 554, 332), new Vec3(-130, 0, 0), new Vec3(0, 0, -105), light),
|
||||
new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0 ,0), new Vec3(0, 0, 555), white),
|
||||
new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0 ,0), new Vec3(0, 0, -555), white),
|
||||
new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0 ,0), new Vec3(0, 555, 0), white),
|
||||
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
|
||||
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65))
|
||||
),
|
||||
SimpleCamera.builder()
|
||||
.withImage(height, height)
|
||||
.withFieldOfView(Math.toRadians(40))
|
||||
.withPosition(new Vec3(278, 278, -800))
|
||||
.withTarget(new Vec3(278, 278, 0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Example getCornellBoxSmoke(int height) {
|
||||
if (height <= 0) height = 600;
|
||||
var red = new LambertianMaterial(new Color(.65, .05, .05));
|
||||
var white = new LambertianMaterial(new Color(.73, .73, .73));
|
||||
var green = new LambertianMaterial(new Color(.12, .45, .15));
|
||||
var light = new DiffuseLight(new Color(7.0, 7.0, 7.0));
|
||||
|
||||
return new Example(
|
||||
new Scene(
|
||||
new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green),
|
||||
new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red),
|
||||
new Parallelogram(new Vec3(113, 554, 127), new Vec3(330, 0, 0), new Vec3(0, 0, 305), light),
|
||||
new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0 ,0), new Vec3(0, 0, 555), white),
|
||||
new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0 ,0), new Vec3(0, 0, -555), white),
|
||||
new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0 ,0), new Vec3(0, 555, 0), white),
|
||||
new ConstantMedium(
|
||||
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
|
||||
0.01, new IsotropicMaterial(Color.BLACK)
|
||||
),
|
||||
new ConstantMedium(
|
||||
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)),
|
||||
0.01, new IsotropicMaterial(Color.WHITE)
|
||||
)
|
||||
),
|
||||
SimpleCamera.builder()
|
||||
.withImage(height, height)
|
||||
.withFieldOfView(Math.toRadians(40))
|
||||
.withPosition(new Vec3(278, 278, -800))
|
||||
.withTarget(new Vec3(278, 278, 0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private static @NotNull SkyBox getSkyBox() {
|
||||
return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
private record Example(@NotNull Scene scene, @NotNull Camera camera) {}
|
||||
}
|
@ -3,8 +3,18 @@ package eu.jonahbauer.raytracing.math;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record BoundingBox(@NotNull Vec3 min, @NotNull Vec3 max) {
|
||||
public BoundingBox {
|
||||
var a = min;
|
||||
var b = max;
|
||||
min = Vec3.min(a, b);
|
||||
max = Vec3.max(a, b);
|
||||
}
|
||||
|
||||
public @NotNull Vec3 center() {
|
||||
return Vec3.average(min, max, 2);
|
||||
}
|
||||
|
||||
public @NotNull BoundingBox expand(@NotNull BoundingBox box) {
|
||||
return new BoundingBox(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max));
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class Octree<T> {
|
||||
private final @NotNull NodeStorage<T> storage;
|
||||
@ -20,17 +19,15 @@ public final class Octree<T> {
|
||||
|
||||
/**
|
||||
* Use HERO algorithms to find all elements that could possibly be hit by the given ray.
|
||||
* @see <a href="https://diglib.eg.org/server/api/core/bitstreams/33fe8d58-1101-40ff-878a-79d689a4607d/content">The HERO Algorithm for Ray-Tracing Octrees</a>
|
||||
* @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 Consumer<T> action) {
|
||||
public void hit(@NotNull Ray ray, @NotNull Predicate<T> action) {
|
||||
storage.hit(ray, action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return storage.toString();
|
||||
}
|
||||
|
||||
public static int getOctantIndex(@NotNull Vec3 center, @NotNull Vec3 pos) {
|
||||
return (pos.x() < center.x() ? 0 : 1)
|
||||
| (pos.y() < center.y() ? 0 : 2)
|
||||
@ -38,21 +35,31 @@ public final class Octree<T> {
|
||||
|
||||
}
|
||||
|
||||
private sealed interface Storage<T> {
|
||||
int LIST_SIZE_LIMIT = 32;
|
||||
private static sealed abstract class Storage<T> {
|
||||
protected static final int LIST_SIZE_LIMIT = 32;
|
||||
|
||||
@NotNull Storage<T> add(@NotNull Entry<T> entry);
|
||||
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> implements Storage<T> {
|
||||
private final @NotNull Vec3 center;
|
||||
private final double dimension;
|
||||
|
||||
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) {
|
||||
this.center = Objects.requireNonNull(center);
|
||||
this.dimension = dimension;
|
||||
super(center, dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,22 +76,22 @@ public final class Octree<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return list.toString();
|
||||
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> implements Storage<T> {
|
||||
private final @NotNull Vec3 center;
|
||||
private final double dimension;
|
||||
|
||||
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) {
|
||||
this.center = Objects.requireNonNull(center);
|
||||
this.dimension = dimension;
|
||||
super(center, dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -113,7 +120,8 @@ public final class Octree<T> {
|
||||
return new ListStorage<>(newCenter, newSize);
|
||||
}
|
||||
|
||||
public void hit(@NotNull Ray ray, @NotNull Consumer<T> action) {
|
||||
@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);
|
||||
@ -143,24 +151,32 @@ public final class Octree<T> {
|
||||
}
|
||||
|
||||
var hit = tlmax < tumin;
|
||||
if (!hit) return;
|
||||
if (!hit) return false;
|
||||
|
||||
hit0(ray, vmask, tlmax, tumin, action);
|
||||
return hit0(ray, vmask, tlmax, tumin, action);
|
||||
}
|
||||
|
||||
private void hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Consumer<T> action) {
|
||||
if (tmax < 0) return;
|
||||
@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) {
|
||||
action.accept(entry.object());
|
||||
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 = calculateMastlist(tmid);
|
||||
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);
|
||||
@ -169,29 +185,36 @@ public final class Octree<T> {
|
||||
|
||||
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])];
|
||||
}
|
||||
|
||||
if (child instanceof ListStorage<T> list) {
|
||||
for (Entry<T> entry : list.list) {
|
||||
action.accept(entry.object);
|
||||
}
|
||||
} else if (child instanceof NodeStorage<T> node) {
|
||||
node.hit0(ray, vmask, childTmin, childTmax, action);
|
||||
}
|
||||
// process child
|
||||
var childHit = child != null && child.hit0(ray, vmask, childTmin, childTmax, action);
|
||||
hit |= childHit;
|
||||
|
||||
if (childTmax == tmax) break;
|
||||
// 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) {
|
||||
@ -202,50 +225,33 @@ public final class Octree<T> {
|
||||
};
|
||||
}
|
||||
|
||||
private int @NotNull [] calculateMastlist(double @NotNull[] tmid) {
|
||||
var masklist = new int[3];
|
||||
if (tmid[0] < tmid[1] && tmid[0] < tmid[2]) {
|
||||
masklist[0] = 1;
|
||||
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]) {
|
||||
masklist[1] = 2;
|
||||
masklist[2] = 4;
|
||||
return MASKLISTS[0]; // {1, 2, 4}
|
||||
} else if (tmid[0] < tmid[2]) {
|
||||
return MASKLISTS[1]; // {1, 4, 2}
|
||||
} else {
|
||||
masklist[1] = 4;
|
||||
masklist[2] = 2;
|
||||
}
|
||||
} else if (tmid[1] < tmid[2]) {
|
||||
masklist[0] = 2;
|
||||
if (tmid[0] < tmid[2]) {
|
||||
masklist[1] = 1;
|
||||
masklist[2] = 4;
|
||||
} else {
|
||||
masklist[1] = 4;
|
||||
masklist[2] = 1;
|
||||
return MASKLISTS[2]; // {4, 1, 2}
|
||||
}
|
||||
} else {
|
||||
masklist[0] = 4;
|
||||
if (tmid[0] < tmid[1]) {
|
||||
masklist[1] = 1;
|
||||
masklist[2] = 2;
|
||||
if (tmid[0] < tmid[2]) {
|
||||
return MASKLISTS[3]; // {2, 1, 4}
|
||||
} else if (tmid[1] < tmid[2]) {
|
||||
return MASKLISTS[4]; // {2, 4, 1}
|
||||
} else {
|
||||
masklist[1] = 2;
|
||||
masklist[2] = 1;
|
||||
return MASKLISTS[5]; // {4, 2, 1}
|
||||
}
|
||||
}
|
||||
return masklist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
var out = new StringBuilder("Octree centered on " + center + " with dimension " + dimension + "\n");
|
||||
for (int i = 0; i < 8; i++) {
|
||||
out.append(i == 7 ? "\\- [" : "|- [").append(i).append("]: ");
|
||||
|
||||
var prefix = i == 7 ? " " : "| ";
|
||||
out.append(Objects.toString(octants[i]).lines().map(str -> prefix + str).collect(Collectors.joining("\n")).substring(8));
|
||||
out.append("\n");
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
|
||||
}
|
||||
|
||||
public @NotNull Vec3 at(double t) {
|
||||
if (t < 0) throw new IllegalArgumentException("t must not be negative");
|
||||
return new Vec3(
|
||||
origin().x() + t * direction.x(),
|
||||
origin().y() + t * direction.y(),
|
||||
|
@ -7,7 +7,6 @@ import java.util.Random;
|
||||
public record Color(double r, double g, double b) {
|
||||
public static final @NotNull Color BLACK = new Color(0.0, 0.0, 0.0);
|
||||
public static final @NotNull Color WHITE = new Color(1.0, 1.0, 1.0);
|
||||
public static final @NotNull Color SKY = new Color(0.5, 0.7, 1.0);
|
||||
public static final @NotNull Color RED = new Color(1.0, 0.0, 0.0);
|
||||
public static final @NotNull Color GREEN = new Color(0.0, 1.0, 0.0);
|
||||
public static final @NotNull Color BLUE = new Color(0.0, 0.0, 1.0);
|
||||
@ -26,6 +25,10 @@ public record Color(double r, double g, double b) {
|
||||
return new Color(a.r() * b.r(), a.g() * b.g(), a.b() * b.b());
|
||||
}
|
||||
|
||||
public static @NotNull Color add(@NotNull Color a, @NotNull Color b) {
|
||||
return new Color(a.r() + b.r(), a.g() + b.g(), a.b() + b.b());
|
||||
}
|
||||
|
||||
public static @NotNull Color random(@NotNull Random random) {
|
||||
return new Color(random.nextDouble(), random.nextDouble(), random.nextDouble());
|
||||
}
|
||||
@ -65,25 +68,25 @@ public record Color(double r, double g, double b) {
|
||||
}
|
||||
}
|
||||
|
||||
public Color {
|
||||
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1) {
|
||||
throw new IllegalArgumentException("r, g and b must be in the range 0 to 1");
|
||||
}
|
||||
}
|
||||
public Color {}
|
||||
|
||||
public Color(int red, int green, int blue) {
|
||||
this(red / 255f, green / 255f, blue / 255f);
|
||||
}
|
||||
|
||||
public int red() {
|
||||
return (int) (255.99 * r);
|
||||
return toInt(r);
|
||||
}
|
||||
|
||||
public int green() {
|
||||
return (int) (255.99 * g);
|
||||
return toInt(g);
|
||||
}
|
||||
|
||||
public int blue() {
|
||||
return (int) (255.99 * b);
|
||||
return toInt(b);
|
||||
}
|
||||
|
||||
private static int toInt(double value) {
|
||||
return Math.max(0, Math.min(255, (int) (255.99 * value)));
|
||||
}
|
||||
}
|
||||
|
@ -42,15 +42,19 @@ public final class LiveCanvas implements Canvas {
|
||||
|
||||
public @NotNull Thread preview() {
|
||||
var frame = new JFrame();
|
||||
frame.setSize(getWidth(), getHeight());
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
frame.setContentPane(new JPanel() {
|
||||
{
|
||||
setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
g.drawImage(image, 0, 0, null);
|
||||
}
|
||||
});
|
||||
frame.setResizable(false);
|
||||
frame.pack();
|
||||
frame.setVisible(true);
|
||||
|
||||
var update = Thread.ofVirtual().start(() -> {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.jonahbauer.raytracing.material;
|
||||
package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
@ -0,0 +1,20 @@
|
||||
package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record DiffuseLight(@NotNull Color emit) implements Material {
|
||||
@Override
|
||||
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Color emitted(@NotNull HitResult hit) {
|
||||
return emit;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record IsotropicMaterial(@NotNull Color albedo) implements Material{
|
||||
@Override
|
||||
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
|
||||
return Optional.of(new ScatterResult(new Ray(hit.position(), Vec3.random(true)), albedo()));
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.jonahbauer.raytracing.material;
|
||||
package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
@ -1,4 +1,4 @@
|
||||
package eu.jonahbauer.raytracing.material;
|
||||
package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
@ -12,6 +12,10 @@ public interface Material {
|
||||
|
||||
@NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit);
|
||||
|
||||
default @NotNull Color emitted(@NotNull HitResult hit) {
|
||||
return Color.BLACK;
|
||||
}
|
||||
|
||||
record ScatterResult(@NotNull Ray ray, @NotNull Color attenuation) {
|
||||
public ScatterResult {
|
||||
Objects.requireNonNull(ray, "ray");
|
@ -1,4 +1,4 @@
|
||||
package eu.jonahbauer.raytracing.material;
|
||||
package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
@ -86,18 +86,28 @@ public final class SimpleRenderer implements Renderer {
|
||||
}
|
||||
|
||||
private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth) {
|
||||
if (depth <= 0) return Color.BLACK;
|
||||
var color = Color.BLACK;
|
||||
var attenuation = Color.WHITE;
|
||||
|
||||
while (depth-- > 0) {
|
||||
var optional = scene.hit(ray, new Range(0.001, Double.POSITIVE_INFINITY));
|
||||
if (optional.isEmpty()) {
|
||||
color = Color.add(color, Color.multiply(attenuation, scene.getBackgroundColor(ray)));
|
||||
break;
|
||||
}
|
||||
|
||||
var optional = scene.hit(ray, new Range(0.001, Double.POSITIVE_INFINITY));
|
||||
if (optional.isPresent()) {
|
||||
var hit = optional.get();
|
||||
var material = hit.material();
|
||||
return material.scatter(ray, hit)
|
||||
.map(scatter -> Color.multiply(scatter.attenuation(), getColor0(scene, scatter.ray(), depth - 1)))
|
||||
.orElse(Color.BLACK);
|
||||
} else {
|
||||
return getSkyboxColor(ray);
|
||||
var emitted = material.emitted(hit);
|
||||
var scatter = material.scatter(ray, hit);
|
||||
color = Color.add(color, Color.multiply(attenuation, emitted));
|
||||
|
||||
if (scatter.isEmpty()) break;
|
||||
attenuation = Color.multiply(attenuation, scatter.get().attenuation());
|
||||
ray = scatter.get().ray();
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,19 +121,6 @@ public final class SimpleRenderer implements Renderer {
|
||||
return parallel ? stream.parallel() : stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the color of the skybox for a given ray} The skybox color is a linear gradient based on the altitude of
|
||||
* the ray above the horizon with {@link Color#SKY} at the top and {@link Color#WHITE} at the bottom.
|
||||
*/
|
||||
private static @NotNull Color getSkyboxColor(@NotNull Ray ray) {
|
||||
// altitude from -pi/2 to pi/2
|
||||
var alt = Math.copySign(
|
||||
Math.acos(ray.direction().withY(0).unit().times(ray.direction().unit())),
|
||||
ray.direction().y()
|
||||
);
|
||||
return Color.lerp(Color.WHITE, Color.SKY, alt / Math.PI + 0.5);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private int samplesPerPixel = 100;
|
||||
private int maxDepth = 10;
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.jonahbauer.raytracing.scene;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.material.Material;
|
||||
import eu.jonahbauer.raytracing.render.material.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
@ -14,7 +14,6 @@ public record HitResult(
|
||||
boolean frontFace
|
||||
) implements Comparable<HitResult> {
|
||||
public HitResult {
|
||||
if (t < 0 || !Double.isFinite(t)) throw new IllegalArgumentException("t must be non-negative");
|
||||
Objects.requireNonNull(position, "position");
|
||||
normal = normal.unit();
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ package eu.jonahbauer.raytracing.scene;
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.scene.transform.RotateY;
|
||||
import eu.jonahbauer.raytracing.scene.transform.Translate;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
@ -19,4 +22,12 @@ public interface Hittable {
|
||||
default @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
default @NotNull Hittable translate(@NotNull Vec3 offset) {
|
||||
return new Translate(this, offset);
|
||||
}
|
||||
|
||||
default @NotNull Hittable rotateY(double angle) {
|
||||
return new RotateY(this, angle);
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +1,64 @@
|
||||
package eu.jonahbauer.raytracing.scene;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Octree;
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.scene.util.HittableCollection;
|
||||
import eu.jonahbauer.raytracing.scene.util.HittableList;
|
||||
import eu.jonahbauer.raytracing.scene.util.HittableOctree;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class Scene implements Hittable {
|
||||
private final @NotNull Octree<@NotNull Hittable> octree;
|
||||
private final @NotNull List<@NotNull Hittable> list;
|
||||
public final class Scene extends HittableCollection {
|
||||
private final @NotNull HittableOctree octree;
|
||||
private final @NotNull HittableList list;
|
||||
private final @NotNull SkyBox background;
|
||||
|
||||
public Scene(@NotNull List<? extends @NotNull Hittable> objects) {
|
||||
this.octree = newOctree(objects);
|
||||
this.list = new ArrayList<>();
|
||||
this(Color.BLACK, objects);
|
||||
}
|
||||
|
||||
for (Hittable object : objects) {
|
||||
var bbox = object.getBoundingBox();
|
||||
if (bbox.isPresent()) {
|
||||
octree.add(bbox.get(), object);
|
||||
public Scene(@NotNull Color background, @NotNull List<? extends @NotNull Hittable> objects) {
|
||||
this(SkyBox.solid(background), objects);
|
||||
}
|
||||
|
||||
public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects) {
|
||||
var bounded = new ArrayList<Hittable>();
|
||||
var unbounded = new ArrayList<Hittable>();
|
||||
|
||||
objects.forEach(object -> {
|
||||
if (object.getBoundingBox().isPresent()) {
|
||||
bounded.add(object);
|
||||
} else {
|
||||
list.add(object);
|
||||
unbounded.add(object);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.octree = new HittableOctree(bounded);
|
||||
this.list = new HittableList(unbounded);
|
||||
this.background = background;
|
||||
}
|
||||
|
||||
public Scene(@NotNull Hittable @NotNull... objects) {
|
||||
this(List.of(objects));
|
||||
}
|
||||
|
||||
public Scene(@NotNull Color background, @NotNull Hittable @NotNull... objects) {
|
||||
this(background, List.of(objects));
|
||||
}
|
||||
|
||||
public Scene(@NotNull SkyBox background, @NotNull Hittable @NotNull... objects) {
|
||||
this(background, List.of(objects));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
||||
var state = new State();
|
||||
state.range = range;
|
||||
|
||||
octree.hit(ray, object -> hit(state, ray, object));
|
||||
list.forEach(object -> hit(state, ray, object));
|
||||
|
||||
return Optional.ofNullable(state.result);
|
||||
public void hit(@NotNull Ray ray, @NotNull State state) {
|
||||
octree.hit(ray, state);
|
||||
list.hit(ray, state);
|
||||
}
|
||||
|
||||
private void hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object) {
|
||||
var r = object.hit(ray, state.range);
|
||||
if (r.isPresent() && state.range.surrounds(r.get().t())) {
|
||||
state.result = r.get();
|
||||
state.range = new Range(state.range.min(), state.result.t());
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull Octree<Hittable> newOctree(@NotNull List<? extends Hittable> objects) {
|
||||
Vec3 center = Vec3.ZERO, max = Vec3.MIN, min = Vec3.MAX;
|
||||
|
||||
int i = 1;
|
||||
for (Hittable object : objects) {
|
||||
var bbox = object.getBoundingBox();
|
||||
if (bbox.isPresent()) {
|
||||
center = Vec3.average(center, bbox.get().center(), i++);
|
||||
max = Vec3.max(max, bbox.get().max());
|
||||
min = Vec3.min(min, bbox.get().min());
|
||||
}
|
||||
}
|
||||
|
||||
var dimension = Arrays.stream(new double[] {
|
||||
Math.abs(max.x() - center.x()),
|
||||
Math.abs(max.y() - center.y()),
|
||||
Math.abs(max.z() - center.z()),
|
||||
Math.abs(min.x() - center.x()),
|
||||
Math.abs(min.y() - center.y()),
|
||||
Math.abs(min.z() - center.z())
|
||||
}).max().orElse(10);
|
||||
|
||||
return new Octree<Hittable>(center, dimension);
|
||||
}
|
||||
|
||||
private static class State {
|
||||
HitResult result;
|
||||
Range range;
|
||||
public @NotNull Color getBackgroundColor(@NotNull Ray ray) {
|
||||
return background.getColor(ray);
|
||||
}
|
||||
}
|
||||
|
25
src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java
Normal file
25
src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java
Normal file
@ -0,0 +1,25 @@
|
||||
package eu.jonahbauer.raytracing.scene;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SkyBox {
|
||||
@NotNull Color getColor(@NotNull Ray ray);
|
||||
|
||||
static @NotNull SkyBox gradient(@NotNull Color top, @NotNull Color bottom) {
|
||||
return ray -> {
|
||||
// altitude from -pi/2 to pi/2
|
||||
var alt = Math.copySign(
|
||||
Math.acos(ray.direction().withY(0).unit().times(ray.direction().unit())),
|
||||
ray.direction().y()
|
||||
);
|
||||
return Color.lerp(bottom, top, alt / Math.PI + 0.5);
|
||||
};
|
||||
}
|
||||
|
||||
static @NotNull SkyBox solid(@NotNull Color color) {
|
||||
return _ -> color;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package eu.jonahbauer.raytracing.scene.hittable2d;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.material.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class Ellipse extends Hittable2D {
|
||||
public Ellipse(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
|
||||
super(origin, u, v, material);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isInterior(double alpha, double beta) {
|
||||
return alpha * alpha + beta * beta < 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
var a = origin.minus(u).minus(v);
|
||||
var b = origin.plus(u).plus(v);
|
||||
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package eu.jonahbauer.raytracing.scene.hittable2d;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.material.Material;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class Hittable2D implements Hittable {
|
||||
protected final @NotNull Vec3 origin;
|
||||
protected final @NotNull Vec3 u;
|
||||
protected final @NotNull Vec3 v;
|
||||
private final @NotNull Material material;
|
||||
|
||||
// internal
|
||||
private final @NotNull Vec3 normal;
|
||||
private final double d;
|
||||
private final @NotNull Vec3 w;
|
||||
|
||||
protected Hittable2D(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
|
||||
this.origin = Objects.requireNonNull(origin);
|
||||
this.u = Objects.requireNonNull(u);
|
||||
this.v = Objects.requireNonNull(v);
|
||||
this.material = Objects.requireNonNull(material);
|
||||
|
||||
var n = u.cross(v);
|
||||
if (n.squared() < 1e-8) throw new IllegalArgumentException();
|
||||
this.normal = n.unit();
|
||||
this.d = origin.times(normal);
|
||||
this.w = n.div(n.squared());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
||||
var denominator = ray.direction().times(normal);
|
||||
if (Math.abs(denominator) < 1e-8) return Optional.empty(); // parallel
|
||||
|
||||
var t = (d - ray.origin().times(normal)) / denominator;
|
||||
if (!range.surrounds(t)) return Optional.empty();
|
||||
|
||||
var position = ray.at(t);
|
||||
var p = position.minus(origin);
|
||||
|
||||
if (!isInterior(p)) return Optional.empty();
|
||||
|
||||
var frontFace = denominator < 0;
|
||||
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace));
|
||||
}
|
||||
|
||||
protected boolean isInterior(@NotNull Vec3 p) {
|
||||
var alpha = w.times(p.cross(v));
|
||||
var beta = w.times(u.cross(p));
|
||||
return isInterior(alpha, beta);
|
||||
}
|
||||
|
||||
protected boolean isInterior(double alpha, double beta) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.jonahbauer.raytracing.scene.hittable2d;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.material.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class Parallelogram extends Hittable2D {
|
||||
|
||||
public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
|
||||
super(origin, u, v, material);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isInterior(double alpha, double beta) {
|
||||
return 0 <= alpha && alpha < 1 && 0 <= beta && beta < 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
var a = origin;
|
||||
var b = origin.plus(u).plus(v);
|
||||
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package eu.jonahbauer.raytracing.scene.hittable2d;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.material.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class Plane extends Hittable2D {
|
||||
public Plane(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
|
||||
super(origin, u, v, material);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isInterior(@NotNull Vec3 p) {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.jonahbauer.raytracing.scene.hittable2d;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.material.Material;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class Triangle extends Hittable2D {
|
||||
|
||||
public Triangle(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
|
||||
super(origin, u, v, material);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isInterior(double alpha, double beta) {
|
||||
return 0 <= alpha && 0 <= beta && alpha + beta <= 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
var a = origin;
|
||||
var b = origin.plus(u).plus(v);
|
||||
return Optional.of(new BoundingBox(Vec3.min(a, b), Vec3.max(a, b)));
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package eu.jonahbauer.raytracing.scene.hittable3d;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.material.IsotropicMaterial;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNull IsotropicMaterial material) implements Hittable {
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
||||
var hit1 = boundary.hit(ray, Range.UNIVERSE);
|
||||
if (hit1.isEmpty()) return Optional.empty();
|
||||
|
||||
var hit2 = boundary.hit(ray, new Range(hit1.get().t() + 0.0001, Double.POSITIVE_INFINITY));
|
||||
if (hit2.isEmpty()) return Optional.empty();
|
||||
|
||||
var tmin = Math.max(range.min(), hit1.get().t());
|
||||
var tmax = Math.min(range.max(), hit2.get().t());
|
||||
if (tmin >= tmax) return Optional.empty();
|
||||
if (tmin < 0) tmin = 0;
|
||||
|
||||
var length = ray.direction().length();
|
||||
var distance = length * (tmax - tmin);
|
||||
var hitDistance = - Math.log(Math.random()) / density;
|
||||
if (hitDistance > distance) return Optional.empty();
|
||||
|
||||
var t = tmin + hitDistance / length;
|
||||
return Optional.of(new HitResult(t, ray.at(t), Vec3.UNIT_X, material, true)); // arbitrary normal and frontFace
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
return boundary().getBoundingBox();
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
package eu.jonahbauer.raytracing.scene;
|
||||
package eu.jonahbauer.raytracing.scene.hittable3d;
|
||||
|
||||
import eu.jonahbauer.raytracing.material.Material;
|
||||
import eu.jonahbauer.raytracing.render.material.Material;
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
@ -0,0 +1,91 @@
|
||||
package eu.jonahbauer.raytracing.scene.transform;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class RotateY extends Transform {
|
||||
private final double cos;
|
||||
private final double sin;
|
||||
|
||||
private final @NotNull Optional<BoundingBox> bbox;
|
||||
|
||||
public RotateY(@NotNull Hittable object, double angle) {
|
||||
super(object);
|
||||
this.cos = Math.cos(angle);
|
||||
this.sin = Math.sin(angle);
|
||||
|
||||
this.bbox = object.getBoundingBox().map(bbox -> {
|
||||
var min = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
|
||||
var max = new Vec3(- Double.MAX_VALUE, - Double.MAX_VALUE, - Double.MAX_VALUE);
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 2; k++) {
|
||||
var x = i * bbox.max().x() + (1 - i) * bbox.min().x();
|
||||
var y = j * bbox.max().y() + (1 - j) * bbox.min().y();
|
||||
var z = k * bbox.max().z() + (1 - k) * bbox.min().z();
|
||||
|
||||
var newx = cos * x + sin * z;
|
||||
var newz = -sin * x + cos * z;
|
||||
|
||||
var temp = new Vec3(newx, y, newz);
|
||||
|
||||
min = Vec3.min(min, temp);
|
||||
max = Vec3.max(max, temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new BoundingBox(min, max);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull Ray transform(@NotNull Ray ray) {
|
||||
var origin = ray.origin();
|
||||
var direction = ray.direction();
|
||||
|
||||
var newOrigin = new Vec3(
|
||||
cos * origin.x() - sin * origin.z(),
|
||||
origin.y(),
|
||||
sin * origin.x() + cos * origin.z()
|
||||
);
|
||||
var newDirection = new Vec3(
|
||||
cos * direction.x() - sin * direction.z(),
|
||||
direction.y(),
|
||||
sin * direction.x() + cos * direction.z()
|
||||
);
|
||||
|
||||
return new Ray(newOrigin, newDirection);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull HitResult transform(@NotNull HitResult result) {
|
||||
var position = result.position();
|
||||
var newPosition = new Vec3(
|
||||
cos * position.x() + sin * position.z(),
|
||||
position.y(),
|
||||
- sin * position.x() + cos * position.z()
|
||||
);
|
||||
|
||||
var normal = result.normal();
|
||||
var newNormal = new Vec3(
|
||||
cos * normal.x() + sin * normal.z(),
|
||||
normal.y(),
|
||||
-sin * normal.x() + cos * normal.z()
|
||||
);
|
||||
|
||||
return new HitResult(result.t(), newPosition, newNormal, result.material(), result.frontFace());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
return bbox;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.jonahbauer.raytracing.scene.transform;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class Transform implements Hittable {
|
||||
protected final @NotNull Hittable object;
|
||||
|
||||
protected Transform(@NotNull Hittable object) {
|
||||
this.object = Objects.requireNonNull(object);
|
||||
}
|
||||
|
||||
protected abstract @NotNull Ray transform(@NotNull Ray ray);
|
||||
|
||||
protected abstract @NotNull HitResult transform(@NotNull HitResult result);
|
||||
|
||||
@Override
|
||||
public final @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
||||
return object.hit(transform(ray), range).map(this::transform);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package eu.jonahbauer.raytracing.scene.transform;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public final class Translate extends Transform {
|
||||
private final @NotNull Vec3 offset;
|
||||
|
||||
private final @NotNull Optional<BoundingBox> bbox;
|
||||
|
||||
public Translate(@NotNull Hittable object, @NotNull Vec3 offset) {
|
||||
super(object);
|
||||
this.offset = offset;
|
||||
this.bbox = object.getBoundingBox().map(bbox -> new BoundingBox(
|
||||
bbox.min().plus(offset),
|
||||
bbox.max().plus(offset)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull Ray transform(@NotNull Ray ray) {
|
||||
return new Ray(ray.origin().minus(offset), ray.direction());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull HitResult transform(@NotNull HitResult result) {
|
||||
return new HitResult(
|
||||
result.t(),
|
||||
result.position().plus(offset),
|
||||
result.normal(),
|
||||
result.material(),
|
||||
result.frontFace()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
return bbox;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package eu.jonahbauer.raytracing.scene.util;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class HittableCollection implements Hittable {
|
||||
|
||||
@Override
|
||||
public final @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
||||
var state = new State(range);
|
||||
hit(ray, state);
|
||||
return state.getResult();
|
||||
}
|
||||
|
||||
public abstract void hit(@NotNull Ray ray, @NotNull State state);
|
||||
|
||||
protected static @NotNull Optional<BoundingBox> getBoundingBox(@NotNull Collection<? extends @NotNull Hittable> objects) {
|
||||
var bbox = new BoundingBox(Vec3.ZERO, Vec3.ZERO);
|
||||
for (var object : objects) {
|
||||
var b = object.getBoundingBox();
|
||||
if (b.isPresent()) {
|
||||
bbox = bbox.expand(b.get());
|
||||
} else {
|
||||
bbox = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(bbox);
|
||||
}
|
||||
|
||||
protected static boolean hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object) {
|
||||
var r = object.hit(ray, state.range);
|
||||
if (r.isPresent()) {
|
||||
if (state.range.surrounds(r.get().t())){
|
||||
state.result = r.get();
|
||||
state.range = new Range(state.range.min(), state.result.t());
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class State {
|
||||
private @NotNull Range range;
|
||||
private HitResult result;
|
||||
|
||||
private State(@NotNull Range range) {
|
||||
this.range = Objects.requireNonNull(range);
|
||||
}
|
||||
|
||||
private @NotNull Optional<HitResult> getResult() {
|
||||
return Optional.ofNullable(result);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package eu.jonahbauer.raytracing.scene.util;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.BoundingBox;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class HittableList extends HittableCollection {
|
||||
private final @NotNull List<Hittable> objects;
|
||||
private final @NotNull Optional<BoundingBox> bbox;
|
||||
|
||||
public HittableList(@NotNull List<? extends @NotNull Hittable> objects) {
|
||||
this.objects = List.copyOf(objects);
|
||||
this.bbox = getBoundingBox(this.objects);
|
||||
}
|
||||
|
||||
public HittableList(@NotNull Hittable @NotNull... objects) {
|
||||
this(List.of(objects));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(@NotNull Ray ray, @NotNull State state) {
|
||||
objects.forEach(object -> hit(state, ray, object));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
return bbox;
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package eu.jonahbauer.raytracing.scene.util;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.*;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class HittableOctree extends HittableCollection {
|
||||
private final @NotNull Octree<Hittable> objects;
|
||||
private final @NotNull Optional<BoundingBox> bbox;
|
||||
|
||||
public HittableOctree(@NotNull List<? extends @NotNull Hittable> objects) {
|
||||
var result = newOctree(objects);
|
||||
this.objects = result.getKey();
|
||||
this.bbox = Optional.of(result.getValue());
|
||||
}
|
||||
|
||||
public HittableOctree(@NotNull Hittable @NotNull... objects) {
|
||||
this(List.of(objects));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hit(@NotNull Ray ray, @NotNull State state) {
|
||||
objects.hit(ray, object -> hit(state, ray, object));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
||||
return bbox;
|
||||
}
|
||||
|
||||
private static @NotNull Entry<@NotNull Octree<Hittable>, @NotNull BoundingBox> newOctree(@NotNull List<? extends Hittable> objects) {
|
||||
Vec3 center = Vec3.ZERO, max = Vec3.MIN, min = Vec3.MAX;
|
||||
|
||||
int i = 1;
|
||||
for (var object : objects) {
|
||||
var bbox = object.getBoundingBox().orElseThrow();
|
||||
center = Vec3.average(center, bbox.center(), i++);
|
||||
max = Vec3.max(max, bbox.max());
|
||||
min = Vec3.min(min, bbox.min());
|
||||
}
|
||||
|
||||
var dimension = Arrays.stream(new double[] {
|
||||
Math.abs(max.x() - center.x()),
|
||||
Math.abs(max.y() - center.y()),
|
||||
Math.abs(max.z() - center.z()),
|
||||
Math.abs(min.x() - center.x()),
|
||||
Math.abs(min.y() - center.y()),
|
||||
Math.abs(min.z() - center.z())
|
||||
}).max().orElse(10);
|
||||
|
||||
var out = new Octree<Hittable>(center, dimension);
|
||||
objects.forEach(object -> out.add(object.getBoundingBox().get(), object));
|
||||
return Map.entry(out, new BoundingBox(min, max));
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package eu.jonahbauer.raytracing.scene.util;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.material.Material;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import eu.jonahbauer.raytracing.scene.hittable2d.Parallelogram;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class Hittables {
|
||||
private Hittables() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static @NotNull Hittable box(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Material material) {
|
||||
var sides = new ArrayList<Hittable>();
|
||||
|
||||
var min = Vec3.min(a, b);
|
||||
var max = Vec3.max(a, b);
|
||||
|
||||
var dx = new Vec3(max.x() - min.x(), 0, 0);
|
||||
var dy = new Vec3(0, max.y() - min.y(), 0);
|
||||
var dz = new Vec3(0, 0, max.z() - min.z());
|
||||
|
||||
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), max.z()), dx, dy, material)); // front
|
||||
sides.add(new Parallelogram(new Vec3(max.x(), min.y(), max.z()), dz.neg(), dy, material)); // right
|
||||
sides.add(new Parallelogram(new Vec3(max.x(), min.y(), min.z()), dx.neg(), dy, material)); // back
|
||||
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), min.z()), dz, dy, material)); // left
|
||||
sides.add(new Parallelogram(new Vec3(min.x(), max.y(), max.z()), dx, dz.neg(), material)); // top
|
||||
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), min.z()), dx, dz, material)); // bottom
|
||||
|
||||
return new HittableList(sides);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
package eu.jonahbauer.raytracing.render.canvas;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.ImageFormat;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@ -17,18 +19,18 @@ class ImageTest {
|
||||
void test(@TempDir Path dir) throws IOException {
|
||||
var image = new Image(256, 256);
|
||||
|
||||
for (var y = 0; y < image.height(); y++) {
|
||||
for (var x = 0; x < image.width(); x++) {
|
||||
var r = (double) x / (image.width() - 1);
|
||||
var g = (double) y / (image.height() - 1);
|
||||
for (var y = 0; y < image.getHeight(); y++) {
|
||||
for (var x = 0; x < image.getWidth(); x++) {
|
||||
var r = (double) x / (image.getWidth() - 1);
|
||||
var g = (double) y / (image.getHeight() - 1);
|
||||
var b = 0;
|
||||
|
||||
image.set(x, y, r, g, b);
|
||||
image.set(x, y, new Color(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(dir);
|
||||
ImageIO.write(image, dir.resolve("img.ppm"));
|
||||
ImageFormat.PPM.write(image, dir.resolve("img.ppm"));
|
||||
|
||||
String expected;
|
||||
String actual;
|
@ -1,8 +1,10 @@
|
||||
package eu.jonahbauer.raytracing.scene;
|
||||
package eu.jonahbauer.raytracing.scene.hittable3d;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.material.LambertianMaterial;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
@ -13,7 +15,7 @@ class SphereTest {
|
||||
void hit() {
|
||||
var center = new Vec3(1, 2, 3);
|
||||
var radius = 5;
|
||||
var sphere = new Sphere(center, radius);
|
||||
var sphere = new Sphere(center, radius, new LambertianMaterial(Color.WHITE));
|
||||
|
||||
var origin = new Vec3(6, 7, 8);
|
||||
var direction = new Vec3(-1, -1, -1);
|
Loading…
x
Reference in New Issue
Block a user