Compare commits

..

8 Commits

Author SHA1 Message Date
c6c43ad895 fix length of IDAT chunk 2024-08-06 17:39:09 +02:00
705e2d715d use a more performant RandomGenerator 2024-08-06 16:24:01 +02:00
85f61b7991 switch from octree to binary tree 2024-08-06 16:16:25 +02:00
a90b2c0d9e disallow unbounded Hittables and refactor Octree 2024-08-06 15:47:42 +02:00
235a61f354 add color to DielectricMaterial 2024-08-06 13:32:39 +02:00
5033cf78c6 move examples to their own file 2024-08-06 11:15:56 +02:00
c2d230bed0 fix checksum of IEND chunk 2024-08-06 01:11:46 +02:00
0cffee4cfa add README.md 2024-08-06 01:11:45 +02:00
58 changed files with 558 additions and 1849 deletions

View File

@@ -10,13 +10,6 @@ Based on the series <a href="https://raytracing.github.io"><cite>Ray Tracing in
``` ```
java -jar raytracing.jar --samples 5000 --height 1080 SIMPLE java -jar raytracing.jar --samples 5000 --height 1080 SIMPLE
``` ```
### spheres
![](./docs/spheres.png)
```
java -jar raytracing.jar --samples 1000 --height 1080 SPHERES
```
### squares ### squares
![](./docs/squares.png) ![](./docs/squares.png)
@@ -26,7 +19,7 @@ java -jar raytracing.jar --samples 500 --height 1200 SQUARES
### cornell box ### cornell box
![](./docs/cornell.png) ![](./docs/cornell_smoke.png)
``` ```
java -jar raytracing.jar --samples 50000 --height 1200 CORNELL java -jar raytracing.jar --samples 50000 --height 1200 CORNELL
@@ -46,12 +39,4 @@ java -jar raytracing.jar --samples 50000 --height 600 CORNELL_SMOKE
``` ```
java -jar raytracing.jar --samples 1000 --height 1080 DIAGRAMM java -jar raytracing.jar --samples 1000 --height 1080 DIAGRAMM
```
### a little bit of everything
![](./docs/final.png)
```
java -jar raytracing.jar --samples 10000 --height 1200 FINAL
``` ```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 KiB

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

After

Width:  |  Height:  |  Size: 702 KiB

View File

