Compare commits
55 Commits
d67f877428
...
main
Author | SHA1 | Date | |
---|---|---|---|
5c92606f99 | |||
23c7a550ec | |||
6599c41b14 | |||
9e79333e1e | |||
dbd3d5fc4b | |||
e2c9609e0e | |||
89c4340821 | |||
c0dccbbd0c | |||
ed9e50b8f2 | |||
6e35453932 | |||
77c1a87e4f | |||
cb4dcc53f1 | |||
940e8ebc37 | |||
2a2cf7b642 | |||
c4ee560dc9 | |||
6b47f44ad2 | |||
5f1e816edd | |||
a31488bc78 | |||
67bfafc5b8 | |||
f7d9153ad8 | |||
a22b1cb238 | |||
c91baf9e0c | |||
a90a0db6d5 | |||
1b02f8a96d | |||
18c179f8e3 | |||
1d48a49987 | |||
9b617a82a8 | |||
dfe80011c9 | |||
37539a1906 | |||
70f2f38e96 | |||
e6447fe684 | |||
7c0bc68ab2 | |||
2c28b10a6e | |||
d5173c2d5a | |||
48a591de7e | |||
9ebf8bd1c4 | |||
9106ccf8b0 | |||
3a3949f518 | |||
414af5860b | |||
9175377ac4 | |||
96909002d7 | |||
ac5a42e7bc | |||
e4e241a314 | |||
360fb2c990 | |||
c002b8215a | |||
580d8eca12 | |||
27e3fc0990 | |||
ebbf711403 | |||
3be855cffd | |||
36de714f46 | |||
8ea894cd3e | |||
b5acbd1b6c | |||
86b6f1891c | |||
7a5526f987 | |||
137c0b2190 |
57
README.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# raytracing
|
||||||
|
|
||||||
|
Based on the series <a href="https://raytracing.github.io"><cite>Ray Tracing in One Weekend</cite></a>.
|
||||||
|
|
||||||
|
## Scenes
|
||||||
|
|
||||||
|
### simple
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
java -jar raytracing.jar --samples 5000 --height 1080 SIMPLE
|
||||||
|
```
|
||||||
|
|
||||||
|
### spheres
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
java -jar raytracing.jar --samples 1000 --height 1080 SPHERES
|
||||||
|
```
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### diagramm
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
java -jar raytracing.jar --samples 1000 --height 1080 DIAGRAMM
|
||||||
|
```
|
||||||
|
|
||||||
|
### a little bit of everything
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
java -jar raytracing.jar --samples 10000 --height 1200 FINAL
|
||||||
|
```
|
@@ -1,5 +1,16 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("java")
|
id("java")
|
||||||
|
id("application")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(22)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass = "eu.jonahbauer.raytracing.Main"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
BIN
docs/cornell.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/cornell_smoke.png
Normal file
After Width: | Height: | Size: 441 KiB |
BIN
docs/diagramm.png
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
docs/final.png
Normal file
After Width: | Height: | Size: 3.1 MiB |
BIN
docs/simple.png
Normal file
After Width: | Height: | Size: 881 KiB |
BIN
docs/spheres.png
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/squares.png
Normal file
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) {
|
||||||
|
}
|
397
src/main/java/eu/jonahbauer/raytracing/Examples.java
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
package eu.jonahbauer.raytracing;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.AABB;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.CheckerTexture;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
|
||||||
|
import eu.jonahbauer.raytracing.render.material.*;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.ImageTexture;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.PerlinTexture;
|
||||||
|
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.Box;
|
||||||
|
import eu.jonahbauer.raytracing.scene.hittable3d.ConstantMedium;
|
||||||
|
import eu.jonahbauer.raytracing.scene.hittable3d.Sphere;
|
||||||
|
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree;
|
||||||
|
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("CORNELL_SPHERE", Examples::getCornellBoxSphere);
|
||||||
|
register("DIAGRAMM", Examples::getDiagramm);
|
||||||
|
register("EARTH", Examples::getEarth);
|
||||||
|
register("PERLIN", Examples::getPerlin);
|
||||||
|
register("FINAL", Examples::getFinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(), 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))
|
||||||
|
)),
|
||||||
|
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 CheckerTexture(0.32, new Color(.2, .3, .1), new Color(.9, .9, .9)))
|
||||||
|
));
|
||||||
|
|
||||||
|
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(), List.of(
|
||||||
|
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(List.of(
|
||||||
|
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(List.of(
|
||||||
|
new Box(new Vec3(0, 0, 0), new Vec3(555, 555, 555), white, white, red, green, white, null),
|
||||||
|
new Parallelogram(new Vec3(343, 554, 332), new Vec3(-130, 0, 0), new Vec3(0, 0, -105), light),
|
||||||
|
new Box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white)
|
||||||
|
.rotateY(Math.toRadians(15))
|
||||||
|
.translate(new Vec3(265, 0, 295)),
|
||||||
|
new 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(List.of(
|
||||||
|
new Box(new Vec3(0, 0, 0), new Vec3(555, 555, 555), white, white, red, green, white, null),
|
||||||
|
new Parallelogram(new Vec3(113, 554, 127), new Vec3(330, 0, 0), new Vec3(0, 0, 305), light),
|
||||||
|
new ConstantMedium(
|
||||||
|
new 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(
|
||||||
|
new 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 getCornellBoxSphere(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));
|
||||||
|
var glass = new DielectricMaterial(1.5);
|
||||||
|
|
||||||
|
var room = new Box(new Vec3(0, 0, 0), new Vec3(555, 555, 555), white, white, red, green, white, null);
|
||||||
|
var lamp = new Parallelogram(new Vec3(343, 554, 332), new Vec3(-130, 0, 0), new Vec3(0, 0, -105), light);
|
||||||
|
var box = new Box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white)
|
||||||
|
.rotateY(Math.toRadians(15))
|
||||||
|
.translate(new Vec3(265, 0, 295));
|
||||||
|
var sphere = new Sphere(new Vec3(190, 90, 190), 90, glass);
|
||||||
|
|
||||||
|
return new Example(
|
||||||
|
new Scene(List.of(room, box), List.of(lamp, sphere)),
|
||||||
|
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(new 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull Example getEarth(int height) {
|
||||||
|
if (height <= 0) height = 450;
|
||||||
|
|
||||||
|
return new Example(
|
||||||
|
new Scene(getSkyBox(), List.of(
|
||||||
|
new Sphere(Vec3.ZERO, 2, new LambertianMaterial(new ImageTexture("/earthmap.jpg")))
|
||||||
|
)),
|
||||||
|
SimpleCamera.builder()
|
||||||
|
.withImage(height * 16 / 9, height)
|
||||||
|
.withFieldOfView(Math.toRadians(20))
|
||||||
|
.withPosition(new Vec3(12, 0, 0))
|
||||||
|
.withTarget(Vec3.ZERO)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull Example getPerlin(int height) {
|
||||||
|
if (height <= 0) height = 450;
|
||||||
|
|
||||||
|
var material = new LambertianMaterial(new PerlinTexture(4));
|
||||||
|
|
||||||
|
return new Example(
|
||||||
|
new Scene(getSkyBox(), List.of(
|
||||||
|
new Sphere(new Vec3(0, -1000, 0), 1000, material),
|
||||||
|
new Sphere(new Vec3(0, 2, 0), 2, material)
|
||||||
|
)),
|
||||||
|
SimpleCamera.builder()
|
||||||
|
.withImage(height * 16 / 9, height)
|
||||||
|
.withFieldOfView(Math.toRadians(20))
|
||||||
|
.withPosition(new Vec3(13, 2, 3))
|
||||||
|
.withTarget(Vec3.ZERO)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull Example getFinal(int height) {
|
||||||
|
if (height <= 0) height = 400;
|
||||||
|
|
||||||
|
var objects = new ArrayList<Hittable>();
|
||||||
|
var random = new Random(1);
|
||||||
|
|
||||||
|
// boxes
|
||||||
|
var boxes = new ArrayList<Hittable>();
|
||||||
|
var ground = new LambertianMaterial(new Color(0.48, 0.83, 0.53));
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
for (int j = 0; j < 20; j++) {
|
||||||
|
var w = 100.0;
|
||||||
|
var x0 = -1000.0 + i * w;
|
||||||
|
var z0 = -1000.0 + j * w;
|
||||||
|
var y0 = 0.0;
|
||||||
|
var x1 = x0 + w;
|
||||||
|
var y1 = random.nextInt(1, 101);
|
||||||
|
var z1 = z0 + w;
|
||||||
|
boxes.add(new Box(new Vec3(x0, y0, z0), new Vec3(x1, y1, z1), ground));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objects.add(new HittableBinaryTree(boxes));
|
||||||
|
|
||||||
|
// light
|
||||||
|
objects.add(new Parallelogram(
|
||||||
|
new Vec3(123, 554, 147), new Vec3(300, 0, 0), new Vec3(0, 0, 265),
|
||||||
|
new DiffuseLight(new Color(7., 7., 7.))
|
||||||
|
));
|
||||||
|
|
||||||
|
// spheres with different materials
|
||||||
|
objects.add(new Sphere(new Vec3(400, 400, 200), 50, new LambertianMaterial(new Color(0.7, 0.3, 0.1))));
|
||||||
|
objects.add(new Sphere(new Vec3(260, 150, 45), 50, new DielectricMaterial(1.5)));
|
||||||
|
objects.add(new Sphere(new Vec3(0, 150, 145), 50, new MetallicMaterial(new Color(0.8, 0.8, 0.9), 1.0)));
|
||||||
|
|
||||||
|
// glass sphere filled with gas
|
||||||
|
var boundary = new Sphere(new Vec3(360, 150, 145), 70, new DielectricMaterial(1.5));
|
||||||
|
objects.add(boundary);
|
||||||
|
objects.add(new ConstantMedium(boundary, 0.2, new IsotropicMaterial(new Color(0.2, 0.4, 0.9))));
|
||||||
|
|
||||||
|
// put the world in a glass sphere
|
||||||
|
objects.add(new ConstantMedium(
|
||||||
|
new Sphere(new Vec3(0, 0, 0), 5000, new DielectricMaterial(1.5)),
|
||||||
|
0.0001, new IsotropicMaterial(new Color(1., 1., 1.))
|
||||||
|
));
|
||||||
|
|
||||||
|
// textures spheres
|
||||||
|
objects.add(new Sphere(new Vec3(400, 200, 400), 100, new LambertianMaterial(new ImageTexture("/earthmap.jpg"))));
|
||||||
|
objects.add(new Sphere(new Vec3(220, 280, 300), 80, new LambertianMaterial(new PerlinTexture(0.2))));
|
||||||
|
|
||||||
|
// box from spheres
|
||||||
|
var white = new LambertianMaterial(new Color(.73, .73, .73));
|
||||||
|
var spheres = new ArrayList<Hittable>();
|
||||||
|
for (int j = 0; j < 1000; j++) {
|
||||||
|
spheres.add(new Sphere(new Vec3(random.nextDouble(165), random.nextDouble(165), random.nextDouble(165)), 10, white));
|
||||||
|
}
|
||||||
|
objects.add(new HittableBinaryTree(spheres).rotateY(Math.toRadians(15)).translate(new Vec3(-100, 270, 395)));
|
||||||
|
|
||||||
|
return new Example(
|
||||||
|
new Scene(objects),
|
||||||
|
SimpleCamera.builder()
|
||||||
|
.withImage(height, height)
|
||||||
|
.withFieldOfView(Math.toRadians(40))
|
||||||
|
.withPosition(new Vec3(478, 278, -600))
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,97 +1,123 @@
|
|||||||
package eu.jonahbauer.raytracing;
|
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.ImageFormat;
|
||||||
import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
|
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||||
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
|
|
||||||
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.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.Sphere;
|
|
||||||
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.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.function.IntFunction;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) throws IOException {
|
public static final boolean DEBUG = false;
|
||||||
var scene = getScene();
|
|
||||||
|
|
||||||
var camera = SimpleCamera.builder()
|
public static void main(String[] args) throws IOException {
|
||||||
.withImage(1200, 675)
|
var config = Config.parse(args);
|
||||||
.withPosition(new Vec3(13, 2, 3))
|
var example = config.example;
|
||||||
.withTarget(new Vec3(0, 0, 0))
|
var scene = example.scene();
|
||||||
.withFieldOfView(Math.toRadians(20))
|
var camera = example.camera();
|
||||||
.withFocusDistance(10.0)
|
|
||||||
.withBlurAngle(Math.toRadians(0.6))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
var renderer = SimpleRenderer.builder()
|
var renderer = SimpleRenderer.builder()
|
||||||
.withSamplesPerPixel(500)
|
.withSamplesPerPixel(config.samples)
|
||||||
.withMaxDepth(50)
|
.withMaxDepth(config.depth)
|
||||||
.withIterative(true)
|
.withIterative(config.iterative)
|
||||||
|
.withParallel(config.parallel)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
Canvas canvas;
|
||||||
|
if (config.preview) {
|
||||||
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
|
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
|
||||||
image.preview();
|
image.preview();
|
||||||
|
canvas = image;
|
||||||
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 {
|
} else {
|
||||||
// glass
|
canvas = new Image(camera.getWidth(), camera.getHeight());
|
||||||
material = new DielectricMaterial(1.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.add(new Sphere(center, 0.2, material));
|
long time = System.nanoTime();
|
||||||
|
renderer.render(camera, scene, canvas);
|
||||||
|
System.out.printf("rendering finished after %dms", (System.nanoTime() - time) / 1_000_000);
|
||||||
|
|
||||||
|
ImageFormat.PNG.write(canvas, config.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, boolean parallel, int samples, int depth) {
|
||||||
|
public static @NotNull Config parse(@NotNull String @NotNull[] args) {
|
||||||
|
IntFunction<Example> example = null;
|
||||||
|
Path path = null;
|
||||||
|
boolean preview = true;
|
||||||
|
boolean iterative = false;
|
||||||
|
boolean parallel = false;
|
||||||
|
int samples = 1000;
|
||||||
|
int depth = 50;
|
||||||
|
int height = -1;
|
||||||
|
|
||||||
|
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 "--parallel" -> parallel = true;
|
||||||
|
case "--no-parallel" -> parallel = 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("-") -> {
|
||||||
|
try {
|
||||||
|
example = Examples.getByName(str);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
throw fail(ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> throw fail("unknown option " + args[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5)));
|
if (example == null) example = Examples::getCornellBoxSmoke;
|
||||||
objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1))));
|
if (path == null) path = Path.of("scene-" + System.currentTimeMillis() + ".png");
|
||||||
objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5))));
|
return new Config(example.apply(height), path, preview, iterative, parallel, samples, depth);
|
||||||
|
|
||||||
return new Scene(objects);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NotNull Scene getSimpleScene() {
|
private static @NotNull RuntimeException fail(@NotNull String message) {
|
||||||
return new Scene(List.of(
|
System.err.println(message);
|
||||||
new Sphere(new Vec3(0, -100.5, -1.0), 100.0, new LambertianMaterial(new Color(0.8, 0.8, 0.0))),
|
System.exit(1);
|
||||||
new Sphere(new Vec3(0, 0, -1.2), 0.5, new LambertianMaterial(new Color(0.1, 0.2, 0.5))),
|
return new RuntimeException();
|
||||||
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))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,25 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.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.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public record LambertianMaterial(@NotNull Color albedo) implements Material {
|
|
||||||
public LambertianMaterial {
|
|
||||||
Objects.requireNonNull(albedo, "albedo");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
|
|
||||||
var newDirection = hit.normal().plus(Vec3.random(true));
|
|
||||||
if (newDirection.isNearZero()) newDirection = hit.normal();
|
|
||||||
|
|
||||||
var scattered = new Ray(hit.position(), newDirection);
|
|
||||||
return Optional.of(new ScatterResult(scattered, albedo));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.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.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface Material {
|
|
||||||
|
|
||||||
@NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit);
|
|
||||||
|
|
||||||
record ScatterResult(@NotNull Ray ray, @NotNull Color attenuation) {
|
|
||||||
public ScatterResult {
|
|
||||||
Objects.requireNonNull(ray, "ray");
|
|
||||||
Objects.requireNonNull(attenuation, "attenuation");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.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.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Material {
|
|
||||||
|
|
||||||
public MetallicMaterial(@NotNull Color albedo) {
|
|
||||||
this(albedo, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MetallicMaterial {
|
|
||||||
Objects.requireNonNull(albedo, "albedo");
|
|
||||||
if (fuzz < 0 || !Double.isFinite(fuzz)) throw new IllegalArgumentException("fuzz must be non-negative");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit) {
|
|
||||||
var newDirection = Vec3.reflect(ray.direction(), hit.normal());
|
|
||||||
if (fuzz > 0) {
|
|
||||||
newDirection = newDirection.unit().plus(Vec3.random(true).times(fuzz));
|
|
||||||
}
|
|
||||||
return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), albedo));
|
|
||||||
}
|
|
||||||
}
|
|
125
src/main/java/eu/jonahbauer/raytracing/math/AABB.java
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the range of x values}
|
||||||
|
*/
|
||||||
|
public @NotNull Range x() {
|
||||||
|
return new Range(min.x(), max.x());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the range of y values}
|
||||||
|
*/
|
||||||
|
public @NotNull Range y() {
|
||||||
|
return new Range(min.y(), max.y());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the range of z values}
|
||||||
|
*/
|
||||||
|
public @NotNull Range z() {
|
||||||
|
return new Range(min.z(), max.z());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the center of this bounding box}
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 center() {
|
||||||
|
return Vec3.average(min, max, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands this bounding box to include the other bounding box.
|
||||||
|
* @param box a bounding box
|
||||||
|
* @return the expanded bounding box
|
||||||
|
*/
|
||||||
|
public @NotNull AABB expand(@NotNull AABB box) {
|
||||||
|
return new AABB(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the {@code ray} intersects this bounding box withing the {@code range}
|
||||||
|
* @param ray a ray
|
||||||
|
* @param range a range of valid {@code t}s
|
||||||
|
* @return {@code true} iff the ray intersects this bounding box, {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public boolean hit(@NotNull Ray ray, @NotNull Range range) {
|
||||||
|
var origin = ray.origin();
|
||||||
|
var direction = ray.direction();
|
||||||
|
var invDirection = 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 ray direction
|
||||||
|
if (direction.get(i) >= 0) {
|
||||||
|
// min is lower limit and max is upper limit
|
||||||
|
if (tmin[i] > tlmax) tlmax = tmin[i];
|
||||||
|
if (tmax[i] < tumin) tumin = tmax[i];
|
||||||
|
} else {
|
||||||
|
// max is lower limit and min is upper limit
|
||||||
|
if (tmax[i] > tlmax) tlmax = tmax[i];
|
||||||
|
if (tmin[i] < tumin) tumin = tmin[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlmax < tumin && tumin >= range.min() && tlmax <= range.max();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the {@code t} values of the intersections of a ray with the axis-aligned planes through a point.
|
||||||
|
* @param corner the point
|
||||||
|
* @param origin the origin point of the ray
|
||||||
|
* @param invDirection the {@linkplain Vec3#inv() inverted} direction of the ray
|
||||||
|
* @return a three-element array of the {@code t} values of the intersection with the yz-, xz- and xy-plane through {@code corner}
|
||||||
|
*/
|
||||||
|
public 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,10 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.math;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public record BoundingBox(@NotNull Vec3 min, @NotNull Vec3 max) {
|
|
||||||
|
|
||||||
public @NotNull Vec3 center() {
|
|
||||||
return Vec3.average(min, max, 2);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,11 +11,6 @@ public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Vec3 at(double t) {
|
public @NotNull Vec3 at(double t) {
|
||||||
if (t < 0) throw new IllegalArgumentException("t must not be negative");
|
return Vec3.fma(t, direction, origin);
|
||||||
return new Vec3(
|
|
||||||
origin().x() + t * direction.x(),
|
|
||||||
origin().y() + t * direction.y(),
|
|
||||||
origin().z() + t * direction.z()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,34 +3,76 @@ 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;
|
||||||
|
|
||||||
|
import static eu.jonahbauer.raytracing.Main.DEBUG;
|
||||||
|
|
||||||
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);
|
||||||
|
public static final Vec3 MAX = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
|
||||||
|
public static final Vec3 MIN = new Vec3(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE);
|
||||||
public static final Vec3 UNIT_X = new Vec3(1, 0, 0);
|
public static final Vec3 UNIT_X = new Vec3(1, 0, 0);
|
||||||
public static final Vec3 UNIT_Y = new Vec3(0, 1, 0);
|
public static final Vec3 UNIT_Y = new Vec3(0, 1, 0);
|
||||||
public static final Vec3 UNIT_Z = new Vec3(0, 0, 1);
|
public static final Vec3 UNIT_Z = new Vec3(0, 0, 1);
|
||||||
|
|
||||||
public Vec3 {
|
public Vec3 {
|
||||||
assert Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z) : "x, y and z must be finite";
|
if (DEBUG) {
|
||||||
|
if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z)) {
|
||||||
|
throw new IllegalArgumentException("x, y and z must be finite");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Vec3 random() {
|
/**
|
||||||
return random(false);
|
* {@return a uniformly random unit vector}
|
||||||
|
*/
|
||||||
|
public static @NotNull Vec3 random(@NotNull RandomGenerator random) {
|
||||||
|
double x, y, z;
|
||||||
|
double squared;
|
||||||
|
do {
|
||||||
|
x = Math.fma(2, random.nextDouble(), -1);
|
||||||
|
y = Math.fma(2, random.nextDouble(), -1);
|
||||||
|
z = Math.fma(2, random.nextDouble(), -1);
|
||||||
|
squared = x * x + y * y + z * z;
|
||||||
|
} while (squared > 1);
|
||||||
|
var factor = 1 / Math.sqrt(squared);
|
||||||
|
return new Vec3(x * factor, y * factor, z * factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Vec3 random(boolean unit) {
|
/**
|
||||||
var random = new Vec3(
|
* {@return a uniformly random unit vector on the opposite hemisphere of the given <code>direction</code>}
|
||||||
2 * Math.random() - 1,
|
*/
|
||||||
2 * Math.random() - 1,
|
public static @NotNull Vec3 randomOppositeHemisphere(@NotNull RandomGenerator random, @NotNull Vec3 direction) {
|
||||||
2 * Math.random() - 1
|
double x, y, z;
|
||||||
);
|
double squared;
|
||||||
return unit ? random.unit() : random;
|
do {
|
||||||
|
x = Math.fma(2, random.nextDouble(), -1);
|
||||||
|
y = Math.fma(2, random.nextDouble(), -1);
|
||||||
|
z = Math.fma(2, random.nextDouble(), -1);
|
||||||
|
squared = x * x + y * y + z * z;
|
||||||
|
} while (squared > 1 || direction.x() * x + direction.y() * y + direction.z() * z >= 0);
|
||||||
|
var factor = 1 / Math.sqrt(squared);
|
||||||
|
return new Vec3(x * factor, y * factor, z * factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reflects a vector on the given {@code normal} vector.
|
||||||
|
* @param vec a vector
|
||||||
|
* @param normal the surface normal (must be a unit vector)
|
||||||
|
* @return the reflected vector
|
||||||
|
*/
|
||||||
public static @NotNull Vec3 reflect(@NotNull Vec3 vec, @NotNull Vec3 normal) {
|
public static @NotNull Vec3 reflect(@NotNull Vec3 vec, @NotNull Vec3 normal) {
|
||||||
return vec.minus(normal.times(2 * normal.times(vec)));
|
var factor = - 2 * normal.times(vec);
|
||||||
|
return Vec3.fma(factor, normal, vec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refracts a vector on the given {@code normal} vector.
|
||||||
|
* @param vec a vector
|
||||||
|
* @param normal the surface normal (must be a unit vector)
|
||||||
|
* @param ri the refractive index
|
||||||
|
* @return the refracted vector
|
||||||
|
*/
|
||||||
public static @NotNull Optional<Vec3> refract(@NotNull Vec3 vec, @NotNull Vec3 normal, double ri) {
|
public static @NotNull Optional<Vec3> refract(@NotNull Vec3 vec, @NotNull Vec3 normal, double ri) {
|
||||||
vec = vec.unit();
|
vec = vec.unit();
|
||||||
var cosTheta = Math.min(- vec.times(normal), 1.0);
|
var cosTheta = Math.min(- vec.times(normal), 1.0);
|
||||||
@@ -42,63 +84,212 @@ public record Vec3(double x, double y, double z) {
|
|||||||
return Optional.of(rOutPerp.plus(rOutParallel));
|
return Optional.of(rOutPerp.plus(rOutParallel));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates a vector around an {@code axis}.
|
||||||
|
* @param vec a vector
|
||||||
|
* @param axis the rotation axis
|
||||||
|
* @param angle the angle in radians
|
||||||
|
* @return the rotated vector
|
||||||
|
*/
|
||||||
public static @NotNull Vec3 rotate(@NotNull Vec3 vec, @NotNull Vec3 axis, double angle) {
|
public static @NotNull Vec3 rotate(@NotNull Vec3 vec, @NotNull Vec3 axis, double angle) {
|
||||||
Vec3 vxp = axis.cross(vec);
|
Vec3 vxp = axis.cross(vec);
|
||||||
Vec3 vxvxp = axis.cross(vxp);
|
Vec3 vxvxp = axis.cross(vxp);
|
||||||
return vec.plus(vxp.times(Math.sin(angle))).plus(vxvxp.times(1 - Math.cos(angle)));
|
return vec.plus(vxp.times(Math.sin(angle))).plus(vxvxp.times(1 - Math.cos(angle)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the euclidean distance between two vectors}
|
||||||
|
* @param a a vector
|
||||||
|
* @param b another vector
|
||||||
|
*/
|
||||||
public static double distance(@NotNull Vec3 a, @NotNull Vec3 b) {
|
public static double distance(@NotNull Vec3 a, @NotNull Vec3 b) {
|
||||||
return a.minus(b).length();
|
return a.minus(b).length();
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Vec3 plus(@NotNull Vec3 b) {
|
/**
|
||||||
return new Vec3(this.x + b.x, this.y + b.y, this.z + b.z);
|
* Computes a running average of vectors.
|
||||||
|
* @param current the current running average
|
||||||
|
* @param next the next vector
|
||||||
|
* @param index the one-based index of the next vector
|
||||||
|
* @return the new running average
|
||||||
|
*/
|
||||||
|
public static @NotNull Vec3 average(@NotNull Vec3 current, @NotNull Vec3 next, int index) {
|
||||||
|
var factor = 1d / index;
|
||||||
|
return new Vec3(
|
||||||
|
Math.fma(factor, next.x() - current.x(), current.x()),
|
||||||
|
Math.fma(factor, next.y() - current.y(), current.y()),
|
||||||
|
Math.fma(factor, next.z() - current.z(), current.z())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Vec3 minus(@NotNull Vec3 b) {
|
/**
|
||||||
return new Vec3(this.x - b.x, this.y - b.y, this.z - b.z);
|
* {@return a component-wise maximum vector}
|
||||||
|
* @param a a vector
|
||||||
|
* @param b another vector
|
||||||
|
*/
|
||||||
|
public static @NotNull Vec3 max(@NotNull Vec3 a, @NotNull Vec3 b) {
|
||||||
|
return new Vec3(
|
||||||
|
Math.max(a.x(), b.x()),
|
||||||
|
Math.max(a.y(), b.y()),
|
||||||
|
Math.max(a.z(), b.z())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public double times(@NotNull Vec3 b) {
|
/**
|
||||||
return this.x * b.x + this.y * b.y + this.z * b.z;
|
* {@return a component-wise minimum vector}
|
||||||
|
* @param a a vector
|
||||||
|
* @param b another vector
|
||||||
|
*/
|
||||||
|
public static @NotNull Vec3 min(@NotNull Vec3 a, @NotNull Vec3 b) {
|
||||||
|
return new Vec3(
|
||||||
|
Math.min(a.x(), b.x()),
|
||||||
|
Math.min(a.y(), b.y()),
|
||||||
|
Math.min(a.z(), b.z())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Vec3 times(double b) {
|
/**
|
||||||
return new Vec3(this.x * b, this.y * b, this.z * b);
|
* {@return <code>a * b + c</code>}
|
||||||
|
* @param a scalar
|
||||||
|
* @param b a vector
|
||||||
|
* @param c another vector
|
||||||
|
*/
|
||||||
|
public static @NotNull Vec3 fma(double a, @NotNull Vec3 b, @NotNull Vec3 c) {
|
||||||
|
return new Vec3(
|
||||||
|
Math.fma(a, b.x(), c.x()),
|
||||||
|
Math.fma(a, b.y(), c.y()),
|
||||||
|
Math.fma(a, b.z(), c.z())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static double tripleProduct(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Vec3 c) {
|
||||||
|
return a.x * b.y * c.z + a.y * b.z * c.x + a.z * b.x * c.y - c.x * b.y * a.z - c.y * b.z * a.x - c.z * b.x * a.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Vec3 plus(double x, double y, double z) {
|
||||||
|
return new Vec3(this.x + x, this.y + y, this.z + z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Vec3 minus(double x, double y, double z) {
|
||||||
|
return new Vec3(this.x - x, this.y - y, this.z - z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a vector to this vector
|
||||||
|
* @param other a vector
|
||||||
|
* @return the sum of this and the other vector
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 plus(@NotNull Vec3 other) {
|
||||||
|
return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtracts a vector from this vector
|
||||||
|
* @param other a vector
|
||||||
|
* @return the difference of this and the other vector
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 minus(@NotNull Vec3 other) {
|
||||||
|
return new Vec3(this.x - other.x, this.y - other.y, this.z - other.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the scalar product of this and another vector
|
||||||
|
* @param other a vector
|
||||||
|
* @return the scalar product
|
||||||
|
*/
|
||||||
|
public double times(@NotNull Vec3 other) {
|
||||||
|
return this.x * other.x + this.y * other.y + this.z * other.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies this vector with a scalar
|
||||||
|
* @param t a scalar
|
||||||
|
* @return the product of this vector and the scalar
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 times(double t) {
|
||||||
|
return new Vec3(this.x * t, this.y * t, this.z * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Negates this vector.
|
||||||
|
* {@return the negated vector}
|
||||||
|
*/
|
||||||
public @NotNull Vec3 neg() {
|
public @NotNull Vec3 neg() {
|
||||||
return new Vec3(-x, -y, -z);
|
return new Vec3(-x, -y, -z);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Vec3 cross(@NotNull Vec3 b) {
|
/**
|
||||||
|
* Inverts each component of this vector.
|
||||||
|
* @return the inverted vector.
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 inv() {
|
||||||
|
return new Vec3(1 / x, 1 / y, 1 / z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the cross-product of this and another vector
|
||||||
|
* @param other a vector
|
||||||
|
* @return the cross-product
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 cross(@NotNull Vec3 other) {
|
||||||
return new Vec3(
|
return new Vec3(
|
||||||
this.y() * b.z() - b.y() * this.z(),
|
Math.fma(this.y, other.z, - other.y * this.z),
|
||||||
this.z() * b.x() - b.z() * this.x(),
|
Math.fma(this.z, other.x, - other.z * this.x),
|
||||||
this.x() * b.y() - b.x() * this.y()
|
Math.fma(this.x, other.y, - other.x * this.y)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Vec3 div(double b) {
|
/**
|
||||||
return new Vec3(this.x / b, this.y / b, this.z / b);
|
* Divides this vector by a scalar
|
||||||
|
* @param t a scalar
|
||||||
|
* @return this vector divided by the scalar
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 div(double t) {
|
||||||
|
return new Vec3(this.x / t, this.y / t, this.z / t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the squared length of this vector}
|
||||||
|
*/
|
||||||
public double squared() {
|
public double squared() {
|
||||||
return this.x * this.x + this.y * this.y + this.z * this.z;
|
return this.x * this.x + this.y * this.y + this.z * this.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the length of this vector}
|
||||||
|
*/
|
||||||
public double length() {
|
public double length() {
|
||||||
return Math.sqrt(squared());
|
return Math.sqrt(squared());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return whether this vector is near zero}
|
||||||
|
*/
|
||||||
public boolean isNearZero() {
|
public boolean isNearZero() {
|
||||||
var s = 1e-8;
|
var s = 1e-8;
|
||||||
return Math.abs(x) < s && Math.abs(y) < s && Math.abs(z) < s;
|
return Math.abs(x) < s && Math.abs(y) < s && Math.abs(z) < s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return a unit vector with the same direction as this vector}
|
||||||
|
*/
|
||||||
public @NotNull Vec3 unit() {
|
public @NotNull Vec3 unit() {
|
||||||
return div(length());
|
var squared = squared();
|
||||||
|
if (squared == 1) return this;
|
||||||
|
return div(Math.sqrt(squared));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the n-th component of this vector}
|
||||||
|
* @param axis the component index
|
||||||
|
*/
|
||||||
|
public double get(int axis) {
|
||||||
|
return switch (axis) {
|
||||||
|
case 0 -> x;
|
||||||
|
case 1 -> y;
|
||||||
|
case 2 -> z;
|
||||||
|
default -> throw new IndexOutOfBoundsException(axis);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Vec3 withX(double x) {
|
public @NotNull Vec3 withX(double x) {
|
||||||
@@ -112,4 +303,9 @@ public record Vec3(double x, double y, double z) {
|
|||||||
public @NotNull Vec3 withZ(double z) {
|
public @NotNull Vec3 withZ(double z) {
|
||||||
return new Vec3(x, y, z);
|
return new Vec3(x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return "(" + x + "," + y + "," + 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}
|
||||||
@@ -16,7 +18,13 @@ public interface Camera {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Casts a ray through the given pixel.
|
* Casts a ray through the given pixel.
|
||||||
|
* @param x the pixel x coordinate
|
||||||
|
* @param y the pixel y coordinate
|
||||||
|
* @param i the subpixel x coordinate
|
||||||
|
* @param j the subpixel y coordinate
|
||||||
|
* @param n the subpixel count (per side)
|
||||||
|
* @param random a random number generator
|
||||||
* @return a new ray
|
* @return a new ray
|
||||||
*/
|
*/
|
||||||
@NotNull Ray cast(int x, int y);
|
@NotNull Ray cast(int x, int y, int i, int j, int n, @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
|
||||||
@@ -59,7 +60,7 @@ public final class SimpleCamera implements Camera {
|
|||||||
|
|
||||||
this.pixel00 = origin.plus(direction.times(builder.focusDistance))
|
this.pixel00 = origin.plus(direction.times(builder.focusDistance))
|
||||||
.minus(u.times(0.5 * viewportWidth)).minus(v.times(0.5 * viewportHeight))
|
.minus(u.times(0.5 * viewportWidth)).minus(v.times(0.5 * viewportHeight))
|
||||||
.plus(pixelU.div(2)).plus(pixelV.div(2));
|
.plus(pixelU.times(.5)).plus(pixelV.times(.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,13 +79,20 @@ public final class SimpleCamera implements Camera {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
|
* @param x {@inheritDoc}
|
||||||
|
* @param y {@inheritDoc}
|
||||||
|
* @param i {@inheritDoc}
|
||||||
|
* @param j {@inheritDoc}
|
||||||
|
* @param n {@inheritDoc}
|
||||||
|
* @param random {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
public @NotNull Ray cast(int x, int y) {
|
@Override
|
||||||
|
public @NotNull Ray cast(int x, int y, int i, int j, int n, @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, i, j, n, random);
|
||||||
return new Ray(origin, target.minus(origin));
|
return new Ray(origin, target.minus(origin));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,12 +101,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 = Math.fma(2, random.nextDouble(), -1);
|
||||||
var dv = 2 * Math.random() - 1;
|
var dv = Math.fma(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 +123,10 @@ 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, int i, int j, int n, @NotNull RandomGenerator random) {
|
||||||
double dx = x + Math.random() - 0.5;
|
var factor = 1d / n;
|
||||||
double dy = y + Math.random() - 0.5;
|
var dx = x + Math.fma(factor, i + random.nextDouble(), -0.5);
|
||||||
|
var dy = y + Math.fma(factor, j + random.nextDouble(), -0.5);
|
||||||
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
|
return pixel00.plus(pixelU.times(dx)).plus(pixelV.times(dy));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
package eu.jonahbauer.raytracing.render.canvas;
|
package eu.jonahbauer.raytracing.render.canvas;
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.render.Color;
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
package eu.jonahbauer.raytracing.render.canvas;
|
package eu.jonahbauer.raytracing.render.canvas;
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.render.Color;
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class Image implements Canvas {
|
public final class Image implements Canvas {
|
||||||
@@ -21,6 +22,16 @@ public final class Image implements Canvas {
|
|||||||
this.data = new Color[height][width];
|
this.data = new Color[height][width];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Image(@NotNull BufferedImage image) {
|
||||||
|
this(image.getWidth(), image.getHeight());
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
this.data[y][x] = new Color(image.getRGB(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getWidth() {
|
public int getWidth() {
|
||||||
return width;
|
return width;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
package eu.jonahbauer.raytracing.render.canvas;
|
package eu.jonahbauer.raytracing.render.canvas;
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.render.Color;
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
@@ -42,15 +42,19 @@ public final class LiveCanvas implements Canvas {
|
|||||||
|
|
||||||
public @NotNull Thread preview() {
|
public @NotNull Thread preview() {
|
||||||
var frame = new JFrame();
|
var frame = new JFrame();
|
||||||
frame.setSize(getWidth(), getHeight());
|
|
||||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||||
frame.setContentPane(new JPanel() {
|
frame.setContentPane(new JPanel() {
|
||||||
|
{
|
||||||
|
setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void paintComponent(Graphics g) {
|
protected void paintComponent(Graphics g) {
|
||||||
g.drawImage(image, 0, 0, null);
|
g.drawImage(image, 0, 0, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
frame.setResizable(false);
|
frame.setResizable(false);
|
||||||
|
frame.pack();
|
||||||
frame.setVisible(true);
|
frame.setVisible(true);
|
||||||
|
|
||||||
var update = Thread.ofVirtual().start(() -> {
|
var update = Thread.ofVirtual().start(() -> {
|
||||||
|
@@ -1,26 +1,38 @@
|
|||||||
package eu.jonahbauer.raytracing.material;
|
package eu.jonahbauer.raytracing.render.material;
|
||||||
|
|
||||||
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.render.Color;
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||||
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 Texture texture) implements Material {
|
||||||
|
public DielectricMaterial(double refractionIndex) {
|
||||||
|
this(refractionIndex, Color.WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DielectricMaterial {
|
||||||
|
Objects.requireNonNull(texture, "texture");
|
||||||
|
}
|
||||||
|
|
||||||
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.isFrontFace() ? (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));
|
var attenuation = texture.get(hit);
|
||||||
|
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private double reflectance(double cos) {
|
private double reflectance(double cos) {
|
@@ -0,0 +1,22 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.material;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public record DiffuseLight(@NotNull Texture texture) implements Material {
|
||||||
|
@Override
|
||||||
|
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Color emitted(@NotNull HitResult hit) {
|
||||||
|
return texture.get(hit);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,68 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.material;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public final class DirectionalMaterial implements Material {
|
||||||
|
private final @Nullable Material front;
|
||||||
|
private final @Nullable Material back;
|
||||||
|
|
||||||
|
private final @NotNull Texture texture;
|
||||||
|
|
||||||
|
public DirectionalMaterial(@Nullable Material front, @Nullable Material back) {
|
||||||
|
if (front == null && back == null) throw new IllegalArgumentException("front and back must not both be null");
|
||||||
|
this.front = front;
|
||||||
|
this.back = back;
|
||||||
|
this.texture = new DirectionalTexture(
|
||||||
|
front != null ? front.texture() : null,
|
||||||
|
back != null ? back.texture() : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Texture texture() {
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
|
||||||
|
if (hit.isFrontFace()) {
|
||||||
|
if (front != null) return front.scatter(ray, hit, random);
|
||||||
|
} else {
|
||||||
|
if (back != null) return back.scatter(ray, hit, random);
|
||||||
|
}
|
||||||
|
// let the ray pass through without obstruction
|
||||||
|
return Optional.of(new SpecularScatterResult(Color.WHITE, new Ray(ray.at(hit.t()), ray.direction())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Color emitted(@NotNull HitResult hit) {
|
||||||
|
if (hit.isFrontFace()) {
|
||||||
|
if (front != null) return front.emitted(hit);
|
||||||
|
} else {
|
||||||
|
if (back != null) return back.emitted(hit);
|
||||||
|
}
|
||||||
|
return Material.super.emitted(hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DirectionalTexture(@Nullable Texture front, @Nullable Texture back) implements Texture {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUVRequired() {
|
||||||
|
return front() != null && front().isUVRequired() || back() != null && back().isUVRequired();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.material;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.render.renderer.pdf.SphereProbabilityDensityFunction;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public record IsotropicMaterial(@NotNull Color albedo) implements Material {
|
||||||
|
@Override
|
||||||
|
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
|
||||||
|
return Optional.of(new PdfScatterResult(albedo(), new SphereProbabilityDensityFunction()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Texture texture() {
|
||||||
|
return albedo();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.material;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.render.renderer.pdf.CosineProbabilityDensityFunction;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public record LambertianMaterial(@NotNull Texture texture) implements Material {
|
||||||
|
public LambertianMaterial {
|
||||||
|
Objects.requireNonNull(texture, "texture");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
|
||||||
|
var attenuation = texture.get(hit);
|
||||||
|
return Optional.of(new PdfScatterResult(attenuation, new CosineProbabilityDensityFunction(hit.normal())));
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,71 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.material;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.render.renderer.pdf.ProbabilityDensityFunction;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public interface Material {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the texture associated with this material}
|
||||||
|
*/
|
||||||
|
@NotNull Texture texture();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scatters a light ray after it hit a surface.
|
||||||
|
* @param ray the incoming light ray
|
||||||
|
* @param hit information about the light ray hitting some object
|
||||||
|
* @param random a random number generator
|
||||||
|
* @return a {@code ScatterResult} if the ray is scattered or an {@linkplain Optional#empty() empty optional} if the
|
||||||
|
* ray is absorbed.
|
||||||
|
*/
|
||||||
|
@NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the color emitted for a given hit}
|
||||||
|
* @implSpec the default implementation returns {@linkplain Color#BLACK black}, i.e. no emission
|
||||||
|
*/
|
||||||
|
default @NotNull Color emitted(@NotNull HitResult hit) {
|
||||||
|
return Color.BLACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a {@linkplain Material#scatter(Ray, HitResult, RandomGenerator) scattering operation}.
|
||||||
|
*/
|
||||||
|
sealed interface ScatterResult {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a specular {@linkplain #scatter(Ray, HitResult, RandomGenerator) scattering operation}. A
|
||||||
|
* specular is a scattering operation with a very small number of possible scattered rays (like a
|
||||||
|
* perfect reflection which only has one possible scattered ray).
|
||||||
|
* @param attenuation the attenuation of the scattered light ray
|
||||||
|
* @param ray the scattered light ray
|
||||||
|
*/
|
||||||
|
record SpecularScatterResult(@NotNull Color attenuation, @NotNull Ray ray) implements ScatterResult {
|
||||||
|
public SpecularScatterResult {
|
||||||
|
Objects.requireNonNull(attenuation, "attenuation");
|
||||||
|
Objects.requireNonNull(ray, "ray");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a probability density function based
|
||||||
|
* {@linkplain #scatter(Ray, HitResult, RandomGenerator) scattering operation}. A probability density function
|
||||||
|
* based scattering operation uses a probability density function to determine the scatter direction.
|
||||||
|
* @param attenuation the attenuation of the scattered light ray
|
||||||
|
* @param pdf the probability density function
|
||||||
|
*/
|
||||||
|
record PdfScatterResult(@NotNull Color attenuation, @NotNull ProbabilityDensityFunction pdf) implements ScatterResult {
|
||||||
|
public PdfScatterResult {
|
||||||
|
Objects.requireNonNull(attenuation, "attenuation");
|
||||||
|
Objects.requireNonNull(pdf, "pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.material;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements Material {
|
||||||
|
|
||||||
|
public MetallicMaterial(@NotNull Texture texture) {
|
||||||
|
this(texture, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetallicMaterial {
|
||||||
|
Objects.requireNonNull(texture, "texture");
|
||||||
|
if (fuzz < 0 || !Double.isFinite(fuzz)) throw new IllegalArgumentException("fuzz must be non-negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
|
||||||
|
var newDirection = Vec3.reflect(ray.direction(), hit.normal());
|
||||||
|
if (fuzz > 0) {
|
||||||
|
newDirection = Vec3.fma(fuzz, Vec3.random(random), newDirection.unit());
|
||||||
|
}
|
||||||
|
var attenuation = texture.get(hit);
|
||||||
|
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection)));
|
||||||
|
}
|
||||||
|
}
|
@@ -2,18 +2,24 @@ package eu.jonahbauer.raytracing.render.renderer;
|
|||||||
|
|
||||||
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.render.Color;
|
import eu.jonahbauer.raytracing.render.material.Material;
|
||||||
|
import eu.jonahbauer.raytracing.render.renderer.pdf.TargetingProbabilityDensityFunction;
|
||||||
|
import eu.jonahbauer.raytracing.render.renderer.pdf.MixtureProbabilityDensityFunction;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
import eu.jonahbauer.raytracing.render.camera.Camera;
|
import eu.jonahbauer.raytracing.render.camera.Camera;
|
||||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
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;
|
|
||||||
|
import static eu.jonahbauer.raytracing.Main.DEBUG;
|
||||||
|
|
||||||
public final class SimpleRenderer implements Renderer {
|
public final class SimpleRenderer implements Renderer {
|
||||||
private final int samplesPerPixel;
|
private final int sqrtSamplesPerPixel;
|
||||||
private final int maxDepth;
|
private final int maxDepth;
|
||||||
private final double gamma;
|
private final double gamma;
|
||||||
|
|
||||||
@@ -29,7 +35,7 @@ public final class SimpleRenderer implements Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SimpleRenderer(@NotNull Builder builder) {
|
private SimpleRenderer(@NotNull Builder builder) {
|
||||||
this.samplesPerPixel = builder.samplesPerPixel;
|
this.sqrtSamplesPerPixel = (int) Math.sqrt(builder.samplesPerPixel);
|
||||||
this.maxDepth = builder.maxDepth;
|
this.maxDepth = builder.maxDepth;
|
||||||
this.gamma = builder.gamma;
|
this.gamma = builder.gamma;
|
||||||
|
|
||||||
@@ -37,6 +43,9 @@ public final class SimpleRenderer implements Renderer {
|
|||||||
this.iterative = builder.iterative;
|
this.iterative = builder.iterative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
|
public void render(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
|
||||||
if (canvas.getWidth() != camera.getWidth() || canvas.getHeight() != camera.getHeight()) {
|
if (canvas.getWidth() != camera.getWidth() || canvas.getHeight() != camera.getHeight()) {
|
||||||
@@ -44,86 +53,173 @@ public final class SimpleRenderer implements Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (iterative) {
|
if (iterative) {
|
||||||
|
renderIterative(camera, scene, canvas);
|
||||||
|
} else {
|
||||||
|
renderNonIterative(camera, scene, canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}, taking one sample per pixel at
|
||||||
|
* a time and updating the canvas after each sample.
|
||||||
|
*/
|
||||||
|
private void renderIterative(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
|
||||||
|
var random = new Random(0);
|
||||||
|
|
||||||
// render one sample after the other
|
// render one sample after the other
|
||||||
for (int i = 1 ; i <= samplesPerPixel; i++) {
|
int i = 0;
|
||||||
var sample = i;
|
for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) {
|
||||||
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> {
|
for (int si = 0; si < sqrtSamplesPerPixel; si++) {
|
||||||
var y = (int) (pixel >> 32);
|
var sample = ++i;
|
||||||
var x = (int) pixel;
|
var sif = si;
|
||||||
var ray = camera.cast(x, y);
|
var sjf = sj;
|
||||||
var c = getColor(scene, ray);
|
getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
|
||||||
|
for (int x = 0; x < camera.getWidth(); x++) {
|
||||||
|
var ray = camera.cast(x, y, sif, sjf, sqrtSamplesPerPixel, random);
|
||||||
|
var c = getColor(scene, ray, random);
|
||||||
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
|
}
|
||||||
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> {
|
|
||||||
var y = (int) (pixel >> 32);
|
|
||||||
var x = (int) pixel;
|
|
||||||
canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// render one pixel after the other
|
|
||||||
getPixelStream(camera.getWidth(), camera.getHeight(), parallel).forEach(pixel -> {
|
|
||||||
var y = (int) (pixel >> 32);
|
|
||||||
var x = (int) pixel;
|
|
||||||
|
|
||||||
|
// apply gamma correction
|
||||||
|
getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
|
||||||
|
for (int x = 0; x < camera.getWidth(); x++) {
|
||||||
|
canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}, taking some amount of samples
|
||||||
|
* per pixel and updating the canvas after each pixel.
|
||||||
|
*/
|
||||||
|
private void renderNonIterative(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
|
||||||
|
var splittable = new SplittableRandom(0);
|
||||||
|
// render one pixel after the other
|
||||||
|
getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
|
||||||
|
var random = splittable.split();
|
||||||
|
for (int x = 0; x < camera.getWidth(); x++) {
|
||||||
var color = Color.BLACK;
|
var color = Color.BLACK;
|
||||||
for (int i = 1; i <= samplesPerPixel; i++) {
|
int i = 0;
|
||||||
var ray = camera.cast(x, y);
|
for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) {
|
||||||
var c = getColor(scene, ray);
|
for (int si = 0; si < sqrtSamplesPerPixel; si++) {
|
||||||
color = Color.average(color, c, i);
|
var ray = camera.cast(x, y, si, sj, sqrtSamplesPerPixel, random);
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println("Casting ray " + ray + " through pixel (" + x + "," + y + ") at subpixel (" + si + "," + sj + ")...");
|
||||||
|
}
|
||||||
|
var c = getColor(scene, ray, random);
|
||||||
|
color = Color.average(color, c, ++i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
canvas.set(x, y, Color.gamma(color, gamma));
|
canvas.set(x, y, Color.gamma(color, gamma));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@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) {
|
||||||
if (depth <= 0) return Color.BLACK;
|
var color = Color.BLACK;
|
||||||
|
var attenuation = Color.WHITE;
|
||||||
|
|
||||||
|
while (depth-- > 0) {
|
||||||
|
var optional = scene.hit(ray);
|
||||||
|
if (optional.isEmpty()) {
|
||||||
|
var background = scene.getBackgroundColor(ray);
|
||||||
|
color = Color.add(color, Color.multiply(attenuation, background));
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println(" Hit background: " + background);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
var optional = scene.hit(ray, new Range(0.001, Double.POSITIVE_INFINITY));
|
|
||||||
if (optional.isPresent()) {
|
|
||||||
var hit = optional.get();
|
var hit = optional.get();
|
||||||
var material = hit.material();
|
if (DEBUG) {
|
||||||
return material.scatter(ray, hit)
|
System.out.println(" Hit " + hit.target() + " at t=" + hit.t() + " (" + hit.position() + ")");
|
||||||
.map(scatter -> Color.multiply(scatter.attenuation(), getColor0(scene, scatter.ray(), depth - 1)))
|
|
||||||
.orElse(Color.BLACK);
|
|
||||||
} else {
|
|
||||||
return getSkyboxColor(ray);
|
|
||||||
}
|
}
|
||||||
|
var material = hit.material();
|
||||||
|
var emitted = material.emitted(hit);
|
||||||
|
if (DEBUG && !Color.BLACK.equals(emitted)) {
|
||||||
|
System.out.println(" Emitted: " + emitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = material.scatter(ray, hit, random);
|
||||||
|
color = Color.add(color, Color.multiply(attenuation, emitted));
|
||||||
|
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println(" Absorbed");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (result.get()) {
|
||||||
|
case Material.SpecularScatterResult(var a, var scattered) -> {
|
||||||
|
attenuation = Color.multiply(attenuation, a);
|
||||||
|
ray = scattered;
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println(" Specular scattering with albedo " + a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Material.PdfScatterResult(var a, var pdf) -> {
|
||||||
|
if (scene.getTargets() == null) {
|
||||||
|
attenuation = Color.multiply(attenuation, a);
|
||||||
|
ray = new Ray(hit.position(), pdf.generate(random));
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println(" Pdf scattering with albedo " + a);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var mixed = new MixtureProbabilityDensityFunction(new TargetingProbabilityDensityFunction(hit.position(), scene.getTargets()), pdf, 0.5);
|
||||||
|
var direction = mixed.generate(random).unit();
|
||||||
|
|
||||||
|
var idealPdf = pdf.value(direction);
|
||||||
|
var actualPdf = mixed.value(direction);
|
||||||
|
if (actualPdf == 0) break; // when actualPdf is 0, the ray should have never been generated by mixed.generate
|
||||||
|
|
||||||
|
var factor = idealPdf / actualPdf;
|
||||||
|
|
||||||
|
attenuation = Color.multiply(attenuation, Color.multiply(a, factor));
|
||||||
|
ray = new Ray(hit.position(), direction);
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println(" Pdf scattering with albedo " + a + " and factor " + factor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println(" Combined color is " + color);
|
||||||
|
System.out.println(" Combined attenuation is " + attenuation);
|
||||||
|
System.out.println(" New ray is " + ray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
System.out.println(" Final color is " + color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@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 {
|
public static class Builder {
|
||||||
private int samplesPerPixel = 100;
|
private int samplesPerPixel = 100;
|
||||||
private int maxDepth = 10;
|
private int maxDepth = 10;
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.renderer.pdf;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public record CosineProbabilityDensityFunction(@NotNull Vec3 normal) implements ProbabilityDensityFunction {
|
||||||
|
|
||||||
|
public CosineProbabilityDensityFunction {
|
||||||
|
Objects.requireNonNull(normal, "normal");
|
||||||
|
normal = normal.unit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double value(@NotNull Vec3 direction) {
|
||||||
|
var cos = normal.times(direction);
|
||||||
|
return Math.max(0, cos / Math.PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 generate(@NotNull RandomGenerator random) {
|
||||||
|
var out = normal().plus(Vec3.random(random));
|
||||||
|
return out.isNearZero() ? normal() : out;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,46 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.renderer.pdf;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mixes between two probability density functions (pdf) using a weight. When the weight is closer to zero, the
|
||||||
|
* influence of the second pdf is stronger. When the weight is closer to one, the influence of the first pdf is stronger.
|
||||||
|
* @param a the first probability density function
|
||||||
|
* @param b the second probability density function
|
||||||
|
* @param weight a weight in the range [0, 1]
|
||||||
|
*/
|
||||||
|
public record MixtureProbabilityDensityFunction(
|
||||||
|
@NotNull ProbabilityDensityFunction a,
|
||||||
|
@NotNull ProbabilityDensityFunction b,
|
||||||
|
double weight
|
||||||
|
) implements ProbabilityDensityFunction {
|
||||||
|
public MixtureProbabilityDensityFunction(@NotNull ProbabilityDensityFunction a, @NotNull ProbabilityDensityFunction b) {
|
||||||
|
this(a, b, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MixtureProbabilityDensityFunction {
|
||||||
|
Objects.requireNonNull(a);
|
||||||
|
Objects.requireNonNull(b);
|
||||||
|
weight = Math.clamp(weight, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double value(@NotNull Vec3 direction) {
|
||||||
|
var v = a.value(direction);
|
||||||
|
var w = b.value(direction);
|
||||||
|
return Math.fma(weight, v, Math.fma(-weight, w, w));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 generate(@NotNull RandomGenerator random) {
|
||||||
|
if (random.nextDouble() < weight) {
|
||||||
|
return a.generate(random);
|
||||||
|
} else {
|
||||||
|
return b.generate(random);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.renderer.pdf;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A probability density function used for sampling random directions when scattering a ray.
|
||||||
|
*/
|
||||||
|
public interface ProbabilityDensityFunction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the value of this probability density function at the given point}
|
||||||
|
* @param direction the direction
|
||||||
|
*/
|
||||||
|
double value(@NotNull Vec3 direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random direction based on this probability density function.
|
||||||
|
* @param random a random number generator
|
||||||
|
* @return the random direction
|
||||||
|
*/
|
||||||
|
@NotNull Vec3 generate(@NotNull RandomGenerator random);
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.renderer.pdf;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A probability density function sampling the sphere uniformly.
|
||||||
|
*/
|
||||||
|
public record SphereProbabilityDensityFunction() implements ProbabilityDensityFunction {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double value(@NotNull Vec3 direction) {
|
||||||
|
return 1 / (4 * Math.PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 generate(@NotNull RandomGenerator random) {
|
||||||
|
return Vec3.random(random);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.renderer.pdf;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Target;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A probability density function targeting a target.
|
||||||
|
* @see Target
|
||||||
|
*/
|
||||||
|
public final class TargetingProbabilityDensityFunction implements ProbabilityDensityFunction {
|
||||||
|
private final @NotNull Vec3 origin;
|
||||||
|
private final @NotNull List<@NotNull Target> targets;
|
||||||
|
|
||||||
|
public TargetingProbabilityDensityFunction(@NotNull Vec3 origin, @NotNull List<@NotNull Target> targets) {
|
||||||
|
this.origin = Objects.requireNonNull(origin, "origin");
|
||||||
|
this.targets = new ArrayList<>(targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double value(@NotNull Vec3 direction) {
|
||||||
|
var weight = 1d / targets.size();
|
||||||
|
var sum = 0.0;
|
||||||
|
|
||||||
|
for (var target : targets) {
|
||||||
|
sum = Math.fma(weight, target.getProbabilityDensity(origin, direction), sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 generate(@NotNull RandomGenerator random) {
|
||||||
|
return targets.get(random.nextInt(targets.size())).getTargetingDirection(origin, random);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.texture;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public record CheckerTexture(double scale, @NotNull Texture even, @NotNull Texture odd) implements Texture {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||||
|
var x = (int) Math.floor(p.x() / scale);
|
||||||
|
var y = (int) Math.floor(p.y() / scale);
|
||||||
|
var z = (int) Math.floor(p.z() / scale);
|
||||||
|
var even = (x + y + z) % 2 == 0;
|
||||||
|
return even ? even().get(u, v, p) : odd().get(u, v, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUVRequired() {
|
||||||
|
return even.isUVRequired() || odd.isUVRequired();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,17 @@
|
|||||||
package eu.jonahbauer.raytracing.render;
|
package eu.jonahbauer.raytracing.render.texture;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.scene.SkyBox;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
public record Color(double r, double g, double b) {
|
import static eu.jonahbauer.raytracing.Main.DEBUG;
|
||||||
|
|
||||||
|
public record Color(double r, double g, double b) implements Texture, SkyBox {
|
||||||
public static final @NotNull Color BLACK = new Color(0.0, 0.0, 0.0);
|
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 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 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 GREEN = new Color(0.0, 1.0, 0.0);
|
||||||
public static final @NotNull Color BLUE = new Color(0.0, 0.0, 1.0);
|
public static final @NotNull Color BLUE = new Color(0.0, 0.0, 1.0);
|
||||||
@@ -26,6 +30,14 @@ public record Color(double r, double g, double b) {
|
|||||||
return new Color(a.r() * b.r(), a.g() * b.g(), a.b() * b.b());
|
return new Color(a.r() * b.r(), a.g() * b.g(), a.b() * b.b());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @NotNull Color multiply(@NotNull Color a, double b) {
|
||||||
|
return new Color(a.r() * b, a.g() * b, a.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) {
|
public static @NotNull Color random(@NotNull Random random) {
|
||||||
return new Color(random.nextDouble(), random.nextDouble(), random.nextDouble());
|
return new Color(random.nextDouble(), random.nextDouble(), random.nextDouble());
|
||||||
}
|
}
|
||||||
@@ -40,10 +52,11 @@ public record Color(double r, double g, double b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static @NotNull Color average(@NotNull Color current, @NotNull Color next, int index) {
|
public static @NotNull Color average(@NotNull Color current, @NotNull Color next, int index) {
|
||||||
|
var factor = 1d / index;
|
||||||
return new Color(
|
return new Color(
|
||||||
current.r() + (next.r() - current.r()) / index,
|
Math.fma(factor, next.r() - current.r(), current.r()),
|
||||||
current.g() + (next.g() - current.g()) / index,
|
Math.fma(factor, next.g() - current.g(), current.g()),
|
||||||
current.b() + (next.b() - current.b()) / index
|
Math.fma(factor, next.b() - current.b(), current.b())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,25 +78,53 @@ public record Color(double r, double g, double b) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color {
|
public Color(int rgb) {
|
||||||
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1) {
|
this((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF);
|
||||||
throw new IllegalArgumentException("r, g and b must be in the range 0 to 1");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color(int red, int green, int blue) {
|
public Color(int red, int green, int blue) {
|
||||||
this(red / 255f, green / 255f, blue / 255f);
|
this(red / 255f, green / 255f, blue / 255f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Color {
|
||||||
|
if (DEBUG) {
|
||||||
|
if (!Double.isFinite(r) || !Double.isFinite(g) || !Double.isFinite(b)) {
|
||||||
|
throw new IllegalArgumentException("r, g and b must be finite");
|
||||||
|
}
|
||||||
|
if (r < 0 || g < 0 || b < 0) {
|
||||||
|
throw new IllegalArgumentException("r, g and b must be non-negative");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public int red() {
|
public int red() {
|
||||||
return (int) (255.99 * r);
|
return toInt(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int green() {
|
public int green() {
|
||||||
return (int) (255.99 * g);
|
return toInt(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int blue() {
|
public int blue() {
|
||||||
return (int) (255.99 * b);
|
return toInt(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Color getColor(@NotNull Ray ray) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUVRequired() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int toInt(double value) {
|
||||||
|
return Math.clamp((int) (255.99 * value), 0, 255);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,43 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.texture;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.canvas.Image;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record ImageTexture(@NotNull Image image) implements Texture {
|
||||||
|
|
||||||
|
public ImageTexture {
|
||||||
|
Objects.requireNonNull(image, "image");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageTexture(@NotNull BufferedImage image) {
|
||||||
|
this(new Image(image));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageTexture(@NotNull String path) {
|
||||||
|
this(read(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull BufferedImage read(@NotNull String path) {
|
||||||
|
try (var in = Objects.requireNonNull(ImageTexture.class.getResourceAsStream(path))) {
|
||||||
|
return ImageIO.read(in);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new UncheckedIOException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||||
|
u = Math.clamp(u, 0, 1);
|
||||||
|
v = 1 - Math.clamp(v, 0, 1);
|
||||||
|
int x = (int) (u * (image.getWidth() - 1));
|
||||||
|
int y = (int) (v * (image.getHeight() - 1));
|
||||||
|
return image.get(x, y);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,152 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.texture;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.function.DoubleFunction;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public final class PerlinTexture implements Texture {
|
||||||
|
private static final int POINT_COUNT = 256;
|
||||||
|
private static final @NotNull Random RANDOM = new Random();
|
||||||
|
private static final @NotNull DoubleFunction<Color> GREYSCALE = t -> new Color(t, t, t);
|
||||||
|
|
||||||
|
private final double scale;
|
||||||
|
private final int turbulence;
|
||||||
|
private final @NotNull DoubleFunction<Color> color;
|
||||||
|
|
||||||
|
private final int mask;
|
||||||
|
private final Vec3[] randvec;
|
||||||
|
private final int[] permX;
|
||||||
|
private final int[] permY;
|
||||||
|
private final int[] permZ;
|
||||||
|
|
||||||
|
public PerlinTexture() {
|
||||||
|
this(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PerlinTexture(double scale) {
|
||||||
|
this(scale, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PerlinTexture(double scale, int turbulence) {
|
||||||
|
this(scale, turbulence, GREYSCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PerlinTexture(double scale, int turbulence, @NotNull DoubleFunction<Color> color) {
|
||||||
|
this(scale, turbulence, color, POINT_COUNT, RANDOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PerlinTexture(
|
||||||
|
double scale, int turbulence, @NotNull DoubleFunction<Color> color,
|
||||||
|
int count, @NotNull RandomGenerator random
|
||||||
|
) {
|
||||||
|
if ((count & (count - 1)) != 0) throw new IllegalArgumentException("count must be a power of two");
|
||||||
|
if (turbulence <= 0) throw new IllegalArgumentException("turbulence must be positive");
|
||||||
|
|
||||||
|
this.scale = scale;
|
||||||
|
this.turbulence = turbulence;
|
||||||
|
this.color = Objects.requireNonNull(color, "color");
|
||||||
|
|
||||||
|
this.mask = count - 1;
|
||||||
|
this.randvec = new Vec3[count];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
this.randvec[i] = Vec3.random(random);
|
||||||
|
}
|
||||||
|
this.permX = generatePerm(count, random);
|
||||||
|
this.permY = generatePerm(count, random);
|
||||||
|
this.permZ = generatePerm(count, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int @NotNull[] generatePerm(int count, @NotNull RandomGenerator random) {
|
||||||
|
int[] p = new int[count];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
p[i] = i;
|
||||||
|
}
|
||||||
|
permutate(p, random);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void permutate(int @NotNull[] p, @NotNull RandomGenerator random) {
|
||||||
|
for (int i = p.length - 1; i > 0; i--) {
|
||||||
|
int target = random.nextInt(i);
|
||||||
|
int tmp = p[i];
|
||||||
|
p[i] = p[target];
|
||||||
|
p[target] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getNoise(@NotNull Vec3 p) {
|
||||||
|
var x = p.x() * scale;
|
||||||
|
var y = p.y() * scale;
|
||||||
|
var z = p.z() * scale;
|
||||||
|
|
||||||
|
var u = x - Math.floor(x);
|
||||||
|
var v = y - Math.floor(y);
|
||||||
|
var w = z - Math.floor(z);
|
||||||
|
|
||||||
|
int i = (int) Math.floor(x);
|
||||||
|
int j = (int) Math.floor(y);
|
||||||
|
int k = (int) Math.floor(z);
|
||||||
|
|
||||||
|
var c = new Vec3[8];
|
||||||
|
for (int di = 0; di < 2; di++) {
|
||||||
|
for (int dj = 0; dj < 2; dj++) {
|
||||||
|
for (int dk = 0; dk < 2; dk++) {
|
||||||
|
c[di << 2 | dj << 1 | dk] = randvec[permX[(i + di) & mask] ^ permY[(j + dj) & mask] ^ permZ[(k + dk) & mask]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return interpolate(c, u, v, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getNoise(@NotNull Vec3 p, int depth) {
|
||||||
|
var accum = 0.0;
|
||||||
|
var temp = p;
|
||||||
|
var weight = 1.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < depth; i++) {
|
||||||
|
accum = Math.fma(weight, getNoise(temp), accum);
|
||||||
|
weight *= 0.5;
|
||||||
|
temp = temp.times(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||||
|
var noise = getNoise(p, turbulence);
|
||||||
|
var t = Math.fma(0.5, Math.sin(Math.PI * noise), 0.5);
|
||||||
|
return color.apply(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double interpolate(Vec3[] c, double u, double v, double w) {
|
||||||
|
var uu = u * u * Math.fma(-2, u, 3);
|
||||||
|
var vv = v * v * Math.fma(-2, v, 3);
|
||||||
|
var ww = w * w * Math.fma(-2, w, 3);
|
||||||
|
|
||||||
|
var accum = 0.0;
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
for (int j = 0; j < 2; j++) {
|
||||||
|
for (int k = 0; k < 2; k++) {
|
||||||
|
var vec = c[i << 2 | j << 1 | k];
|
||||||
|
var dot = (u - i) * vec.x() + (v - j) * vec.y() + (w - k) * vec.z();
|
||||||
|
accum += Math.fma(i, uu, (1 - i) * (1 - uu))
|
||||||
|
* Math.fma(j, vv, (1 - j) * (1 - vv))
|
||||||
|
* Math.fma(k, ww, (1 - k) * (1 - ww))
|
||||||
|
* dot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accum;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUVRequired() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.jonahbauer.raytracing.render.texture;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public interface Texture {
|
||||||
|
/**
|
||||||
|
* {@return the color of <code>this</code> texture for a hit}
|
||||||
|
*/
|
||||||
|
default @NotNull Color get(@NotNull HitResult hit) {
|
||||||
|
return get(hit.u(), hit.v(), hit.position());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the color of <code>this</code> texture at the specified position}
|
||||||
|
* @param u the texture u coordinate
|
||||||
|
* @param v the texture v coordinate
|
||||||
|
* @param p the position
|
||||||
|
*/
|
||||||
|
@NotNull Color get(double u, double v, @NotNull Vec3 p);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether {@link #get(double, double, Vec3)} uses the {@code u} and/or {@code v} parameters.
|
||||||
|
* When a texture indicates that the {@code u} and {@code v} coordinates are not required, the calculation may be
|
||||||
|
* skipped and {@link Double#NaN} will be passed.
|
||||||
|
* @return whether {@link #get(double, double, Vec3)} uses the {@code u} and/or {@code v} parameters
|
||||||
|
*/
|
||||||
|
default boolean isUVRequired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,24 +1,38 @@
|
|||||||
package eu.jonahbauer.raytracing.scene;
|
package eu.jonahbauer.raytracing.scene;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Range;
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
import eu.jonahbauer.raytracing.math.Vec3;
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
import eu.jonahbauer.raytracing.material.Material;
|
import eu.jonahbauer.raytracing.render.material.Material;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a {@linkplain Hittable#hit(Ray, Range) hit}.
|
||||||
|
* @param t the {@code t} value at which the hit occurs
|
||||||
|
* @param position the position of the hit
|
||||||
|
* @param normal the surface normal at the hit position
|
||||||
|
* @param target the hit target (for debug purposes only)
|
||||||
|
* @param material the material of the surface
|
||||||
|
* @param u the texture u coordinate (or {@code Double.NaN} if the {@linkplain Material#texture() material's texture} does {@linkplain Texture#isUVRequired() not depend} on the uv-coordinates)
|
||||||
|
* @param v the texture v coordinate (or {@code Double.NaN} if the {@linkplain Material#texture() material's texture} does {@linkplain Texture#isUVRequired() not depend} on the uv-coordinates)
|
||||||
|
* @param isFrontFace whether the front or the back of the surface was it
|
||||||
|
*/
|
||||||
public record HitResult(
|
public record HitResult(
|
||||||
double t,
|
double t, @NotNull Vec3 position, @NotNull Vec3 normal, @NotNull Hittable target,
|
||||||
@NotNull Vec3 position,
|
@NotNull Material material, double u, double v, boolean isFrontFace
|
||||||
@NotNull Vec3 normal,
|
|
||||||
@NotNull Material material,
|
|
||||||
boolean frontFace
|
|
||||||
) implements Comparable<HitResult> {
|
) implements Comparable<HitResult> {
|
||||||
public HitResult {
|
public HitResult {
|
||||||
if (t < 0 || !Double.isFinite(t)) throw new IllegalArgumentException("t must be non-negative");
|
|
||||||
Objects.requireNonNull(position, "position");
|
Objects.requireNonNull(position, "position");
|
||||||
normal = normal.unit();
|
normal = normal.unit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @NotNull HitResult withPositionAndNormal(@NotNull Vec3 position, @NotNull Vec3 normal) {
|
||||||
|
return new HitResult(t, position, normal, target, material, u, v, isFrontFace);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(@NotNull HitResult o) {
|
public int compareTo(@NotNull HitResult o) {
|
||||||
return Double.compare(t, o.t);
|
return Double.compare(t, o.t);
|
||||||
|
@@ -1,22 +1,48 @@
|
|||||||
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.scene.transform.RotateY;
|
||||||
|
import eu.jonahbauer.raytracing.scene.transform.Translate;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface Hittable {
|
public interface Hittable {
|
||||||
|
@NotNull Range FORWARD = new Range(0.001, Double.POSITIVE_INFINITY);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@return the value <code>t</code> such that <code>ray.at(t)</code> is the intersection of this shaped closest to
|
* @see #hit(Ray, Range)
|
||||||
* the ray origin, or <code>Double.NaN</code> if the ray does not intersect this shape}
|
*/
|
||||||
|
default @NotNull Optional<HitResult> hit(@NotNull Ray ray) {
|
||||||
|
return hit(ray, FORWARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the {@code ray} intersects {@code this} hittable.
|
||||||
|
* <p>
|
||||||
|
* The second parameter {@code range} allows the implementation to skip unnecessary calculations if it can
|
||||||
|
* determine that a hit (if any) will fall outside the valid range of {@code t}s. The returned hit may still be
|
||||||
|
* outside the valid range and has to be checked by the caller.
|
||||||
* @param ray a ray
|
* @param ray a ray
|
||||||
|
* @param range the range of valid {@code t}s
|
||||||
|
* @return the result of the hit test, containing (among others) the value {@code t} such that {@code ray.at(t)} is
|
||||||
|
* a point on {@code this} 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() {
|
/**
|
||||||
return Optional.empty();
|
* {@return the axis-aligned bounding box of this hittable}
|
||||||
|
*/
|
||||||
|
@NotNull AABB getBoundingBox();
|
||||||
|
|
||||||
|
default @NotNull Hittable translate(@NotNull Vec3 offset) {
|
||||||
|
return Translate.create(this, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
default @NotNull Hittable rotateY(double angle) {
|
||||||
|
return RotateY.create(this, angle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,31 +1,60 @@
|
|||||||
package eu.jonahbauer.raytracing.scene;
|
package eu.jonahbauer.raytracing.scene;
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.math.Range;
|
import eu.jonahbauer.raytracing.math.AABB;
|
||||||
import eu.jonahbauer.raytracing.math.Ray;
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree;
|
||||||
|
import eu.jonahbauer.raytracing.scene.util.HittableCollection;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Objects;
|
||||||
|
|
||||||
public record Scene(@NotNull List<@NotNull Hittable> objects) implements Hittable {
|
public final class Scene extends HittableCollection {
|
||||||
|
private final @NotNull HittableCollection objects;
|
||||||
|
private final @NotNull SkyBox background;
|
||||||
|
|
||||||
public Scene {
|
private final @Nullable List<@NotNull Target> targets;
|
||||||
objects = List.copyOf(objects);
|
|
||||||
|
public Scene(@NotNull List<? extends @NotNull Hittable> objects) {
|
||||||
|
this(objects, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Scene(@NotNull Hittable @NotNull ... objects) {
|
public Scene(@NotNull List<? extends @NotNull Hittable> objects, @Nullable List<? extends @NotNull Target> targets) {
|
||||||
this(List.of(objects));
|
this(Color.BLACK, objects, targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects) {
|
||||||
var result = (HitResult) null;
|
this(background, objects, null);
|
||||||
for (var object : objects) {
|
|
||||||
var r = object.hit(ray, range);
|
|
||||||
if (r.isPresent() && range.surrounds(r.get().t())) {
|
|
||||||
result = r.get();
|
|
||||||
range = new Range(range.min(), result.t());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects, @Nullable List<? extends @NotNull Target> targets) {
|
||||||
|
var list = new ArrayList<Hittable>(objects.size() + (targets != null ? targets.size() : 0));
|
||||||
|
list.addAll(objects);
|
||||||
|
if (targets != null) list.addAll(targets);
|
||||||
|
this.objects = new HittableBinaryTree(list);
|
||||||
|
this.background = Objects.requireNonNull(background);
|
||||||
|
|
||||||
|
this.targets = targets != null ? List.copyOf(targets) : null;
|
||||||
}
|
}
|
||||||
return Optional.ofNullable(result);
|
|
||||||
|
@Override
|
||||||
|
public void hit(@NotNull Ray ray, @NotNull State state) {
|
||||||
|
objects.hit(ray, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull AABB getBoundingBox() {
|
||||||
|
return objects.getBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable List<@NotNull Target> getTargets() {
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Color getBackgroundColor(@NotNull Ray ray) {
|
||||||
|
return background.getColor(ray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -1,67 +0,0 @@
|
|||||||
package eu.jonahbauer.raytracing.scene;
|
|
||||||
|
|
||||||
import eu.jonahbauer.raytracing.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 org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public record Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) implements Hittable {
|
|
||||||
|
|
||||||
public Sphere {
|
|
||||||
Objects.requireNonNull(center, "center");
|
|
||||||
Objects.requireNonNull(material, "material");
|
|
||||||
if (radius <= 0 || !Double.isFinite(radius)) throw new IllegalArgumentException("radius must be positive");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Sphere(double x, double y, double z, double r, @NotNull Material material) {
|
|
||||||
this(new Vec3(x, y, z), r, material);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
|
||||||
var oc = ray.origin().minus(center());
|
|
||||||
|
|
||||||
var a = ray.direction().squared();
|
|
||||||
var h = ray.direction().times(oc);
|
|
||||||
var c = oc.squared() - radius * radius;
|
|
||||||
|
|
||||||
var discriminant = h * h - a * c;
|
|
||||||
if (discriminant < 0) return Optional.empty();
|
|
||||||
|
|
||||||
var sd = Math.sqrt(discriminant);
|
|
||||||
|
|
||||||
double t = (- h - sd) / a;
|
|
||||||
if (!range.surrounds(t)) t = (- h + sd) / a;
|
|
||||||
if (!range.surrounds(t)) return Optional.empty();
|
|
||||||
|
|
||||||
var position = ray.at(t);
|
|
||||||
var normal = position.minus(center);
|
|
||||||
var frontFace = normal.times(ray.direction()) < 0;
|
|
||||||
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.times(-1), material, frontFace));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull Optional<BoundingBox> getBoundingBox() {
|
|
||||||
return Optional.of(new BoundingBox(
|
|
||||||
center.minus(radius, radius, radius),
|
|
||||||
center.plus(radius, radius, radius)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Sphere withCenter(@NotNull Vec3 center) {
|
|
||||||
return new Sphere(center, radius, material);
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Sphere withCenter(double x, double y, double z) {
|
|
||||||
return withCenter(new Vec3(x, y, z));
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Sphere withRadius(double radius) {
|
|
||||||
return new Sphere(center, radius, material);
|
|
||||||
}
|
|
||||||
}
|
|
39
src/main/java/eu/jonahbauer/raytracing/scene/Target.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.renderer.pdf.TargetingProbabilityDensityFunction;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for objects that can be targeted. A target can construct randomly distributed directions in which
|
||||||
|
* it will be hit from a given origin.
|
||||||
|
* @see TargetingProbabilityDensityFunction
|
||||||
|
*/
|
||||||
|
public interface Target extends Hittable {
|
||||||
|
/**
|
||||||
|
* Returns the probability density for a direction as sampled by {@link #getTargetingDirection(Vec3, RandomGenerator)}.
|
||||||
|
* @param origin the origin
|
||||||
|
* @param direction the direction
|
||||||
|
* @return the probability density for a direction as sampled by {@link #getTargetingDirection(Vec3, RandomGenerator)}
|
||||||
|
*/
|
||||||
|
double getProbabilityDensity(@NotNull Vec3 origin, @NotNull Vec3 direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return a vector targeting this hittable from the <code>origin</code>} The vector is chosen randomly.
|
||||||
|
* @param origin the origin
|
||||||
|
* @param random a random number generator
|
||||||
|
*/
|
||||||
|
@NotNull Vec3 getTargetingDirection(@NotNull Vec3 origin, @NotNull RandomGenerator random);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default @NotNull Target translate(@NotNull Vec3 offset) {
|
||||||
|
return (Target) Hittable.super.translate(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default @NotNull Target rotateY(double angle) {
|
||||||
|
return (Target) Hittable.super.rotateY(angle);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.hittable2d;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.AABB;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.material.Material;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public final class Ellipse extends Hittable2D {
|
||||||
|
private final @NotNull AABB bbox;
|
||||||
|
|
||||||
|
public Ellipse(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
|
||||||
|
super(origin, u, v, material);
|
||||||
|
this.bbox = new AABB(origin.minus(u).minus(v), origin.plus(u).plus(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isInterior(double alpha, double beta) {
|
||||||
|
return alpha * alpha + beta * beta < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull AABB getBoundingBox() {
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,91 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
var alpha = w.times(p.cross(v));
|
||||||
|
var beta = w.times(u.cross(p));
|
||||||
|
if (!isInterior(alpha, beta)) return Optional.empty();
|
||||||
|
|
||||||
|
var frontFace = denominator < 0;
|
||||||
|
return Optional.of(new HitResult(
|
||||||
|
t, position, frontFace ? normal : normal.neg(), this,
|
||||||
|
material, alpha, beta, frontFace
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected double hit0(@NotNull Ray ray, @NotNull Range range) {
|
||||||
|
var denominator = ray.direction().times(normal);
|
||||||
|
if (Math.abs(denominator) < 1e-8) return Double.NaN; // parallel
|
||||||
|
|
||||||
|
var t = (d - ray.origin().times(normal)) / denominator;
|
||||||
|
if (!range.surrounds(t)) return Double.NaN;
|
||||||
|
|
||||||
|
var position = ray.at(t);
|
||||||
|
var p = position.minus(origin);
|
||||||
|
|
||||||
|
var alpha = Vec3.tripleProduct(w, p, v);
|
||||||
|
var beta = Vec3.tripleProduct(w, u, p);
|
||||||
|
if (!isInterior(alpha, beta)) return Double.NaN;
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @NotNull Vec3 get(double alpha, double beta) {
|
||||||
|
return new Vec3(
|
||||||
|
Math.fma(beta, v.x(), Math.fma(alpha, u.x(), origin.x())),
|
||||||
|
Math.fma(beta, v.y(), Math.fma(alpha, u.y(), origin.y())),
|
||||||
|
Math.fma(beta, v.z(), Math.fma(alpha, u.z(), origin.z()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract boolean isInterior(double alpha, double beta);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return this.getClass().getSimpleName() + "(origin=" + origin + ", u=" + u + ", v=" + v + ")";
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,50 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.hittable2d;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.AABB;
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.material.Material;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Target;
|
||||||
|
import eu.jonahbauer.raytracing.scene.util.PdfUtil;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public final class Parallelogram extends Hittable2D implements Target {
|
||||||
|
private final @NotNull AABB bbox;
|
||||||
|
|
||||||
|
public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
|
||||||
|
super(origin, u, v, material);
|
||||||
|
this.bbox = new AABB(origin, origin.plus(u).plus(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isInterior(double alpha, double beta) {
|
||||||
|
return 0 <= alpha && alpha < 1 && 0 <= beta && beta < 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull AABB getBoundingBox() {
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getProbabilityDensity(@NotNull Vec3 origin, @NotNull Vec3 direction) {
|
||||||
|
if (Double.isNaN(hit0(new Ray(origin, direction), FORWARD))) return 0;
|
||||||
|
|
||||||
|
var o = this.origin.minus(origin);
|
||||||
|
var a = o.unit();
|
||||||
|
var b = o.plus(u).unit();
|
||||||
|
var c = o.plus(v).unit();
|
||||||
|
var d = o.plus(u).plus(v).unit();
|
||||||
|
var angle = PdfUtil.getSolidAngle(a, b, d) + PdfUtil.getSolidAngle(c, b, d);
|
||||||
|
return 1 / angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 getTargetingDirection(@NotNull Vec3 origin, @NotNull RandomGenerator random) {
|
||||||
|
var alpha = random.nextDouble();
|
||||||
|
var beta = random.nextDouble();
|
||||||
|
return get(alpha, beta).minus(origin);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.hittable2d;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.AABB;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.render.material.Material;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public final class Triangle extends Hittable2D {
|
||||||
|
private final @NotNull AABB bbox;
|
||||||
|
|
||||||
|
public Triangle(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
|
||||||
|
super(origin, u, v, material);
|
||||||
|
this.bbox = new AABB(origin, origin.plus(u).plus(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isInterior(double alpha, double beta) {
|
||||||
|
return 0 <= alpha && 0 <= beta && alpha + beta <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull AABB getBoundingBox() {
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
}
|
304
src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Box.java
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.hittable3d;
|
||||||
|
|
||||||
|
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.render.material.Material;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Target;
|
||||||
|
import eu.jonahbauer.raytracing.scene.util.PdfUtil;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public final class Box implements Hittable, Target {
|
||||||
|
private final @NotNull AABB box;
|
||||||
|
private final @Nullable Material @NotNull[] materials;
|
||||||
|
|
||||||
|
public Box(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Material material) {
|
||||||
|
this(new AABB(a, b), material);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Box(@NotNull AABB box, @NotNull Material material) {
|
||||||
|
this(box, Objects.requireNonNull(material, "material"), material, material, material, material, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Box(
|
||||||
|
@NotNull Vec3 a, @NotNull Vec3 b,
|
||||||
|
@Nullable Material top, @Nullable Material bottom,
|
||||||
|
@Nullable Material left, @Nullable Material right,
|
||||||
|
@Nullable Material front, @Nullable Material back
|
||||||
|
) {
|
||||||
|
this(new AABB(a, b), top, bottom, left, right, front, back);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Box(
|
||||||
|
@NotNull AABB box,
|
||||||
|
@Nullable Material top, @Nullable Material bottom,
|
||||||
|
@Nullable Material left, @Nullable Material right,
|
||||||
|
@Nullable Material front, @Nullable Material back
|
||||||
|
) {
|
||||||
|
this.box = Objects.requireNonNull(box, "box");
|
||||||
|
this.materials = new Material[] { left, bottom, back, right, top, front };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
||||||
|
// based on AABB#hit with additional detection of the side hit
|
||||||
|
var origin = ray.origin();
|
||||||
|
var direction = ray.direction();
|
||||||
|
var invDirection = direction.inv();
|
||||||
|
|
||||||
|
var tmin = AABB.intersect(box.min(), origin, invDirection);
|
||||||
|
var tmax = AABB.intersect(box.max(), origin, invDirection);
|
||||||
|
|
||||||
|
double tlmax = Double.NEGATIVE_INFINITY;
|
||||||
|
double tumin = Double.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
Side entry = null;
|
||||||
|
Side exit = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (direction.get(i) >= 0) {
|
||||||
|
if (tmin[i] > tlmax) {
|
||||||
|
tlmax = tmin[i];
|
||||||
|
entry = Side.NEGATIVE[i];
|
||||||
|
}
|
||||||
|
if (tmax[i] < tumin) {
|
||||||
|
tumin = tmax[i];
|
||||||
|
exit = Side.POSITIVE[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (tmax[i] > tlmax) {
|
||||||
|
tlmax = tmax[i];
|
||||||
|
entry = Side.POSITIVE[i];
|
||||||
|
}
|
||||||
|
if (tmin[i] < tumin) {
|
||||||
|
tumin = tmin[i];
|
||||||
|
exit = Side.NEGATIVE[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tlmax < tumin && tumin >= range.min() && tlmax <= range.max()) {
|
||||||
|
assert entry != null && exit != null;
|
||||||
|
return hit0(tlmax, tumin, entry, exit, ray, range);
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull Optional<HitResult> hit0(double tmin, double tmax, @NotNull Side entry, @NotNull Side exit, @NotNull Ray ray, @NotNull Range range) {
|
||||||
|
double t;
|
||||||
|
Side side;
|
||||||
|
boolean frontFace;
|
||||||
|
Material material;
|
||||||
|
Vec3 normal;
|
||||||
|
if (range.surrounds(tmin) && materials[entry.ordinal()] != null) {
|
||||||
|
t = tmin;
|
||||||
|
side = entry;
|
||||||
|
frontFace = true;
|
||||||
|
material = materials[side.ordinal()];
|
||||||
|
normal = side.normal;
|
||||||
|
} else if (range.surrounds(tmax) && materials[exit.ordinal()] != null) {
|
||||||
|
t = tmax;
|
||||||
|
side = exit;
|
||||||
|
frontFace = false;
|
||||||
|
material = materials[side.ordinal()];
|
||||||
|
normal = side.normal.neg();
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
var position = ray.at(t);
|
||||||
|
var uv = material.texture().isUVRequired();
|
||||||
|
var u = uv ? side.getTextureU(box, position) : Double.NaN;
|
||||||
|
var v = uv ? side.getTextureV(box, position) : Double.NaN;
|
||||||
|
return Optional.of(new HitResult(t, position, normal, this, material, u, v, frontFace));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull AABB getBoundingBox() {
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getProbabilityDensity(@NotNull Vec3 origin, @NotNull Vec3 direction) {
|
||||||
|
if (contains(origin)) return 1 / (4 * Math.PI);
|
||||||
|
if (hit(new Ray(origin, direction)).isEmpty()) return 0;
|
||||||
|
|
||||||
|
var solidAngle = 0d;
|
||||||
|
for (var s : Side.values()) {
|
||||||
|
if (!s.isExterior(box, origin)) continue;
|
||||||
|
solidAngle += s.getSolidAngle(box, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1 / solidAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 getTargetingDirection(@NotNull Vec3 origin, @NotNull RandomGenerator random) {
|
||||||
|
if (contains(origin)) return Vec3.random(random);
|
||||||
|
|
||||||
|
// determine sides facing the origin and their solid angles
|
||||||
|
int visible = 0;
|
||||||
|
|
||||||
|
// at most three faces are visible
|
||||||
|
Side[] sides = new Side[3];
|
||||||
|
double[] solidAngle = new double[3];
|
||||||
|
|
||||||
|
double accumSolidAngle = 0;
|
||||||
|
for (var s : Side.values()) {
|
||||||
|
if (!s.isExterior(box, origin)) continue;
|
||||||
|
|
||||||
|
var sa = s.getSolidAngle(box, origin);
|
||||||
|
accumSolidAngle += sa;
|
||||||
|
sides[visible] = s;
|
||||||
|
solidAngle[visible] = accumSolidAngle;
|
||||||
|
visible++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// choose a random side facing the origin based on their relative solid angles
|
||||||
|
var r = random.nextDouble() * solidAngle[visible - 1];
|
||||||
|
for (int j = 0; j < visible; j++) {
|
||||||
|
if (r < solidAngle[j]) {
|
||||||
|
// choose a random point on that side
|
||||||
|
var target = sides[j].random(box, random);
|
||||||
|
return target.minus(origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean contains(@NotNull Vec3 point) {
|
||||||
|
return box.min().x() < point.x() && point.x() < box.max().x()
|
||||||
|
&& box.min().y() < point.y() && point.y() < box.max().y()
|
||||||
|
&& box.min().z() < point.z() && point.z() < box.max().z();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return "Box(min=" + box.min() + ", max=" + box.max() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Side {
|
||||||
|
NEG_X(Vec3.UNIT_X.neg()),
|
||||||
|
NEG_Y(Vec3.UNIT_Y.neg()),
|
||||||
|
NEG_Z(Vec3.UNIT_Z.neg()),
|
||||||
|
POS_X(Vec3.UNIT_X),
|
||||||
|
POS_Y(Vec3.UNIT_Y),
|
||||||
|
POS_Z(Vec3.UNIT_Z),
|
||||||
|
;
|
||||||
|
|
||||||
|
private static final Side[] NEGATIVE = new Side[] {Side.NEG_X, Side.NEG_Y, Side.NEG_Z};
|
||||||
|
private static final Side[] POSITIVE = new Side[] {Side.POS_X, Side.POS_Y, Side.POS_Z};
|
||||||
|
|
||||||
|
private final @NotNull Vec3 normal;
|
||||||
|
|
||||||
|
Side(@NotNull Vec3 normal) {
|
||||||
|
this.normal = Objects.requireNonNull(normal, "normal");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the texture u coordinate for a position on this side of the box}
|
||||||
|
*/
|
||||||
|
public double getTextureU(@NotNull AABB box, @NotNull Vec3 pos) {
|
||||||
|
return switch (this) {
|
||||||
|
case NEG_X -> (pos.z() - box.min().z()) / (box.max().z() - box.min().z());
|
||||||
|
case POS_X -> (box.max().z() - pos.z()) / (box.max().z() - box.min().z());
|
||||||
|
case NEG_Y, POS_Y, POS_Z -> (pos.x() - box.min().x()) / (box.max().x() - box.min().x());
|
||||||
|
case NEG_Z -> (box.max().x() - pos.x()) / (box.max().x() - box.min().x());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the texture v coordinate for a position on this side of the box}
|
||||||
|
*/
|
||||||
|
public double getTextureV(@NotNull AABB box, @NotNull Vec3 pos) {
|
||||||
|
return switch (this) {
|
||||||
|
case NEG_X, POS_X, NEG_Z, POS_Z -> (pos.y() - box.min().y()) / (box.max().y() - box.min().y());
|
||||||
|
case NEG_Y -> (pos.z() - box.min().z()) / (box.max().z() - box.min().z());
|
||||||
|
case POS_Y -> (box.max().z() - pos.z()) / (box.max().z() - box.min().z());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return whether the given position is outside of the box only considering <code>this</code> side}
|
||||||
|
*/
|
||||||
|
public boolean isExterior(@NotNull AABB box, @NotNull Vec3 pos) {
|
||||||
|
return switch (this) {
|
||||||
|
case NEG_X -> pos.x() < box.min().x();
|
||||||
|
case NEG_Y -> pos.y() < box.min().y();
|
||||||
|
case NEG_Z -> pos.z() < box.min().z();
|
||||||
|
case POS_X -> pos.x() > box.max().x();
|
||||||
|
case POS_Y -> pos.y() > box.max().y();
|
||||||
|
case POS_Z -> pos.z() > box.max().z();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the point on <code>this</code> side of the <code>box</code> with the given <code>u</code>-<code>v</code>-coordinates}
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 get(@NotNull AABB box, double u, double v) {
|
||||||
|
return switch (this) {
|
||||||
|
case NEG_X -> new Vec3(
|
||||||
|
box.min().x(),
|
||||||
|
Math.fma(v, box.max().y() - box.min().y(), box.min().y()),
|
||||||
|
Math.fma(u, box.max().z() - box.min().z(), box.min().z())
|
||||||
|
);
|
||||||
|
case NEG_Y -> new Vec3(
|
||||||
|
Math.fma(u, box.max().x() - box.min().x(), box.min().x()),
|
||||||
|
box.min().y(),
|
||||||
|
Math.fma(v, box.max().z() - box.min().z(), box.min().z())
|
||||||
|
);
|
||||||
|
case NEG_Z -> new Vec3(
|
||||||
|
Math.fma(u, box.min().x() - box.max().x(), box.max().x()),
|
||||||
|
Math.fma(v, box.max().y() - box.min().y(), box.min().y()),
|
||||||
|
box.min().z()
|
||||||
|
);
|
||||||
|
case POS_X -> new Vec3(
|
||||||
|
box.max().x(),
|
||||||
|
Math.fma(v, box.max().y() - box.min().y(), box.min().y()),
|
||||||
|
Math.fma(u, box.min().z() - box.max().z(), box.max().z())
|
||||||
|
);
|
||||||
|
case POS_Y -> new Vec3(
|
||||||
|
Math.fma(u, box.max().x() - box.min().x(), box.min().x()),
|
||||||
|
box.max().y(),
|
||||||
|
Math.fma(v, box.min().z() - box.max().z(), box.max().z())
|
||||||
|
);
|
||||||
|
case POS_Z -> new Vec3(
|
||||||
|
Math.fma(u, box.max().x() - box.min().x(), box.min().x()),
|
||||||
|
Math.fma(v, box.max().y() - box.min().y(), box.min().y()),
|
||||||
|
box.max().z()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return a random point on <code>this</code> side of the <code>box</code>}
|
||||||
|
*/
|
||||||
|
public @NotNull Vec3 random(@NotNull AABB box, @NotNull RandomGenerator random) {
|
||||||
|
var u = random.nextDouble();
|
||||||
|
var v = random.nextDouble();
|
||||||
|
return get(box, u, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the solid angle covered by <code>this</code> side of the <code>box</code> when viewed from <code>pos</code>}
|
||||||
|
*/
|
||||||
|
public double getSolidAngle(@NotNull AABB box, @NotNull Vec3 pos) {
|
||||||
|
var a = get(box, 0, 0).minus(pos).unit();
|
||||||
|
var b = get(box, 0, 1).minus(pos).unit();
|
||||||
|
var c = get(box, 1, 1).minus(pos).unit();
|
||||||
|
var d = get(box, 1, 0).minus(pos).unit();
|
||||||
|
|
||||||
|
return PdfUtil.getSolidAngle(a, b, d) + PdfUtil.getSolidAngle(c, b, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.hittable3d;
|
||||||
|
|
||||||
|
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.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, this, material, 0, 0, true)); // arbitrary normal, u, v and isFrontFace
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull AABB getBoundingBox() {
|
||||||
|
return boundary().getBoundingBox();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,117 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.hittable3d;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.render.material.Material;
|
||||||
|
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.HitResult;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Target;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public final class Sphere implements Hittable, Target {
|
||||||
|
private final @NotNull Vec3 center;
|
||||||
|
private final double radius;
|
||||||
|
private final @NotNull Material material;
|
||||||
|
|
||||||
|
private final @NotNull AABB bbox;
|
||||||
|
|
||||||
|
private final @NotNull Vec3 normalizedCenter;
|
||||||
|
private final double invRadius;
|
||||||
|
|
||||||
|
public Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) {
|
||||||
|
this.center = Objects.requireNonNull(center, "center");
|
||||||
|
this.material = Objects.requireNonNull(material, "material");
|
||||||
|
if (radius <= 0 || !Double.isFinite(radius)) throw new IllegalArgumentException("radius must be positive");
|
||||||
|
this.radius = radius;
|
||||||
|
|
||||||
|
this.invRadius = 1 / radius;
|
||||||
|
this.normalizedCenter = this.center.times(-this.invRadius);
|
||||||
|
|
||||||
|
this.bbox = new AABB(
|
||||||
|
center.minus(radius, radius, radius),
|
||||||
|
center.plus(radius, radius, radius)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
|
||||||
|
var t = hit0(ray, range);
|
||||||
|
if (Double.isNaN(t)) return Optional.empty();
|
||||||
|
|
||||||
|
var position = ray.at(t);
|
||||||
|
var normal = Vec3.fma(invRadius, position, normalizedCenter);
|
||||||
|
var frontFace = normal.times(ray.direction()) < 0;
|
||||||
|
|
||||||
|
double u;
|
||||||
|
double v;
|
||||||
|
if (material.texture().isUVRequired()) {
|
||||||
|
var theta = Math.acos(-normal.y());
|
||||||
|
var phi = Math.atan2(-normal.z(), normal.x()) + Math.PI;
|
||||||
|
u = phi / (2 * Math.PI);
|
||||||
|
v = theta / Math.PI;
|
||||||
|
} else {
|
||||||
|
u = Double.NaN;
|
||||||
|
v = Double.NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(new HitResult(
|
||||||
|
t, position, frontFace ? normal : normal.neg(), this,
|
||||||
|
material, u, v, frontFace
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double hit0(@NotNull Ray ray, @NotNull Range range) {
|
||||||
|
var oc = ray.origin().minus(center);
|
||||||
|
|
||||||
|
var a = ray.direction().squared();
|
||||||
|
var h = ray.direction().times(oc);
|
||||||
|
var c = oc.squared() - radius * radius;
|
||||||
|
|
||||||
|
var discriminant = h * h - a * c;
|
||||||
|
if (discriminant < 0) return Double.NaN;
|
||||||
|
|
||||||
|
var sd = Math.sqrt(discriminant);
|
||||||
|
|
||||||
|
double t = (- h - sd) / a;
|
||||||
|
if (!range.surrounds(t)) t = (- h + sd) / a;
|
||||||
|
if (!range.surrounds(t)) return Double.NaN;
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull AABB getBoundingBox() {
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getProbabilityDensity(@NotNull Vec3 origin, @NotNull Vec3 direction) {
|
||||||
|
if (Double.isNaN(hit0(new Ray(origin, direction), FORWARD))) return 0;
|
||||||
|
|
||||||
|
var cos = Math.sqrt(1 - radius * radius / (center.minus(origin).squared()));
|
||||||
|
var solidAngle = 2 * Math.PI * (1 - cos);
|
||||||
|
return 1 / solidAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 getTargetingDirection(@NotNull Vec3 origin, @NotNull RandomGenerator random) {
|
||||||
|
var direction = center.minus(origin);
|
||||||
|
var out = Vec3.randomOppositeHemisphere(random, direction);
|
||||||
|
return new Vec3(
|
||||||
|
Math.fma(radius, out.x(), center.x() - origin.x()),
|
||||||
|
Math.fma(radius, out.y(), center.y() - origin.y()),
|
||||||
|
Math.fma(radius, out.z(), center.z() - origin.z())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return "Sphere(center=" + center + ", radius=" + radius + ")";
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,113 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.transform;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.AABB;
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Target;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public sealed class RotateY extends Transform {
|
||||||
|
private final double cos;
|
||||||
|
private final double sin;
|
||||||
|
|
||||||
|
private final @NotNull AABB bbox;
|
||||||
|
|
||||||
|
public static @NotNull RotateY create(@NotNull Hittable object, double angle) {
|
||||||
|
if (object instanceof Target) {
|
||||||
|
return new RotateYTarget(object, angle);
|
||||||
|
} else {
|
||||||
|
return new RotateY(object, angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RotateY(@NotNull Hittable object, double angle) {
|
||||||
|
super(object);
|
||||||
|
this.cos = Math.cos(angle);
|
||||||
|
this.sin = Math.sin(angle);
|
||||||
|
|
||||||
|
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++) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bbox = new AABB(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final @NotNull Ray transform(@NotNull Ray ray) {
|
||||||
|
var origin = ray.origin();
|
||||||
|
var direction = ray.direction();
|
||||||
|
|
||||||
|
var newOrigin = transform(origin);
|
||||||
|
var newDirection = transform(direction);
|
||||||
|
return new Ray(newOrigin, newDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final @NotNull HitResult transform(@NotNull HitResult result) {
|
||||||
|
var position = result.position();
|
||||||
|
var newPosition = untransform(position);
|
||||||
|
|
||||||
|
var normal = result.normal();
|
||||||
|
var newNormal = untransform(normal);
|
||||||
|
|
||||||
|
return result.withPositionAndNormal(newPosition, newNormal);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final @NotNull Vec3 transform(@NotNull Vec3 vec) {
|
||||||
|
return new Vec3(cos * vec.x() - sin * vec.z(), vec.y(), sin * vec.x() + cos * vec.z());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final @NotNull Vec3 untransform(@NotNull Vec3 vec) {
|
||||||
|
return new Vec3(cos * vec.x() + sin * vec.z(), vec.y(), - sin * vec.x() + cos * vec.z());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull AABB getBoundingBox() {
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return object + " rotated by " + Math.toDegrees(Math.atan2(sin, cos)) + "° around the y axis";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RotateYTarget extends RotateY implements Target {
|
||||||
|
|
||||||
|
private RotateYTarget(@NotNull Hittable object, double angle) {
|
||||||
|
super(object, angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getProbabilityDensity(@NotNull Vec3 origin, @NotNull Vec3 direction) {
|
||||||
|
return ((Target) object).getProbabilityDensity(transform(origin), transform(direction));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 getTargetingDirection(@NotNull Vec3 origin, @NotNull RandomGenerator random) {
|
||||||
|
return untransform(((Target) object).getTargetingDirection(transform(origin), random));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.transform;
|
||||||
|
|
||||||
|
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 eu.jonahbauer.raytracing.scene.Target;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
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,73 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.transform;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.AABB;
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Target;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.random.RandomGenerator;
|
||||||
|
|
||||||
|
public sealed class Translate extends Transform {
|
||||||
|
protected final @NotNull Vec3 offset;
|
||||||
|
private final @NotNull AABB bbox;
|
||||||
|
|
||||||
|
public static @NotNull Translate create(@NotNull Hittable object, @NotNull Vec3 offset) {
|
||||||
|
if (object instanceof Target) {
|
||||||
|
return new TranslateTarget(object, offset);
|
||||||
|
} else {
|
||||||
|
return new Translate(object, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Translate(@NotNull Hittable object, @NotNull Vec3 offset) {
|
||||||
|
super(object);
|
||||||
|
this.offset = offset;
|
||||||
|
|
||||||
|
var bbox = object.getBoundingBox();
|
||||||
|
this.bbox = new AABB(
|
||||||
|
bbox.min().plus(offset),
|
||||||
|
bbox.max().plus(offset)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final @NotNull Ray transform(@NotNull Ray ray) {
|
||||||
|
return new Ray(ray.origin().minus(offset), ray.direction());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected final @NotNull HitResult transform(@NotNull HitResult result) {
|
||||||
|
return result.withPositionAndNormal(result.position().plus(offset), result.normal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final @NotNull AABB getBoundingBox() {
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return object + " translated by " + offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TranslateTarget extends Translate implements Target {
|
||||||
|
private TranslateTarget(@NotNull Hittable object, @NotNull Vec3 offset) {
|
||||||
|
super(object, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getProbabilityDensity(@NotNull Vec3 origin, @NotNull Vec3 direction) {
|
||||||
|
if (!(object instanceof Target target)) throw new UnsupportedOperationException();
|
||||||
|
return target.getProbabilityDensity(origin.minus(offset), direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Vec3 getTargetingDirection(@NotNull Vec3 origin, @NotNull RandomGenerator random) {
|
||||||
|
if (!(object instanceof Target target)) throw new UnsupportedOperationException();
|
||||||
|
return target.getTargetingDirection(origin.minus(offset), random);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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, state.getRange())) 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,52 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.util;
|
||||||
|
|
||||||
|
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 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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Range getRange() {
|
||||||
|
return 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.AABB;
|
||||||
|
import eu.jonahbauer.raytracing.math.Ray;
|
||||||
|
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class HittableList extends HittableCollection {
|
||||||
|
private final @NotNull List<Hittable> objects;
|
||||||
|
private final @NotNull AABB bbox;
|
||||||
|
|
||||||
|
public HittableList(@NotNull List<? extends @NotNull Hittable> objects) {
|
||||||
|
this.objects = new ArrayList<>(objects);
|
||||||
|
this.bbox = AABB.getBoundingBox(this.objects).orElse(AABB.EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 AABB getBoundingBox() {
|
||||||
|
return bbox;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
package eu.jonahbauer.raytracing.scene.util;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.math.Vec3;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public final class PdfUtil {
|
||||||
|
private PdfUtil() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the solid angle of the triangle abc as seen from the origin} The vectors {@code a}, {@code b} and {@code c}
|
||||||
|
* must be unit vectors.
|
||||||
|
*/
|
||||||
|
public static double getSolidAngle(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Vec3 c) {
|
||||||
|
var angle = 2 * Math.atan(Math.abs(Vec3.tripleProduct(a, b, c)) / (1 + a.times(b) + b.times(c) + c.times(a)));
|
||||||
|
return angle < 0 ? 2 * Math.PI + angle : angle;
|
||||||
|
}
|
||||||
|
}
|
BIN
src/main/resources/earthmap.jpg
Normal file
After Width: | Height: | Size: 158 KiB |
@@ -1,5 +1,7 @@
|
|||||||
package eu.jonahbauer.raytracing.render;
|
package eu.jonahbauer.raytracing.render.canvas;
|
||||||
|
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.ImageFormat;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
@@ -17,18 +19,18 @@ class ImageTest {
|
|||||||
void test(@TempDir Path dir) throws IOException {
|
void test(@TempDir Path dir) throws IOException {
|
||||||
var image = new Image(256, 256);
|
var image = new Image(256, 256);
|
||||||
|
|
||||||
for (var y = 0; y < image.height(); y++) {
|
for (var y = 0; y < image.getHeight(); y++) {
|
||||||
for (var x = 0; x < image.width(); x++) {
|
for (var x = 0; x < image.getWidth(); x++) {
|
||||||
var r = (double) x / (image.width() - 1);
|
var r = (double) x / (image.getWidth() - 1);
|
||||||
var g = (double) y / (image.height() - 1);
|
var g = (double) y / (image.getHeight() - 1);
|
||||||
var b = 0;
|
var b = 0;
|
||||||
|
|
||||||
image.set(x, y, r, g, b);
|
image.set(x, y, new Color(r, g, b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println(dir);
|
System.out.println(dir);
|
||||||
ImageIO.write(image, dir.resolve("img.ppm"));
|
ImageFormat.PPM.write(image, dir.resolve("img.ppm"));
|
||||||
|
|
||||||
String expected;
|
String expected;
|
||||||
String actual;
|
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.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;
|
||||||
|
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||||
|
import eu.jonahbauer.raytracing.render.material.LambertianMaterial;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
@@ -13,7 +15,7 @@ class SphereTest {
|
|||||||
void hit() {
|
void hit() {
|
||||||
var center = new Vec3(1, 2, 3);
|
var center = new Vec3(1, 2, 3);
|
||||||
var radius = 5;
|
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 origin = new Vec3(6, 7, 8);
|
||||||
var direction = new Vec3(-1, -1, -1);
|
var direction = new Vec3(-1, -1, -1);
|