@@ -1,21 +1,16 @@
package eu.jonahbauer.raytracing; package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.texture.CheckerTexture; import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.camera.SimpleCamera; import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
import eu.jonahbauer.raytracing.render.material.*; 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.Hittable;
import eu.jonahbauer.raytracing.scene.Scene; import eu.jonahbauer.raytracing.scene.Scene;
import eu.jonahbauer.raytracing.scene.SkyBox; import eu.jonahbauer.raytracing.scene.SkyBox;
import eu.jonahbauer.raytracing.scene.hittable2d.Parallelogram; 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.ConstantMedium;
import eu.jonahbauer.raytracing.scene.hittable3d.Sphere; import eu.jonahbauer.raytracing.scene.hittable3d.Sphere;
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree; import eu.jonahbauer.raytracing.scene.util.Hittables;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
@@ -35,11 +30,7 @@ public class Examples {
register("LIGHT", Examples::getLight); register("LIGHT", Examples::getLight);
register("CORNELL", Examples::getCornellBox); register("CORNELL", Examples::getCornellBox);
register("CORNELL_SMOKE", Examples::getCornellBoxSmoke); register("CORNELL_SMOKE", Examples::getCornellBoxSmoke);
register("CORNELL_SPHERE", Examples::getCornellBoxSphere);
register("DIAGRAMM", Examples::getDiagramm); 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) { public static @NotNull IntFunction<Example> getByName(@NotNull String name) {
@@ -51,13 +42,14 @@ public class Examples {
public static @NotNull Example getSimpleScene(int height) { public static @NotNull Example getSimpleScene(int height) {
if (height <= 0) height = 675; if (height <= 0) height = 675;
return new Example( return new Example(
new Scene(getSkyBox(), List.of( new Scene(
getSkyBox(),
new Sphere(new Vec3(0, -100.5, -1.0), 100.0, new LambertianMaterial(new Color(0.8, 0.8, 0.0))), new Sphere(new Vec3(0, -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(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.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.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)) 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() SimpleCamera.builder()
.withImage(height * 16 / 9, height) .withImage(height * 16 / 9, height)
.build() .build()
@@ -69,10 +61,7 @@ public class Examples {
var rng = new Random(1); var rng = new Random(1);
var objects = new ArrayList<Hittable>(); var objects = new ArrayList<Hittable>();
objects.add(new Sphere( objects.add(new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.5, 0.5, 0.5))));
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 a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) { for (int b = -11; b < 11; b++) {
@@ -118,13 +107,14 @@ public class Examples {
public static @NotNull Example getSquares(int height) { public static @NotNull Example getSquares(int height) {
if (height <= 0) height = 600; if (height <= 0) height = 600;
return new Example( return new Example(
new Scene(getSkyBox(), List.of( new Scene(
getSkyBox(),
new Parallelogram(new Vec3(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(1.0, 0.2, 0.2))), new Parallelogram(new Vec3(-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(-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(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, 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))) 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() SimpleCamera.builder()
.withImage(height, height) .withImage(height, height)
.withFieldOfView(Math.toRadians(80)) .withFieldOfView(Math.toRadians(80))
@@ -137,12 +127,12 @@ public class Examples {
public static @NotNull Example getLight(int height) { public static @NotNull Example getLight(int height) {
if (height <= 0) height = 225; if (height <= 0) height = 225;
return new Example( return new Example(
new Scene(List.of( new Scene(
new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.2, 0.2, 0.2))), new Sphere(new Vec3(0, -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 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 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))) new Sphere(new Vec3(0, 7, 0), 2, new DiffuseLight(new Color(4.0, 4.0, 4.0)))
)), ),
SimpleCamera.builder() SimpleCamera.builder()
.withImage(height * 16 / 9, height) .withImage(height * 16 / 9, height)
.withFieldOfView(Math.toRadians(20)) .withFieldOfView(Math.toRadians(20))
@@ -161,16 +151,16 @@ public class Examples {
var light = new DiffuseLight(new Color(15.0, 15.0, 15.0)); var light = new DiffuseLight(new Color(15.0, 15.0, 15.0));
return new Example( return new Example(
new Scene(List.of( new Scene(
new Box(new Vec3(0, 0, 0), new Vec3(555, 555, 555), white, white, red, green, white, null), new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green),
new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red),
new Parallelogram(new Vec3(343, 554, 332), new Vec3(-130, 0, 0), new Vec3(0, 0, -105), light), new Parallelogram(new Vec3(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) new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0, 0), new Vec3(0, 0, 555), white),
.rotateY(Math.toRadians(15)) new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0, 0), new Vec3(0, 0, -555), white),
.translate(new Vec3(265, 0, 295)), new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0, 0), new Vec3(0, 555, 0), white),
new Box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white) Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
.rotateY(Math.toRadians(-18)) Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65))
.translate(new Vec3(130, 0, 65)) ),
)),
SimpleCamera.builder() SimpleCamera.builder()
.withImage(height, height) .withImage(height, height)
.withFieldOfView(Math.toRadians(40)) .withFieldOfView(Math.toRadians(40))
@@ -188,49 +178,22 @@ public class Examples {
var light = new DiffuseLight(new Color(7.0, 7.0, 7.0)); var light = new DiffuseLight(new Color(7.0, 7.0, 7.0));
return new Example( return new Example(
new Scene(List.of( new Scene(
new Box(new Vec3(0, 0, 0), new Vec3(555, 555, 555), white, white, red, green, white, null), new Parallelogram(new Vec3(555, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), green),
new Parallelogram(new Vec3(0, 0, 0), new Vec3(0, 555, 0), new Vec3(0, 0, 555), red),
new Parallelogram(new Vec3(113, 554, 127), new Vec3(330, 0, 0), new Vec3(0, 0, 305), light), new Parallelogram(new Vec3(113, 554, 127), new Vec3(330, 0, 0), new Vec3(0, 0, 305), light),
new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0, 0), new Vec3(0, 0, 555), white),
new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0, 0), new Vec3(0, 0, -555), white),
new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0, 0), new Vec3(0, 555, 0), white),
new ConstantMedium( new ConstantMedium(
new Box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white) Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
.rotateY(Math.toRadians(15))
.translate(new Vec3(265, 0, 295)),
0.01, new IsotropicMaterial(Color.BLACK) 0.01, new IsotropicMaterial(Color.BLACK)
), ),
new ConstantMedium( new ConstantMedium(
new Box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white) Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)),
.rotateY(Math.toRadians(-18))
.translate(new Vec3(130, 0, 65)),
0.01, new IsotropicMaterial(Color.WHITE) 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() SimpleCamera.builder()
.withImage(height, height) .withImage(height, height)
.withFieldOfView(Math.toRadians(40)) .withFieldOfView(Math.toRadians(40))
@@ -270,7 +233,7 @@ public class Examples {
for (int i = 0; i < data.size(); i++) { for (int i = 0; i < data.size(); i++) {
var partei = data.get(i); var partei = data.get(i);
objects.add(new Box( objects.add(Hittables.box(
new Vec3((i + 1) * spacing + i * size, 0, spacing), new Vec3((i + 1) * spacing + i * size, 0, spacing),
new Vec3((i + 1) * spacing + (i + 1) * size, partei.stimmen() * 15, spacing + size), new Vec3((i + 1) * spacing + (i + 1) * size, partei.stimmen() * 15, spacing + size),
new DielectricMaterial(1.5, partei.color()) new DielectricMaterial(1.5, partei.color())
@@ -288,109 +251,6 @@ public class Examples {
); );
} }
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() { private static @NotNull SkyBox getSkyBox() {
return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE); return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE);
} }

View File

@@ -13,8 +13,6 @@ import java.nio.file.Path;
import java.util.function.IntFunction; import java.util.function.IntFunction;
public class Main { public class Main {
public static final boolean DEBUG = false;
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
var config = Config.parse(args); var config = Config.parse(args);
var example = config.example; var example = config.example;

View File

@@ -11,6 +11,7 @@ import java.util.Optional;
* An axis-aligned bounding box. * An axis-aligned bounding box.
*/ */
public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) { public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
public static final AABB UNIVERSE = new AABB(Vec3.MIN, Vec3.MAX);
public static final AABB EMPTY = new AABB(Vec3.ZERO, Vec3.ZERO); public static final 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> 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> Y_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::y));
@@ -35,53 +36,38 @@ public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
return Optional.ofNullable(bbox); return Optional.ofNullable(bbox);
} }
/**
* {@return the range of x values}
*/
public @NotNull Range x() { public @NotNull Range x() {
return new Range(min.x(), max.x()); return new Range(min.x(), max.x());
} }
/**
* {@return the range of y values}
*/
public @NotNull Range y() { public @NotNull Range y() {
return new Range(min.y(), max.y()); return new Range(min.y(), max.y());
} }
/**
* {@return the range of z values}
*/
public @NotNull Range z() { public @NotNull Range z() {
return new Range(min.z(), max.z()); return new Range(min.z(), max.z());
} }
/**
* {@return the center of this bounding box}
*/
public @NotNull Vec3 center() { public @NotNull Vec3 center() {
return Vec3.average(min, max, 2); 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) { public @NotNull AABB expand(@NotNull AABB box) {
return new AABB(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max)); return new AABB(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max));
} }
/** public boolean hit(@NotNull Ray ray) {
* Tests whether the {@code ray} intersects this bounding box withing the {@code range} return intersect(ray).isPresent();
* @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 @NotNull Optional<Range> intersect(@NotNull Ray ray) {
*/ if (this == UNIVERSE) return Optional.of(Range.UNIVERSE);
public boolean hit(@NotNull Ray ray, @NotNull Range range) { if (this == EMPTY) return Optional.empty();
int vmask = ray.vmask();
var origin = ray.origin(); var origin = ray.origin();
var direction = ray.direction(); var invDirection = ray.direction().inv();
var invDirection = direction.inv();
// calculate t values for intersection points of ray with planes through min // calculate t values for intersection points of ray with planes through min
var tmin = intersect(min(), origin, invDirection); var tmin = intersect(min(), origin, invDirection);
@@ -91,31 +77,27 @@ public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
// determine range of t for which the ray is inside this voxel // determine range of t for which the ray is inside this voxel
double tlmax = Double.NEGATIVE_INFINITY; // lower limit maximum double tlmax = Double.NEGATIVE_INFINITY; // lower limit maximum
double tumin = Double.POSITIVE_INFINITY; // upper limit minimum double tumin = Double.POSITIVE_INFINITY; // upper limit minimum
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
// classify t values as lower or upper limit based on ray direction // classify t values as lower or upper limit based on vmask
if (direction.get(i) >= 0) { if ((vmask & (1 << i)) == 0) {
// min is lower limit and max is upper limit // min is lower limit and max is upper limit
if (tmin[i] > tlmax) tlmax = tmin[i]; tlmax = Math.max(tlmax, tmin[i]);
if (tmax[i] < tumin) tumin = tmax[i]; tumin = Math.min(tumin, tmax[i]);
} else { } else {
// max is lower limit and min is upper limit // max is lower limit and min is upper limit
if (tmax[i] > tlmax) tlmax = tmax[i]; tlmax = Math.max(tlmax, tmax[i]);
if (tmin[i] < tumin) tumin = tmin[i]; tumin = Math.min(tumin, tmin[i]);
} }
} }
return tlmax < tumin && tumin >= range.min() && tlmax <= range.max(); return tlmax < tumin ? Optional.of(new Range(tlmax, tumin)) : Optional.empty();
} }
/** public static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Ray ray) {
* Computes the {@code t} values of the intersections of a ray with the axis-aligned planes through a point. return intersect(corner, ray.origin(), ray.direction().inv());
* @param corner the point }
* @param origin the origin point of the ray
* @param invDirection the {@linkplain Vec3#inv() inverted} direction of the ray private static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Vec3 origin, @NotNull Vec3 invDirection) {
* @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[] { return new double[] {
(corner.x() - origin.x()) * invDirection.x(), (corner.x() - origin.x()) * invDirection.x(),
(corner.y() - origin.y()) * invDirection.y(), (corner.y() - origin.y()) * invDirection.y(),

View File

@@ -11,6 +11,16 @@ public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
} }
public @NotNull Vec3 at(double t) { public @NotNull Vec3 at(double t) {
return Vec3.fma(t, direction, origin); return new Vec3(
origin().x() + t * direction.x(),
origin().y() + t * direction.y(),
origin().z() + t * direction.z()
);
}
public int vmask() {
return (direction().x() < 0 ? 1 : 0)
| (direction().y() < 0 ? 2 : 0)
| (direction().z() < 0 ? 4 : 0);
} }
} }

View File

@@ -5,8 +5,6 @@ import org.jetbrains.annotations.NotNull;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; 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 MAX = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
@@ -16,63 +14,26 @@ public record Vec3(double x, double y, double z) {
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 {
if (DEBUG) { assert Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z) : "x, y and z must be finite";
if (!Double.isFinite(x) || !Double.isFinite(y) || !Double.isFinite(z)) {
throw new IllegalArgumentException("x, y and z must be finite");
}
}
} }
/**
* {@return a uniformly random unit vector}
*/
public static @NotNull Vec3 random(@NotNull RandomGenerator random) { public static @NotNull Vec3 random(@NotNull RandomGenerator random) {
double x, y, z; return random(random, false);
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(@NotNull RandomGenerator random, boolean unit) {
* {@return a uniformly random unit vector on the opposite hemisphere of the given <code>direction</code>} var vec = new Vec3(
*/ 2 * random.nextDouble() - 1,
public static @NotNull Vec3 randomOppositeHemisphere(@NotNull RandomGenerator random, @NotNull Vec3 direction) { 2 * random.nextDouble() - 1,
double x, y, z; 2 * random.nextDouble() - 1
double squared; );
do { return unit ? vec.unit() : vec;
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) {
var factor = - 2 * normal.times(vec); return vec.minus(normal.times(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);
@@ -84,49 +45,24 @@ 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();
} }
/**
* 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) { public static @NotNull Vec3 average(@NotNull Vec3 current, @NotNull Vec3 next, int index) {
var factor = 1d / index;
return new Vec3( return new Vec3(
Math.fma(factor, next.x() - current.x(), current.x()), current.x() + (next.x() - current.x()) / index,
Math.fma(factor, next.y() - current.y(), current.y()), current.y() + (next.y() - current.y()) / index,
Math.fma(factor, next.z() - current.z(), current.z()) current.z() + (next.z() - current.z()) / index
); );
} }
/**
* {@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) { public static @NotNull Vec3 max(@NotNull Vec3 a, @NotNull Vec3 b) {
return new Vec3( return new Vec3(
Math.max(a.x(), b.x()), Math.max(a.x(), b.x()),
@@ -135,11 +71,6 @@ public record Vec3(double x, double y, double 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) { public static @NotNull Vec3 min(@NotNull Vec3 a, @NotNull Vec3 b) {
return new Vec3( return new Vec3(
Math.min(a.x(), b.x()), Math.min(a.x(), b.x()),
@@ -148,24 +79,6 @@ public record Vec3(double x, double y, double z) {
); );
} }
/**
* {@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) { public @NotNull Vec3 plus(double x, double y, double z) {
return new Vec3(this.x + x, this.y + y, this.z + z); return new Vec3(this.x + x, this.y + y, this.z + z);
} }
@@ -174,122 +87,57 @@ public record Vec3(double x, double y, double z) {
return new Vec3(this.x - x, this.y - y, this.z - z); return new Vec3(this.x - x, this.y - y, this.z - z);
} }
/** public @NotNull Vec3 plus(@NotNull Vec3 b) {
* Adds a vector to this vector return new Vec3(this.x + b.x, this.y + b.y, this.z + b.z);
* @param other a vector }
* @return the sum of this and the other vector
*/ public @NotNull Vec3 minus(@NotNull Vec3 b) {
public @NotNull Vec3 plus(@NotNull Vec3 other) { return new Vec3(this.x - b.x, this.y - b.y, this.z - b.z);
return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z); }
public double times(@NotNull Vec3 b) {
return this.x * b.x + this.y * b.y + this.z * b.z;
}
public @NotNull Vec3 times(double b) {
return new Vec3(this.x * b, this.y * b, this.z * b);
} }
/**
* 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);
} }
/**
* Inverts each component of this vector.
* @return the inverted vector.
*/
public @NotNull Vec3 inv() { public @NotNull Vec3 inv() {
return new Vec3(1 / x, 1 / y, 1 / z); return new Vec3(1 / x, 1 / y, 1 / z);
} }
/** public @NotNull Vec3 cross(@NotNull Vec3 b) {
* 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(
Math.fma(this.y, other.z, - other.y * this.z), this.y() * b.z() - b.y() * this.z(),
Math.fma(this.z, other.x, - other.z * this.x), this.z() * b.x() - b.z() * this.x(),
Math.fma(this.x, other.y, - other.x * this.y) this.x() * b.y() - b.x() * this.y()
); );
} }
/** public @NotNull Vec3 div(double b) {
* Divides this vector by a scalar return new Vec3(this.x / b, this.y / b, this.z / b);
* @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() {
var squared = squared(); return div(length());
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) {
@@ -303,9 +151,4 @@ 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 + ")";
}
} }

View File

@@ -1,15 +1,10 @@
package eu.jonahbauer.raytracing.render.texture; package eu.jonahbauer.raytracing.render;
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;
import static eu.jonahbauer.raytracing.Main.DEBUG; public record Color(double r, double g, double b) {
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 RED = new Color(1.0, 0.0, 0.0); public static final @NotNull Color RED = new Color(1.0, 0.0, 0.0);
@@ -30,10 +25,6 @@ public record Color(double r, double g, double b) implements Texture, SkyBox {
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) { 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()); return new Color(a.r() + b.r(), a.g() + b.g(), a.b() + b.b());
} }
@@ -52,11 +43,10 @@ public record Color(double r, double g, double b) implements Texture, SkyBox {
} }
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(
Math.fma(factor, next.r() - current.r(), current.r()), current.r() + (next.r() - current.r()) / index,
Math.fma(factor, next.g() - current.g(), current.g()), current.g() + (next.g() - current.g()) / index,
Math.fma(factor, next.b() - current.b(), current.b()) current.b() + (next.b() - current.b()) / index
); );
} }
@@ -78,25 +68,12 @@ public record Color(double r, double g, double b) implements Texture, SkyBox {
} }
} }
public Color(int rgb) { public Color {}
this((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF);
}
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 toInt(r); return toInt(r);
} }
@@ -109,22 +86,7 @@ public record Color(double r, double g, double b) implements Texture, SkyBox {
return toInt(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) { private static int toInt(double value) {
return Math.clamp((int) (255.99 * value), 0, 255); return Math.max(0, Math.min(255, (int) (255.99 * value)));
} }
} }

View File

@@ -43,7 +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; private static final int IEND_CRC = -1371381630;
@Override @Override
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException { public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {

View File

@@ -18,13 +18,7 @@ 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, int i, int j, int n, @NotNull RandomGenerator random); @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random);
} }

View File

@@ -60,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.times(.5)).plus(pixelV.times(.5)); .plus(pixelU.div(2)).plus(pixelV.div(2));
} }
/** /**
@@ -79,20 +79,13 @@ 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}
*/ */
@Override public @NotNull Ray cast(int x, int y, @NotNull RandomGenerator random) {
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(random); var origin = getRayOrigin(random);
var target = getRayTarget(x, y, i, j, n, random); var target = getRayTarget(x, y, random);
return new Ray(origin, target.minus(origin)); return new Ray(origin, target.minus(origin));
} }
@@ -105,8 +98,8 @@ public final class SimpleCamera implements Camera {
if (blurRadius <= 0) return origin; if (blurRadius <= 0) return origin;
while (true) { while (true) {
var du = Math.fma(2, random.nextDouble(), -1); var du = 2 * random.nextDouble() - 1;
var dv = Math.fma(2, random.nextDouble(), -1); var dv = 2 * random.nextDouble() - 1;
if (du * du + dv * dv >= 1) continue; if (du * du + dv * dv >= 1) continue;
var ru = blurRadius * du; var ru = blurRadius * du;
@@ -123,10 +116,9 @@ public final class SimpleCamera implements Camera {
/** /**
* {@return the target vector for a ray through the given pixel} The position is randomized within the pixel. * {@return the target vector for a ray through the given pixel} The position is randomized within the pixel.
*/ */
private @NotNull Vec3 getRayTarget(int x, int y, int i, int j, int n, @NotNull RandomGenerator random) { private @NotNull Vec3 getRayTarget(int x, int y, @NotNull RandomGenerator random) {
var factor = 1d / n; double dx = x + random.nextDouble() - 0.5;
var dx = x + Math.fma(factor, i + random.nextDouble(), -0.5); double dy = y + 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));
} }

View File

@@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.render.canvas; package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.Color;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.function.Function; import java.util.function.Function;

View File

@@ -1,9 +1,8 @@
package eu.jonahbauer.raytracing.render.canvas; package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.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 {
@@ -22,16 +21,6 @@ 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;

View File

@@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.render.canvas; package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.Color;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.swing.*; import javax.swing.*;

View File

@@ -2,8 +2,7 @@ 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.texture.Color; import eu.jonahbauer.raytracing.render.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;
@@ -11,18 +10,18 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; import java.util.random.RandomGenerator;
public record DielectricMaterial(double refractionIndex, @NotNull Texture texture) implements Material { public record DielectricMaterial(double refractionIndex, @NotNull Color albedo) implements Material {
public DielectricMaterial(double refractionIndex) { public DielectricMaterial(double refractionIndex) {
this(refractionIndex, Color.WHITE); this(refractionIndex, Color.WHITE);
} }
public DielectricMaterial { public DielectricMaterial {
Objects.requireNonNull(texture, "texture"); Objects.requireNonNull(albedo, "albedo");
} }
@Override @Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var ri = hit.isFrontFace() ? (1 / refractionIndex) : refractionIndex; var ri = hit.frontFace() ? (1 / refractionIndex) : refractionIndex;
var cosTheta = Math.min(- ray.direction().unit().times(hit.normal()), 1.0); var cosTheta = Math.min(- ray.direction().unit().times(hit.normal()), 1.0);
var reflectance = reflectance(cosTheta); var reflectance = reflectance(cosTheta);
@@ -31,8 +30,7 @@ public record DielectricMaterial(double refractionIndex, @NotNull Texture textur
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()));
var attenuation = texture.get(hit); return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), albedo));
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection)));
} }
private double reflectance(double cos) { private double reflectance(double cos) {

View File

@@ -1,15 +1,14 @@
package eu.jonahbauer.raytracing.render.material; package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.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.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; import java.util.random.RandomGenerator;
public record DiffuseLight(@NotNull Texture texture) implements Material { public record DiffuseLight(@NotNull Color emit) implements Material {
@Override @Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
return Optional.empty(); return Optional.empty();
@@ -17,6 +16,6 @@ public record DiffuseLight(@NotNull Texture texture) implements Material {
@Override @Override
public @NotNull Color emitted(@NotNull HitResult hit) { public @NotNull Color emitted(@NotNull HitResult hit) {
return texture.get(hit); return emit;
} }
} }

View File

@@ -1,68 +0,0 @@
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();
}
}
}

View File

@@ -1,23 +1,17 @@
package eu.jonahbauer.raytracing.render.material; package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.renderer.pdf.SphereProbabilityDensityFunction; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.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.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; import java.util.random.RandomGenerator;
public record IsotropicMaterial(@NotNull Color albedo) implements Material { public record IsotropicMaterial(@NotNull Color albedo) implements Material{
@Override @Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
return Optional.of(new PdfScatterResult(albedo(), new SphereProbabilityDensityFunction())); return Optional.of(new ScatterResult(new Ray(hit.position(), Vec3.random(random, true)), albedo()));
}
@Override
public @NotNull Texture texture() {
return albedo();
} }
} }

View File

@@ -1,8 +1,8 @@
package eu.jonahbauer.raytracing.render.material; package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.renderer.pdf.CosineProbabilityDensityFunction; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -10,14 +10,17 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; import java.util.random.RandomGenerator;
public record LambertianMaterial(@NotNull Texture texture) implements Material { public record LambertianMaterial(@NotNull Color albedo) implements Material {
public LambertianMaterial { public LambertianMaterial {
Objects.requireNonNull(texture, "texture"); Objects.requireNonNull(albedo, "albedo");
} }
@Override @Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var attenuation = texture.get(hit); var newDirection = hit.normal().plus(Vec3.random(random, true));
return Optional.of(new PdfScatterResult(attenuation, new CosineProbabilityDensityFunction(hit.normal()))); if (newDirection.isNearZero()) newDirection = hit.normal();
var scattered = new Ray(hit.position(), newDirection);
return Optional.of(new ScatterResult(scattered, albedo));
} }
} }

View File

@@ -1,9 +1,7 @@
package eu.jonahbauer.raytracing.render.material; package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.renderer.pdf.ProbabilityDensityFunction; 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;
@@ -13,59 +11,16 @@ import java.util.random.RandomGenerator;
public interface Material { 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); @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) { default @NotNull Color emitted(@NotNull HitResult hit) {
return Color.BLACK; return Color.BLACK;
} }
/** record ScatterResult(@NotNull Ray ray, @NotNull Color attenuation) {
* The result of a {@linkplain Material#scatter(Ray, HitResult, RandomGenerator) scattering operation}. public ScatterResult {
*/
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"); 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(attenuation, "attenuation");
Objects.requireNonNull(pdf, "pdf");
} }
} }
} }

View File

@@ -2,7 +2,7 @@ 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.texture.Texture; import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -10,14 +10,14 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; import java.util.random.RandomGenerator;
public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements Material { public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Material {
public MetallicMaterial(@NotNull Texture texture) { public MetallicMaterial(@NotNull Color albedo) {
this(texture, 0); this(albedo, 0);
} }
public MetallicMaterial { public MetallicMaterial {
Objects.requireNonNull(texture, "texture"); Objects.requireNonNull(albedo, "albedo");
if (fuzz < 0 || !Double.isFinite(fuzz)) throw new IllegalArgumentException("fuzz must be non-negative"); if (fuzz < 0 || !Double.isFinite(fuzz)) throw new IllegalArgumentException("fuzz must be non-negative");
} }
@@ -25,9 +25,8 @@ public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var newDirection = Vec3.reflect(ray.direction(), hit.normal()); var newDirection = Vec3.reflect(ray.direction(), hit.normal());
if (fuzz > 0) { if (fuzz > 0) {
newDirection = Vec3.fma(fuzz, Vec3.random(random), newDirection.unit()); newDirection = newDirection.unit().plus(Vec3.random(random, true).times(fuzz));
} }
var attenuation = texture.get(hit); return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), albedo));
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection)));
} }
} }

View File

@@ -2,10 +2,7 @@ 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.material.Material; import eu.jonahbauer.raytracing.render.Color;
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;
@@ -16,10 +13,8 @@ import java.util.SplittableRandom;
import java.util.random.RandomGenerator; import java.util.random.RandomGenerator;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import static eu.jonahbauer.raytracing.Main.DEBUG;
public final class SimpleRenderer implements Renderer { public final class SimpleRenderer implements Renderer {
private final int sqrtSamplesPerPixel; private final int samplesPerPixel;
private final int maxDepth; private final int maxDepth;
private final double gamma; private final double gamma;
@@ -35,7 +30,7 @@ public final class SimpleRenderer implements Renderer {
} }
private SimpleRenderer(@NotNull Builder builder) { private SimpleRenderer(@NotNull Builder builder) {
this.sqrtSamplesPerPixel = (int) Math.sqrt(builder.samplesPerPixel); this.samplesPerPixel = builder.samplesPerPixel;
this.maxDepth = builder.maxDepth; this.maxDepth = builder.maxDepth;
this.gamma = builder.gamma; this.gamma = builder.gamma;
@@ -43,9 +38,6 @@ 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()) {
@@ -53,69 +45,42 @@ public final class SimpleRenderer implements Renderer {
} }
if (iterative) { if (iterative) {
renderIterative(camera, scene, canvas); var random = new Random();
} else {
renderNonIterative(camera, scene, canvas);
}
}
/** // render one sample after the other
* Renders the {@code scene} as seen by the {@code camera} to the {@code canvas}, taking one sample per pixel at for (int i = 1 ; i <= samplesPerPixel; i++) {
* a time and updating the canvas after each sample. var sample = i;
*/
private void renderIterative(@NotNull Camera camera, @NotNull Scene scene, @NotNull Canvas canvas) {
var random = new Random(0);
// render one sample after the other
int i = 0;
for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) {
for (int si = 0; si < sqrtSamplesPerPixel; si++) {
var sample = ++i;
var sif = si;
var sjf = sj;
getScanlineStream(camera.getHeight(), parallel).forEach(y -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
for (int x = 0; x < camera.getWidth(); x++) { for (int x = 0; x < camera.getWidth(); x++) {
var ray = camera.cast(x, y, sif, sjf, sqrtSamplesPerPixel, random); var ray = camera.cast(x, y, random);
var c = getColor(scene, ray, 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 // apply gamma correction
getScanlineStream(camera.getHeight(), parallel).forEach(y -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
for (int x = 0; x < camera.getWidth(); x++) { for (int x = 0; x < camera.getWidth(); x++) {
canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma)); 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;
int i = 0;
for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) {
for (int si = 0; si < sqrtSamplesPerPixel; si++) {
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)); });
} } else {
}); var splittable = new SplittableRandom();
// 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;
for (int i = 1; i <= samplesPerPixel; i++) {
var ray = camera.cast(x, y, random);
var c = getColor(scene, ray, random);
color = Color.average(color, c, i);
}
canvas.set(x, y, Color.gamma(color, gamma));
}
});
}
} }
/** /**
@@ -130,82 +95,21 @@ public final class SimpleRenderer implements Renderer {
var attenuation = Color.WHITE; var attenuation = Color.WHITE;
while (depth-- > 0) { while (depth-- > 0) {
var optional = scene.hit(ray); var optional = scene.hit(ray, new Range(0.001, Double.POSITIVE_INFINITY));
if (optional.isEmpty()) { if (optional.isEmpty()) {
var background = scene.getBackgroundColor(ray); color = Color.add(color, Color.multiply(attenuation, scene.getBackgroundColor(ray)));
color = Color.add(color, Color.multiply(attenuation, background));
if (DEBUG) {
System.out.println(" Hit background: " + background);
}
break; break;
} }
var hit = optional.get(); var hit = optional.get();
if (DEBUG) {
System.out.println(" Hit " + hit.target() + " at t=" + hit.t() + " (" + hit.position() + ")");
}
var material = hit.material(); var material = hit.material();
var emitted = material.emitted(hit); var emitted = material.emitted(hit);
if (DEBUG && !Color.BLACK.equals(emitted)) { var scatter = material.scatter(ray, hit, random);
System.out.println(" Emitted: " + emitted);
}
var result = material.scatter(ray, hit, random);
color = Color.add(color, Color.multiply(attenuation, emitted)); color = Color.add(color, Color.multiply(attenuation, emitted));
if (result.isEmpty()) { if (scatter.isEmpty()) break;
if (DEBUG) { attenuation = Color.multiply(attenuation, scatter.get().attenuation());
System.out.println(" Absorbed"); ray = scatter.get().ray();
}
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 color;

View File

@@ -1,27 +0,0 @@
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;
}
}

View File

@@ -1,46 +0,0 @@
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);
}
}
}

View File

@@ -1,25 +0,0 @@
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);
}

View File

@@ -1,22 +0,0 @@
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);
}
}

View File

@@ -1,41 +0,0 @@
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);
}
}

View File

@@ -1,21 +0,0 @@
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();
}
}

View File

@@ -1,43 +0,0 @@
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);
}
}

View File

@@ -1,152 +0,0 @@
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;
}
}

View File

@@ -1,32 +0,0 @@
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;
}
}

View File

@@ -1,38 +1,23 @@
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.render.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, @NotNull Vec3 position, @NotNull Vec3 normal, @NotNull Hittable target, double t,
@NotNull Material material, double u, double v, boolean isFrontFace @NotNull Vec3 position,
@NotNull Vec3 normal,
@NotNull Material material,
boolean frontFace
) implements Comparable<HitResult> { ) implements Comparable<HitResult> {
public HitResult { public HitResult {
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);

View File

@@ -11,38 +11,21 @@ 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);
/** /**
* @see #hit(Ray, Range) * {@return the value <code>t</code> such that <code>ray.at(t)</code> is the intersection of this shaped closest to
*/ * 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);
/**
* {@return the axis-aligned bounding box of this hittable}
*/
@NotNull AABB getBoundingBox(); @NotNull AABB getBoundingBox();
default @NotNull Hittable translate(@NotNull Vec3 offset) { default @NotNull Hittable translate(@NotNull Vec3 offset) {
return Translate.create(this, offset); return new Translate(this, offset);
} }
default @NotNull Hittable rotateY(double angle) { default @NotNull Hittable rotateY(double angle) {
return RotateY.create(this, angle); return new RotateY(this, angle);
} }
} }

View File

@@ -2,13 +2,11 @@ package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.AABB; 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.render.Color;
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree; import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree;
import eu.jonahbauer.raytracing.scene.util.HittableCollection; 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.Objects; import java.util.Objects;
@@ -16,28 +14,29 @@ public final class Scene extends HittableCollection {
private final @NotNull HittableCollection objects; private final @NotNull HittableCollection objects;
private final @NotNull SkyBox background; private final @NotNull SkyBox background;
private final @Nullable List<@NotNull Target> targets;
public Scene(@NotNull List<? extends @NotNull Hittable> objects) { public Scene(@NotNull List<? extends @NotNull Hittable> objects) {
this(objects, null); this(Color.BLACK, objects);
} }
public Scene(@NotNull List<? extends @NotNull Hittable> objects, @Nullable List<? extends @NotNull Target> targets) { public Scene(@NotNull Color background, @NotNull List<? extends @NotNull Hittable> objects) {
this(Color.BLACK, objects, targets); this(SkyBox.solid(background), objects);
} }
public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects) { public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects) {
this(background, objects, null); this.objects = new HittableBinaryTree(objects);
this.background = Objects.requireNonNull(background);
} }
public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects, @Nullable List<? extends @NotNull Target> targets) { public Scene(@NotNull Hittable @NotNull... objects) {
var list = new ArrayList<Hittable>(objects.size() + (targets != null ? targets.size() : 0)); this(List.of(objects));
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; public Scene(@NotNull Color background, @NotNull Hittable @NotNull... objects) {
this(background, List.of(objects));
}
public Scene(@NotNull SkyBox background, @NotNull Hittable @NotNull... objects) {
this(background, List.of(objects));
} }
@Override @Override
@@ -50,10 +49,6 @@ public final class Scene extends HittableCollection {
return objects.getBoundingBox(); return objects.getBoundingBox();
} }
public @Nullable List<@NotNull Target> getTargets() {
return targets;
}
public @NotNull Color getBackgroundColor(@NotNull Ray ray) { public @NotNull Color getBackgroundColor(@NotNull Ray ray) {
return background.getColor(ray); return background.getColor(ray);
} }

View File

@@ -1,7 +1,7 @@
package eu.jonahbauer.raytracing.scene; package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.Color;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@FunctionalInterface @FunctionalInterface
@@ -18,4 +18,8 @@ public interface SkyBox {
return Color.lerp(bottom, top, alt / Math.PI + 0.5); return Color.lerp(bottom, top, alt / Math.PI + 0.5);
}; };
} }
static @NotNull SkyBox solid(@NotNull Color color) {
return _ -> color;
}
} }

View File

@@ -1,39 +0,0 @@
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);
}
}

View File

@@ -51,41 +51,8 @@ public abstract class Hittable2D implements Hittable {
if (!isInterior(alpha, beta)) return Optional.empty(); if (!isInterior(alpha, beta)) return Optional.empty();
var frontFace = denominator < 0; var frontFace = denominator < 0;
return Optional.of(new HitResult( return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace));
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); protected abstract boolean isInterior(double alpha, double beta);
@Override
public @NotNull String toString() {
return this.getClass().getSimpleName() + "(origin=" + origin + ", u=" + u + ", v=" + v + ")";
}
} }

View File

@@ -1,16 +1,11 @@
package eu.jonahbauer.raytracing.scene.hittable2d; package eu.jonahbauer.raytracing.scene.hittable2d;
import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.scene.Target;
import eu.jonahbauer.raytracing.scene.util.PdfUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.random.RandomGenerator; public final class Parallelogram extends Hittable2D {
public final class Parallelogram extends Hittable2D implements Target {
private final @NotNull AABB bbox; private final @NotNull AABB bbox;
public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) { public Parallelogram(@NotNull Vec3 origin, @NotNull Vec3 u, @NotNull Vec3 v, @NotNull Material material) {
@@ -27,24 +22,4 @@ public final class Parallelogram extends Hittable2D implements Target {
public @NotNull AABB getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; 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);
}
} }

View File

@@ -1,304 +0,0 @@
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);
}
}
}

View File

@@ -32,7 +32,7 @@ public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNul
if (hitDistance > distance) return Optional.empty(); if (hitDistance > distance) return Optional.empty();
var t = tmin + hitDistance / length; 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 return Optional.of(new HitResult(t, ray.at(t), Vec3.UNIT_X, material, true)); // arbitrary normal and frontFace
} }
@Override @Override

View File

@@ -7,32 +7,24 @@ import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.Target;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public final class Sphere implements Hittable, Target { public final class Sphere implements Hittable {
private final @NotNull Vec3 center; private final @NotNull Vec3 center;
private final double radius; private final double radius;
private final @NotNull Material material; private final @NotNull Material material;
private final @NotNull AABB bbox; private final @NotNull AABB bbox;
private final @NotNull Vec3 normalizedCenter;
private final double invRadius;
public Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) { public Sphere(@NotNull Vec3 center, double radius, @NotNull Material material) {
this.center = Objects.requireNonNull(center, "center"); this.center = Objects.requireNonNull(center, "center");
this.material = Objects.requireNonNull(material, "material"); this.material = Objects.requireNonNull(material, "material");
if (radius <= 0 || !Double.isFinite(radius)) throw new IllegalArgumentException("radius must be positive"); if (radius <= 0 || !Double.isFinite(radius)) throw new IllegalArgumentException("radius must be positive");
this.radius = radius; this.radius = radius;
this.invRadius = 1 / radius;
this.normalizedCenter = this.center.times(-this.invRadius);
this.bbox = new AABB( this.bbox = new AABB(
center.minus(radius, radius, radius), center.minus(radius, radius, radius),
center.plus(radius, radius, radius) center.plus(radius, radius, radius)
@@ -41,32 +33,6 @@ public final class Sphere implements Hittable, Target {
@Override @Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) { public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) {
var 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 oc = ray.origin().minus(center);
var a = ray.direction().squared(); var a = ray.direction().squared();
@@ -74,44 +40,22 @@ public final class Sphere implements Hittable, Target {
var c = oc.squared() - radius * radius; var c = oc.squared() - radius * radius;
var discriminant = h * h - a * c; var discriminant = h * h - a * c;
if (discriminant < 0) return Double.NaN; if (discriminant < 0) return Optional.empty();
var sd = Math.sqrt(discriminant); var sd = Math.sqrt(discriminant);
double t = (- h - sd) / a; double t = (- h - sd) / a;
if (!range.surrounds(t)) t = (- h + sd) / a; if (!range.surrounds(t)) t = (- h + sd) / a;
if (!range.surrounds(t)) return Double.NaN; if (!range.surrounds(t)) return Optional.empty();
return t; 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 @Override
public @NotNull AABB getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; 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 + ")";
}
} }

View File

@@ -5,26 +5,15 @@ import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.Target;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.random.RandomGenerator; public final class RotateY extends Transform {
public sealed class RotateY extends Transform {
private final double cos; private final double cos;
private final double sin; private final double sin;
private final @NotNull AABB bbox; private final @NotNull AABB bbox;
public static @NotNull RotateY create(@NotNull Hittable object, double angle) { public RotateY(@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); super(object);
this.cos = Math.cos(angle); this.cos = Math.cos(angle);
this.sin = Math.sin(angle); this.sin = Math.sin(angle);
@@ -56,58 +45,45 @@ public sealed class RotateY extends Transform {
} }
@Override @Override
protected final @NotNull Ray transform(@NotNull Ray ray) { protected @NotNull Ray transform(@NotNull Ray ray) {
var origin = ray.origin(); var origin = ray.origin();
var direction = ray.direction(); var direction = ray.direction();
var newOrigin = transform(origin); var newOrigin = new Vec3(
var newDirection = transform(direction); cos * origin.x() - sin * origin.z(),
origin.y(),
sin * origin.x() + cos * origin.z()
);
var newDirection = new Vec3(
cos * direction.x() - sin * direction.z(),
direction.y(),
sin * direction.x() + cos * direction.z()
);
return new Ray(newOrigin, newDirection); return new Ray(newOrigin, newDirection);
} }
@Override @Override
protected final @NotNull HitResult transform(@NotNull HitResult result) { protected @NotNull HitResult transform(@NotNull HitResult result) {
var position = result.position(); var position = result.position();
var newPosition = untransform(position); var newPosition = new Vec3(
cos * position.x() + sin * position.z(),
position.y(),
- sin * position.x() + cos * position.z()
);
var normal = result.normal(); var normal = result.normal();
var newNormal = untransform(normal); var newNormal = new Vec3(
cos * normal.x() + sin * normal.z(),
normal.y(),
-sin * normal.x() + cos * normal.z()
);
return result.withPositionAndNormal(newPosition, newNormal); return new HitResult(result.t(), newPosition, newNormal, result.material(), result.frontFace());
}
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 @Override
public @NotNull AABB getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; 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));
}
}
} }

View File

@@ -2,15 +2,12 @@ package eu.jonahbauer.raytracing.scene.transform;
import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.Target;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public abstract class Transform implements Hittable { public abstract class Transform implements Hittable {
protected final @NotNull Hittable object; protected final @NotNull Hittable object;

View File

@@ -5,24 +5,13 @@ import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.Target;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.random.RandomGenerator; public final class Translate extends Transform {
private final @NotNull Vec3 offset;
public sealed class Translate extends Transform {
protected final @NotNull Vec3 offset;
private final @NotNull AABB bbox; private final @NotNull AABB bbox;
public static @NotNull Translate create(@NotNull Hittable object, @NotNull Vec3 offset) { public Translate(@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); super(object);
this.offset = offset; this.offset = offset;
@@ -34,40 +23,23 @@ public sealed class Translate extends Transform {
} }
@Override @Override
protected final @NotNull Ray transform(@NotNull Ray ray) { protected @NotNull Ray transform(@NotNull Ray ray) {
return new Ray(ray.origin().minus(offset), ray.direction()); return new Ray(ray.origin().minus(offset), ray.direction());
} }
@Override @Override
protected final @NotNull HitResult transform(@NotNull HitResult result) { protected @NotNull HitResult transform(@NotNull HitResult result) {
return result.withPositionAndNormal(result.position().plus(offset), result.normal()); return new HitResult(
result.t(),
result.position().plus(offset),
result.normal(),
result.material(),
result.frontFace()
);
} }
@Override @Override
public final @NotNull AABB getBoundingBox() { public @NotNull AABB getBoundingBox() {
return bbox; 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);
}
}
} }

View File

@@ -47,7 +47,7 @@ public final class HittableBinaryTree extends HittableCollection {
@Override @Override
public void hit(@NotNull Ray ray, @NotNull State state) { public void hit(@NotNull Ray ray, @NotNull State state) {
if (!bbox.hit(ray, state.getRange())) return; if (!bbox.hit(ray)) return;
if (left instanceof HittableCollection coll) { if (left instanceof HittableCollection coll) {
coll.hit(ray, state); coll.hit(ray, state);
} else if (left != null) { } else if (left != null) {

View File

@@ -41,10 +41,6 @@ public abstract class HittableCollection implements Hittable {
this.range = Objects.requireNonNull(range); this.range = Objects.requireNonNull(range);
} }
public @NotNull Range getRange() {
return range;
}
private @NotNull Optional<HitResult> getResult() { private @NotNull Optional<HitResult> getResult() {
return Optional.ofNullable(result); return Optional.ofNullable(result);
} }

View File

@@ -5,7 +5,6 @@ import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public final class HittableList extends HittableCollection { public final class HittableList extends HittableCollection {
@@ -13,7 +12,7 @@ public final class HittableList extends HittableCollection {
private final @NotNull AABB bbox; private final @NotNull AABB bbox;
public HittableList(@NotNull List<? extends @NotNull Hittable> objects) { public HittableList(@NotNull List<? extends @NotNull Hittable> objects) {
this.objects = new ArrayList<>(objects); this.objects = List.copyOf(objects);
this.bbox = AABB.getBoundingBox(this.objects).orElse(AABB.EMPTY); this.bbox = AABB.getBoundingBox(this.objects).orElse(AABB.EMPTY);
} }

View File

@@ -0,0 +1,253 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.IntStream;
public final class HittableOctree extends HittableCollection {
private static final int LIST_SIZE_LIMIT = 16;
private final @Nullable Storage storage;
private final @NotNull AABB bbox;
public HittableOctree(@NotNull List<? extends @NotNull Hittable> objects) {
bbox = AABB.getBoundingBox(objects).orElse(AABB.EMPTY);
storage = newStorage(bbox, objects);
}
private static @NotNull AABB[] getBoundingBoxes(@NotNull AABB aabb, @NotNull Vec3 center) {
return new AABB[] {
new AABB(new Range(aabb.x().min(), center.x()), new Range(aabb.y().min(), center.y()), new Range(aabb.z().min(), center.z())),
new AABB(new Range(center.x(), aabb.x().max()), new Range(aabb.y().min(), center.y()), new Range(aabb.z().min(), center.z())),
new AABB(new Range(aabb.x().min(), center.x()), new Range(center.y(), aabb.y().max()), new Range(aabb.z().min(), center.z())),
new AABB(new Range(center.x(), aabb.x().max()), new Range(center.y(), aabb.y().max()), new Range(aabb.z().min(), center.z())),
new AABB(new Range(aabb.x().min(), center.x()), new Range(aabb.y().min(), center.y()), new Range(center.z(), aabb.z().max())),
new AABB(new Range(center.x(), aabb.x().max()), new Range(aabb.y().min(), center.y()), new Range(center.z(), aabb.z().max())),
new AABB(new Range(aabb.x().min(), center.x()), new Range(center.y(), aabb.y().max()), new Range(center.z(), aabb.z().max())),
new AABB(new Range(center.x(), aabb.x().max()), new Range(center.y(), aabb.y().max()), new Range(center.z(), aabb.z().max())),
};
}
private static @Nullable Storage newStorage(@NotNull AABB aabb, @NotNull List<? extends @NotNull Hittable> objects) {
if (objects.isEmpty()) return null;
if (objects.size() < LIST_SIZE_LIMIT) {
return new ListStorage(aabb, objects);
} else {
var center = aabb.center();
var octants = (List<Hittable>[]) new List<?>[8];
for (int i = 0; i < 8; i++) octants[i] = new ArrayList<>();
var bboxes = getBoundingBoxes(aabb, center);
var list = new ArrayList<Hittable>();
for (var object : objects) {
var bbox = object.getBoundingBox();
var imin = getOctantIndex(center, bbox.min());
var imax = getOctantIndex(center, bbox.max());
if (imin == imax) {
octants[imin].add(object);
} else {
list.add(object);
}
}
return new NodeStorage(aabb, center, list, IntStream.range(0, 8).mapToObj(i -> newStorage(bboxes[i], octants[i])).toArray(Storage[]::new));
}
}
@Override
public void hit(@NotNull Ray ray, @NotNull State state) {
hit(ray, object -> hit(state, ray, object));
}
@Override
public @NotNull AABB getBoundingBox() {
return bbox;
}
/**
* Use HERO algorithms to find all elements that could possibly be hit by the given ray.
* @see <a href="https://doi.org/10.1007/978-3-642-76298-7_3">
* Agate, M., Grimsdale, R.L., Lister, P.F. (1991).
* The HERO Algorithm for Ray-Tracing Octrees.
* In: Grimsdale, R.L., Straßer, W. (eds) Advances in Computer Graphics Hardware IV. Eurographic Seminars. Springer, Berlin, Heidelberg.</a>
*/
private void hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> action) {
if (storage != null) storage.hit(ray, action);
}
private static int getOctantIndex(@NotNull Vec3 center, @NotNull Vec3 pos) {
return (pos.x() < center.x() ? 0 : 1)
| (pos.y() < center.y() ? 0 : 2)
| (pos.z() < center.z() ? 0 : 4);
}
private abstract static sealed class Storage {
protected final @NotNull AABB bbox;
public Storage(@NotNull AABB bbox) {
this.bbox = Objects.requireNonNull(bbox);
}
protected boolean hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> action) {
var range = bbox.intersect(ray);
if (range.isEmpty()) return false;
int vmask = ray.vmask();
return hit0(ray, vmask, range.get().min(), range.get().max(), action);
}
protected abstract boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<? super Hittable> action);
}
private static final class ListStorage extends Storage {
private final @NotNull List<Hittable> list;
public ListStorage(@NotNull AABB bbox, @NotNull List<? extends @NotNull Hittable> entries) {
super(bbox);
this.list = List.copyOf(entries);
}
@Override
protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<? super Hittable> action) {
var hit = false;
for (Hittable hittable : list) {
hit |= action.test(hittable);
}
return hit;
}
}
private static final class NodeStorage extends Storage {
private final @Nullable Storage @NotNull[] octants;
private final @NotNull Vec3 center;
private final int degenerate;
private final @NotNull List<Hittable> list; // track elements spanning multiple octants separately
public NodeStorage(@NotNull AABB bbox, @NotNull Vec3 center, @NotNull List<? extends @NotNull Hittable> list, @Nullable Storage @NotNull[] octants) {
super(bbox);
this.octants = octants;
this.center = center;
this.list = List.copyOf(list);
int count = 0;
int degenerate = 0;
for (int i = 0; i < octants.length; i++) {
if (octants[i] != null) {
count++;
degenerate = i;
}
}
this.degenerate = count == 1 ? degenerate : -1;
}
@Override
protected boolean hit(@NotNull Ray ray, @NotNull Predicate<? super Hittable> action) {
if (degenerate >= 0 && list.isEmpty()) {
return octants[degenerate].hit(ray, action);
} else {
return super.hit(ray, action);
}
}
@Override
protected boolean hit0(@NotNull Ray ray, int vmask, double tmin, double tmax, @NotNull Predicate<? super Hittable> action) {
if (tmax < 0) return false;
// check for hit
var hit = false;
// process entries spanning multiple children
for (Hittable object : list) {
hit |= action.test(object);
}
// t values for intersection points of ray with planes through center
var tmid = AABB.intersect(center, ray);
// masks of planes in the order of intersection, e.g. [2, 1, 4] for a ray intersection y = center.y() then x = center.x() then z = center.z()
var masklist = calculateMasklist(tmid);
// the first child to be hit by the ray assuming a ray with positive x, y and z coordinates
var childmask = (tmid[0] < tmin ? 1 : 0)
| (tmid[1] < tmin ? 2 : 0)
| (tmid[2] < tmin ? 4 : 0);
// the last child to be hit by the ray assuming a ray with positive x, y and z coordinates
var lastmask = (tmid[0] < tmax ? 1 : 0)
| (tmid[1] < tmax ? 2 : 0)
| (tmid[2] < tmax ? 4 : 0);
var childTmin = tmin;
int i = 0;
while (true) {
// use vmask to nullify the assumption of a positive ray made for childmask
var child = octants[childmask ^ vmask];
// calculate t value for exit of child
double childTmax;
if (childmask == lastmask) {
// last child shares tmax
childTmax = tmax;
} else {
// determine next child
while ((masklist[i] & childmask) != 0) {
i++;
}
childmask = childmask | masklist[i];
// tmax of current child is the t value for the intersection with the plane dividing the current and next child
childTmax = tmid[Integer.numberOfTrailingZeros(masklist[i])];
}
// process child
var childHit = child != null && child.hit0(ray, vmask, childTmin, childTmax, action);
hit |= childHit;
// break after last child has been processed or a hit has been found
if (childTmax == tmax || childHit) break;
// tmin of next child is tmax of current child
childTmin = childTmax;
}
return hit;
}
private static final int[][] MASKLISTS = new int[][] {
{1, 2, 4},
{1, 4, 2},
{4, 1, 2},
{2, 1, 4},
{2, 4, 1},
{4, 2, 1}
};
private static int @NotNull [] calculateMasklist(double @NotNull[] tmid) {
if (tmid[0] < tmid[1]) {
if (tmid[1] < tmid[2]) {
return MASKLISTS[0]; // {1, 2, 4}
} else if (tmid[0] < tmid[2]) {
return MASKLISTS[1]; // {1, 4, 2}
} else {
return MASKLISTS[2]; // {4, 1, 2}
}
} else {
if (tmid[0] < tmid[2]) {
return MASKLISTS[3]; // {2, 1, 4}
} else if (tmid[1] < tmid[2]) {
return MASKLISTS[4]; // {2, 4, 1}
} else {
return MASKLISTS[5]; // {4, 2, 1}
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
package eu.jonahbauer.raytracing.scene.util;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.hittable2d.Parallelogram;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
public final class Hittables {
private Hittables() {
throw new UnsupportedOperationException();
}
public static @NotNull Hittable box(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Material material) {
var sides = new ArrayList<Hittable>();
var min = Vec3.min(a, b);
var max = Vec3.max(a, b);
var dx = new Vec3(max.x() - min.x(), 0, 0);
var dy = new Vec3(0, max.y() - min.y(), 0);
var dz = new Vec3(0, 0, max.z() - min.z());
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), max.z()), dx, dy, material)); // front
sides.add(new Parallelogram(new Vec3(max.x(), min.y(), max.z()), dz.neg(), dy, material)); // right
sides.add(new Parallelogram(new Vec3(max.x(), min.y(), min.z()), dx.neg(), dy, material)); // back
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), min.z()), dz, dy, material)); // left
sides.add(new Parallelogram(new Vec3(min.x(), max.y(), max.z()), dx, dz.neg(), material)); // top
sides.add(new Parallelogram(new Vec3(min.x(), min.y(), min.z()), dx, dz, material)); // bottom
return new HittableList(sides);
}
}

View File

@@ -1,19 +0,0 @@
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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.render.canvas; package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.Color;
import eu.jonahbauer.raytracing.render.ImageFormat; 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;

View File

@@ -3,7 +3,7 @@ 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.Color;
import eu.jonahbauer.raytracing.render.material.LambertianMaterial; import eu.jonahbauer.raytracing.render.material.LambertianMaterial;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;