Compare commits
10 Commits
2c28b10a6e
...
a90a0db6d5
Author | SHA1 | Date | |
---|---|---|---|
a90a0db6d5 | |||
1b02f8a96d | |||
18c179f8e3 | |||
1d48a49987 | |||
9b617a82a8 | |||
dfe80011c9 | |||
37539a1906 | |||
70f2f38e96 | |||
e6447fe684 | |||
7c0bc68ab2 |
@ -1,16 +1,20 @@
|
||||
package eu.jonahbauer.raytracing;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.CheckerTexture;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
|
||||
import eu.jonahbauer.raytracing.render.material.*;
|
||||
import eu.jonahbauer.raytracing.render.texture.ImageTexture;
|
||||
import eu.jonahbauer.raytracing.render.texture.PerlinTexture;
|
||||
import eu.jonahbauer.raytracing.scene.Hittable;
|
||||
import eu.jonahbauer.raytracing.scene.Scene;
|
||||
import eu.jonahbauer.raytracing.scene.SkyBox;
|
||||
import eu.jonahbauer.raytracing.scene.hittable2d.Parallelogram;
|
||||
import eu.jonahbauer.raytracing.scene.hittable3d.Box;
|
||||
import eu.jonahbauer.raytracing.scene.hittable3d.ConstantMedium;
|
||||
import eu.jonahbauer.raytracing.scene.hittable3d.Sphere;
|
||||
import eu.jonahbauer.raytracing.scene.util.Hittables;
|
||||
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
@ -31,6 +35,9 @@ public class Examples {
|
||||
register("CORNELL", Examples::getCornellBox);
|
||||
register("CORNELL_SMOKE", Examples::getCornellBoxSmoke);
|
||||
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) {
|
||||
@ -61,7 +68,10 @@ public class Examples {
|
||||
|
||||
var rng = new Random(1);
|
||||
var objects = new ArrayList<Hittable>();
|
||||
objects.add(new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.5, 0.5, 0.5))));
|
||||
objects.add(new Sphere(
|
||||
new Vec3(0, -1000, 0), 1000,
|
||||
new LambertianMaterial(new CheckerTexture(0.32, new Color(.2, .3, .1), new Color(.9, .9, .9)))
|
||||
));
|
||||
|
||||
for (int a = -11; a < 11; a++) {
|
||||
for (int b = -11; b < 11; b++) {
|
||||
@ -158,8 +168,8 @@ public class Examples {
|
||||
new Parallelogram(new Vec3(0, 0, 0), new Vec3(555, 0, 0), new Vec3(0, 0, 555), white),
|
||||
new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0, 0), new Vec3(0, 0, -555), white),
|
||||
new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0, 0), new Vec3(0, 555, 0), white),
|
||||
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
|
||||
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65))
|
||||
new Box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
|
||||
new Box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65))
|
||||
),
|
||||
SimpleCamera.builder()
|
||||
.withImage(height, height)
|
||||
@ -186,11 +196,11 @@ public class Examples {
|
||||
new Parallelogram(new Vec3(555, 555, 555), new Vec3(-555, 0, 0), new Vec3(0, 0, -555), white),
|
||||
new Parallelogram(new Vec3(0, 0, 555), new Vec3(555, 0, 0), new Vec3(0, 555, 0), white),
|
||||
new ConstantMedium(
|
||||
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
|
||||
new Box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white).rotateY(Math.toRadians(15)).translate(new Vec3(265, 0, 295)),
|
||||
0.01, new IsotropicMaterial(Color.BLACK)
|
||||
),
|
||||
new ConstantMedium(
|
||||
Hittables.box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)),
|
||||
new Box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white).rotateY(Math.toRadians(-18)).translate(new Vec3(130, 0, 65)),
|
||||
0.01, new IsotropicMaterial(Color.WHITE)
|
||||
)
|
||||
),
|
||||
@ -233,7 +243,7 @@ public class Examples {
|
||||
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
var partei = data.get(i);
|
||||
objects.add(Hittables.box(
|
||||
objects.add(new Box(
|
||||
new Vec3((i + 1) * spacing + i * size, 0, spacing),
|
||||
new Vec3((i + 1) * spacing + (i + 1) * size, partei.stimmen() * 15, spacing + size),
|
||||
new DielectricMaterial(1.5, partei.color())
|
||||
@ -251,6 +261,111 @@ public class Examples {
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Example getEarth(int height) {
|
||||
if (height <= 0) height = 450;
|
||||
|
||||
return new Example(
|
||||
new Scene(
|
||||
getSkyBox(),
|
||||
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(),
|
||||
new Sphere(new Vec3(0, -1000, 0), 1000, material),
|
||||
new Sphere(new Vec3(0, 2, 0), 2, material)
|
||||
),
|
||||
SimpleCamera.builder()
|
||||
.withImage(height * 16 / 9, height)
|
||||
.withFieldOfView(Math.toRadians(20))
|
||||
.withPosition(new Vec3(13, 2, 3))
|
||||
.withTarget(Vec3.ZERO)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Example getFinal(int height) {
|
||||
if (height <= 0) height = 400;
|
||||
|
||||
var objects = new ArrayList<Hittable>();
|
||||
var random = new Random(1);
|
||||
|
||||
// boxes
|
||||
var boxes = new ArrayList<Hittable>();
|
||||
var ground = new LambertianMaterial(new Color(0.48, 0.83, 0.53));
|
||||
for (int i = 0; i < 20; i++) {
|
||||
for (int j = 0; j < 20; j++) {
|
||||
var w = 100.0;
|
||||
var x0 = -1000.0 + i * w;
|
||||
var z0 = -1000.0 + j * w;
|
||||
var y0 = 0.0;
|
||||
var x1 = x0 + w;
|
||||
var y1 = random.nextInt(1, 101);
|
||||
var z1 = z0 + w;
|
||||
boxes.add(new Box(new Vec3(x0, y0, z0), new Vec3(x1, y1, z1), ground));
|
||||
}
|
||||
}
|
||||
objects.add(new HittableBinaryTree(boxes));
|
||||
|
||||
// light
|
||||
objects.add(new Parallelogram(
|
||||
new Vec3(123, 554, 147), new Vec3(300, 0, 0), new Vec3(0, 0, 265),
|
||||
new DiffuseLight(new Color(7., 7., 7.))
|
||||
));
|
||||
|
||||
// spheres with different materials
|
||||
objects.add(new Sphere(new Vec3(400, 400, 200), 50, new LambertianMaterial(new Color(0.7, 0.3, 0.1))));
|
||||
objects.add(new Sphere(new Vec3(260, 150, 45), 50, new DielectricMaterial(1.5)));
|
||||
objects.add(new Sphere(new Vec3(0, 150, 145), 50, new MetallicMaterial(new Color(0.8, 0.8, 0.9), 1.0)));
|
||||
|
||||
// glass sphere filled with gas
|
||||
var boundary = new Sphere(new Vec3(360, 150, 145), 70, new DielectricMaterial(1.5));
|
||||
objects.add(boundary);
|
||||
objects.add(new ConstantMedium(boundary, 0.2, new IsotropicMaterial(new Color(0.2, 0.4, 0.9))));
|
||||
|
||||
// put the world in a glass sphere
|
||||
objects.add(new ConstantMedium(
|
||||
new Sphere(new Vec3(0, 0, 0), 5000, new DielectricMaterial(1.5)),
|
||||
0.0001, new IsotropicMaterial(new Color(1., 1., 1.))
|
||||
));
|
||||
|
||||
// textures spheres
|
||||
objects.add(new Sphere(new Vec3(400, 200, 400), 100, new LambertianMaterial(new ImageTexture("/earthmap.jpg"))));
|
||||
objects.add(new Sphere(new Vec3(220, 280, 300), 80, new LambertianMaterial(new PerlinTexture(0.2))));
|
||||
|
||||
// box from spheres
|
||||
var white = new LambertianMaterial(new Color(.73, .73, .73));
|
||||
var spheres = new ArrayList<Hittable>();
|
||||
for (int j = 0; j < 1000; j++) {
|
||||
spheres.add(new Sphere(new Vec3(random.nextDouble(165), random.nextDouble(165), random.nextDouble(165)), 10, white));
|
||||
}
|
||||
objects.add(new HittableBinaryTree(spheres).rotateY(Math.toRadians(15)).translate(new Vec3(-100, 270, 395)));
|
||||
|
||||
return new Example(
|
||||
new Scene(objects),
|
||||
SimpleCamera.builder()
|
||||
.withImage(height, height)
|
||||
.withFieldOfView(Math.toRadians(40))
|
||||
.withPosition(new Vec3(478, 278, -600))
|
||||
.withTarget(new Vec3(278, 278, 0))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private static @NotNull SkyBox getSkyBox() {
|
||||
return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE);
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import java.util.Optional;
|
||||
* An axis-aligned bounding box.
|
||||
*/
|
||||
public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
|
||||
public static final AABB UNIVERSE = new AABB(Vec3.MIN, Vec3.MAX);
|
||||
public static final AABB EMPTY = new AABB(Vec3.ZERO, Vec3.ZERO);
|
||||
public static final Comparator<AABB> X_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::x));
|
||||
public static final Comparator<AABB> Y_AXIS = Comparator.comparing(AABB::min, Comparator.comparingDouble(Vec3::y));
|
||||
@ -56,18 +55,10 @@ public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
|
||||
return new AABB(Vec3.min(this.min, box.min), Vec3.max(this.max, box.max));
|
||||
}
|
||||
|
||||
public boolean hit(@NotNull Ray ray) {
|
||||
return intersect(ray).isPresent();
|
||||
}
|
||||
|
||||
public @NotNull Optional<Range> intersect(@NotNull Ray ray) {
|
||||
if (this == UNIVERSE) return Optional.of(Range.UNIVERSE);
|
||||
if (this == EMPTY) return Optional.empty();
|
||||
|
||||
int vmask = ray.vmask();
|
||||
|
||||
public boolean hit(@NotNull Ray ray, @NotNull Range range) {
|
||||
var origin = ray.origin();
|
||||
var invDirection = ray.direction().inv();
|
||||
var direction = ray.direction();
|
||||
var invDirection = direction.inv();
|
||||
|
||||
// calculate t values for intersection points of ray with planes through min
|
||||
var tmin = intersect(min(), origin, invDirection);
|
||||
@ -77,27 +68,24 @@ public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
|
||||
// determine range of t for which the ray is inside this voxel
|
||||
double tlmax = Double.NEGATIVE_INFINITY; // lower limit maximum
|
||||
double tumin = Double.POSITIVE_INFINITY; // upper limit minimum
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
// classify t values as lower or upper limit based on vmask
|
||||
if ((vmask & (1 << i)) == 0) {
|
||||
// classify t values as lower or upper limit based on ray direction
|
||||
if (direction.get(i) >= 0) {
|
||||
// min is lower limit and max is upper limit
|
||||
tlmax = Math.max(tlmax, tmin[i]);
|
||||
tumin = Math.min(tumin, tmax[i]);
|
||||
if (tmin[i] > tlmax) tlmax = tmin[i];
|
||||
if (tmax[i] < tumin) tumin = tmax[i];
|
||||
} else {
|
||||
// max is lower limit and min is upper limit
|
||||
tlmax = Math.max(tlmax, tmax[i]);
|
||||
tumin = Math.min(tumin, tmin[i]);
|
||||
if (tmax[i] > tlmax) tlmax = tmax[i];
|
||||
if (tmin[i] < tumin) tumin = tmin[i];
|
||||
}
|
||||
}
|
||||
|
||||
return tlmax < tumin ? Optional.of(new Range(tlmax, tumin)) : Optional.empty();
|
||||
return tlmax < tumin && tumin >= range.min() && tlmax <= range.max();
|
||||
}
|
||||
|
||||
public static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Ray ray) {
|
||||
return intersect(corner, ray.origin(), ray.direction().inv());
|
||||
}
|
||||
|
||||
private static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Vec3 origin, @NotNull Vec3 invDirection) {
|
||||
public static double @NotNull[] intersect(@NotNull Vec3 corner, @NotNull Vec3 origin, @NotNull Vec3 invDirection) {
|
||||
return new double[] {
|
||||
(corner.x() - origin.x()) * invDirection.x(),
|
||||
(corner.y() - origin.y()) * invDirection.y(),
|
||||
|
@ -146,6 +146,15 @@ public record Vec3(double x, double y, double z) {
|
||||
return div(length());
|
||||
}
|
||||
|
||||
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) {
|
||||
return new Vec3(x, y, z);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.jonahbauer.raytracing.render.canvas;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
@ -1,8 +1,9 @@
|
||||
package eu.jonahbauer.raytracing.render.canvas;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class Image implements Canvas {
|
||||
@ -21,6 +22,16 @@ public final class Image implements Canvas {
|
||||
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
|
||||
public int getWidth() {
|
||||
return width;
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.jonahbauer.raytracing.render.canvas;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
|
@ -2,7 +2,8 @@ package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -10,13 +11,13 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public record DielectricMaterial(double refractionIndex, @NotNull Color albedo) implements Material {
|
||||
public record DielectricMaterial(double refractionIndex, @NotNull Texture texture) implements Material {
|
||||
public DielectricMaterial(double refractionIndex) {
|
||||
this(refractionIndex, Color.WHITE);
|
||||
}
|
||||
|
||||
public DielectricMaterial {
|
||||
Objects.requireNonNull(albedo, "albedo");
|
||||
Objects.requireNonNull(texture, "texture");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -30,7 +31,8 @@ public record DielectricMaterial(double refractionIndex, @NotNull Color albedo)
|
||||
var newDirection = (reflect ? Optional.<Vec3>empty() : Vec3.refract(ray.direction(), hit.normal(), ri))
|
||||
.orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal()));
|
||||
|
||||
return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), albedo));
|
||||
var attenuation = texture.get(hit);
|
||||
return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), attenuation));
|
||||
}
|
||||
|
||||
private double reflectance(double cos) {
|
||||
|
@ -1,14 +1,15 @@
|
||||
package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public record DiffuseLight(@NotNull Color emit) implements Material {
|
||||
public record DiffuseLight(@NotNull Texture texture) implements Material {
|
||||
@Override
|
||||
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
|
||||
return Optional.empty();
|
||||
@ -16,6 +17,6 @@ public record DiffuseLight(@NotNull Color emit) implements Material {
|
||||
|
||||
@Override
|
||||
public @NotNull Color emitted(@NotNull HitResult hit) {
|
||||
return emit;
|
||||
return texture.get(hit);
|
||||
}
|
||||
}
|
||||
|
@ -2,16 +2,22 @@ package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public record IsotropicMaterial(@NotNull Color albedo) implements Material{
|
||||
public record IsotropicMaterial(@NotNull Color albedo) implements Material {
|
||||
@Override
|
||||
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
|
||||
return Optional.of(new ScatterResult(new Ray(hit.position(), Vec3.random(random, true)), albedo()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Texture texture() {
|
||||
return albedo();
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -10,9 +10,9 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public record LambertianMaterial(@NotNull Color albedo) implements Material {
|
||||
public record LambertianMaterial(@NotNull Texture texture) implements Material {
|
||||
public LambertianMaterial {
|
||||
Objects.requireNonNull(albedo, "albedo");
|
||||
Objects.requireNonNull(texture, "texture");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -20,7 +20,8 @@ public record LambertianMaterial(@NotNull Color albedo) implements Material {
|
||||
var newDirection = hit.normal().plus(Vec3.random(random, true));
|
||||
if (newDirection.isNearZero()) newDirection = hit.normal();
|
||||
|
||||
var attenuation = texture.get(hit);
|
||||
var scattered = new Ray(hit.position(), newDirection);
|
||||
return Optional.of(new ScatterResult(scattered, albedo));
|
||||
return Optional.of(new ScatterResult(scattered, attenuation));
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -11,6 +12,8 @@ import java.util.random.RandomGenerator;
|
||||
|
||||
public interface Material {
|
||||
|
||||
@NotNull Texture texture();
|
||||
|
||||
@NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random);
|
||||
|
||||
default @NotNull Color emitted(@NotNull HitResult hit) {
|
||||
|
@ -2,7 +2,7 @@ package eu.jonahbauer.raytracing.render.material;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Texture;
|
||||
import eu.jonahbauer.raytracing.scene.HitResult;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -10,14 +10,14 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Material {
|
||||
public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements Material {
|
||||
|
||||
public MetallicMaterial(@NotNull Color albedo) {
|
||||
this(albedo, 0);
|
||||
public MetallicMaterial(@NotNull Texture texture) {
|
||||
this(texture, 0);
|
||||
}
|
||||
|
||||
public MetallicMaterial {
|
||||
Objects.requireNonNull(albedo, "albedo");
|
||||
Objects.requireNonNull(texture, "texture");
|
||||
if (fuzz < 0 || !Double.isFinite(fuzz)) throw new IllegalArgumentException("fuzz must be non-negative");
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ public record MetallicMaterial(@NotNull Color albedo, double fuzz) implements Ma
|
||||
if (fuzz > 0) {
|
||||
newDirection = newDirection.unit().plus(Vec3.random(random, true).times(fuzz));
|
||||
}
|
||||
return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), albedo));
|
||||
var attenuation = texture.get(hit);
|
||||
return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), attenuation));
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package eu.jonahbauer.raytracing.render.renderer;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import eu.jonahbauer.raytracing.render.camera.Camera;
|
||||
import eu.jonahbauer.raytracing.render.canvas.Canvas;
|
||||
import eu.jonahbauer.raytracing.scene.Scene;
|
||||
|
@ -0,0 +1,22 @@
|
||||
package eu.jonahbauer.raytracing.render.texture;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record CheckerTexture(double scale, @NotNull Texture even, @NotNull Texture odd) implements Texture {
|
||||
|
||||
|
||||
@Override
|
||||
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||
var x = (int) Math.floor(p.x() / scale);
|
||||
var y = (int) Math.floor(p.y() / scale);
|
||||
var z = (int) Math.floor(p.z() / scale);
|
||||
var even = (x + y + z) % 2 == 0;
|
||||
return even ? even().get(u, v, p) : odd().get(u, v, p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUVRequired() {
|
||||
return even.isUVRequired() || odd.isUVRequired();
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
package eu.jonahbauer.raytracing.render;
|
||||
package eu.jonahbauer.raytracing.render.texture;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public record Color(double r, double g, double b) {
|
||||
public record Color(double r, double g, double b) implements Texture {
|
||||
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 RED = new Color(1.0, 0.0, 0.0);
|
||||
@ -68,7 +69,9 @@ public record Color(double r, double g, double b) {
|
||||
}
|
||||
}
|
||||
|
||||
public Color {}
|
||||
public Color(int rgb) {
|
||||
this((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF);
|
||||
}
|
||||
|
||||
public Color(int red, int green, int blue) {
|
||||
this(red / 255f, green / 255f, blue / 255f);
|
||||
@ -86,7 +89,17 @@ public record Color(double r, double g, double b) {
|
||||
return toInt(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUVRequired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int toInt(double value) {
|
||||
return Math.max(0, Math.min(255, (int) (255.99 * value)));
|
||||
return Math.clamp((int) (255.99 * value), 0, 255);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package eu.jonahbauer.raytracing.render.texture;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.canvas.Image;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ImageTexture(@NotNull Image image) implements Texture {
|
||||
|
||||
public ImageTexture {
|
||||
Objects.requireNonNull(image, "image");
|
||||
}
|
||||
|
||||
public ImageTexture(@NotNull BufferedImage image) {
|
||||
this(new Image(image));
|
||||
}
|
||||
|
||||
public ImageTexture(@NotNull String path) {
|
||||
this(read(path));
|
||||
}
|
||||
|
||||
private static @NotNull BufferedImage read(@NotNull String path) {
|
||||
try (var in = Objects.requireNonNull(ImageTexture.class.getResourceAsStream(path))) {
|
||||
return ImageIO.read(in);
|
||||
} catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||
u = Math.clamp(u, 0, 1);
|
||||
v = 1 - Math.clamp(v, 0, 1);
|
||||
int x = (int) (u * (image.getWidth() - 1));
|
||||
int y = (int) (v * (image.getHeight() - 1));
|
||||
return image.get(x, y);
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package eu.jonahbauer.raytracing.render.texture;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.function.DoubleFunction;
|
||||
import java.util.random.RandomGenerator;
|
||||
|
||||
public final class PerlinTexture implements Texture {
|
||||
private static final int POINT_COUNT = 256;
|
||||
private static final @NotNull Random RANDOM = new Random();
|
||||
private static final @NotNull DoubleFunction<Color> GREYSCALE = t -> new Color(t, t, t);
|
||||
|
||||
private final double scale;
|
||||
private final int turbulence;
|
||||
private final @NotNull DoubleFunction<Color> color;
|
||||
|
||||
private final int mask;
|
||||
private final Vec3[] randvec;
|
||||
private final int[] permX;
|
||||
private final int[] permY;
|
||||
private final int[] permZ;
|
||||
|
||||
public PerlinTexture() {
|
||||
this(1.0);
|
||||
}
|
||||
|
||||
public PerlinTexture(double scale) {
|
||||
this(scale, 7);
|
||||
}
|
||||
|
||||
public PerlinTexture(double scale, int turbulence) {
|
||||
this(scale, turbulence, GREYSCALE);
|
||||
}
|
||||
|
||||
public PerlinTexture(double scale, int turbulence, @NotNull DoubleFunction<Color> color) {
|
||||
this(scale, turbulence, color, POINT_COUNT, RANDOM);
|
||||
}
|
||||
|
||||
public PerlinTexture(
|
||||
double scale, int turbulence, @NotNull DoubleFunction<Color> color,
|
||||
int count, @NotNull RandomGenerator random
|
||||
) {
|
||||
if ((count & (count - 1)) != 0) throw new IllegalArgumentException("count must be a power of two");
|
||||
if (turbulence <= 0) throw new IllegalArgumentException("turbulence must be positive");
|
||||
|
||||
this.scale = scale;
|
||||
this.turbulence = turbulence;
|
||||
this.color = Objects.requireNonNull(color, "color");
|
||||
|
||||
this.mask = count - 1;
|
||||
this.randvec = new Vec3[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
this.randvec[i] = Vec3.random(random, true);
|
||||
}
|
||||
this.permX = generatePerm(count, random);
|
||||
this.permY = generatePerm(count, random);
|
||||
this.permZ = generatePerm(count, random);
|
||||
}
|
||||
|
||||
private static int @NotNull[] generatePerm(int count, @NotNull RandomGenerator random) {
|
||||
int[] p = new int[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
p[i] = i;
|
||||
}
|
||||
permutate(p, random);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static void permutate(int @NotNull[] p, @NotNull RandomGenerator random) {
|
||||
for (int i = p.length - 1; i > 0; i--) {
|
||||
int target = random.nextInt(i);
|
||||
int tmp = p[i];
|
||||
p[i] = p[target];
|
||||
p[target] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
public double getNoise(@NotNull Vec3 p) {
|
||||
var x = p.x() * scale;
|
||||
var y = p.y() * scale;
|
||||
var z = p.z() * scale;
|
||||
|
||||
var u = x - Math.floor(x);
|
||||
var v = y - Math.floor(y);
|
||||
var w = z - Math.floor(z);
|
||||
|
||||
int i = (int) Math.floor(x);
|
||||
int j = (int) Math.floor(y);
|
||||
int k = (int) Math.floor(z);
|
||||
|
||||
var c = new Vec3[8];
|
||||
for (int di = 0; di < 2; di++) {
|
||||
for (int dj = 0; dj < 2; dj++) {
|
||||
for (int dk = 0; dk < 2; dk++) {
|
||||
c[di << 2 | dj << 1 | dk] = randvec[permX[(i + di) & mask] ^ permY[(j + dj) & mask] ^ permZ[(k + dk) & mask]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return interpolate(c, u, v, w);
|
||||
}
|
||||
|
||||
public double getNoise(@NotNull Vec3 p, int depth) {
|
||||
var accum = 0.0;
|
||||
var temp = p;
|
||||
var weight = 1.0;
|
||||
|
||||
for (int i = 0; i < depth; i++) {
|
||||
accum = Math.fma(weight, getNoise(temp), accum);
|
||||
weight *= 0.5;
|
||||
temp = temp.times(2);
|
||||
}
|
||||
|
||||
return accum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
|
||||
var noise = getNoise(p, turbulence);
|
||||
var t = Math.fma(0.5, Math.sin(Math.PI * noise), 0.5);
|
||||
return color.apply(t);
|
||||
}
|
||||
|
||||
private static double interpolate(Vec3[] c, double u, double v, double w) {
|
||||
var uu = u * u * Math.fma(-2, u, 3);
|
||||
var vv = v * v * Math.fma(-2, v, 3);
|
||||
var ww = w * w * Math.fma(-2, w, 3);
|
||||
|
||||
var accum = 0.0;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 2; k++) {
|
||||
var vec = c[i << 2 | j << 1 | k];
|
||||
var dot = (u - i) * vec.x() + (v - j) * vec.y() + (w - k) * vec.z();
|
||||
accum += Math.fma(i, uu, (1 - i) * (1 - uu))
|
||||
* Math.fma(j, vv, (1 - j) * (1 - vv))
|
||||
* Math.fma(k, ww, (1 - k) * (1 - ww))
|
||||
* dot;
|
||||
}
|
||||
}
|
||||
}
|
||||
return accum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUVRequired() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
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 {
|
||||
default @NotNull Color get(@NotNull HitResult hit) {
|
||||
return get(hit.u(), hit.v(), hit.position());
|
||||
}
|
||||
|
||||
@NotNull Color get(double u, double v, @NotNull Vec3 p);
|
||||
|
||||
default boolean isUVRequired() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -7,17 +7,18 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HitResult(
|
||||
double t,
|
||||
@NotNull Vec3 position,
|
||||
@NotNull Vec3 normal,
|
||||
@NotNull Material material,
|
||||
boolean frontFace
|
||||
double t, @NotNull Vec3 position, @NotNull Vec3 normal,
|
||||
@NotNull Material material, double u, double v, boolean frontFace
|
||||
) implements Comparable<HitResult> {
|
||||
public HitResult {
|
||||
Objects.requireNonNull(position, "position");
|
||||
normal = normal.unit();
|
||||
}
|
||||
|
||||
public @NotNull HitResult withPositionAndNormal(@NotNull Vec3 position, @NotNull Vec3 normal) {
|
||||
return new HitResult(t, position, normal, material, u, v, frontFace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull HitResult o) {
|
||||
return Double.compare(t, o.t);
|
||||
|
@ -2,7 +2,7 @@ package eu.jonahbauer.raytracing.scene;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.AABB;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree;
|
||||
import eu.jonahbauer.raytracing.scene.util.HittableCollection;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.jonahbauer.raytracing.scene;
|
||||
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@FunctionalInterface
|
||||
|
@ -51,7 +51,10 @@ public abstract class Hittable2D implements Hittable {
|
||||
if (!isInterior(alpha, beta)) return Optional.empty();
|
||||
|
||||
var frontFace = denominator < 0;
|
||||
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.neg(), material, frontFace));
|
||||
return Optional.of(new HitResult(
|
||||
t, position, frontFace ? normal : normal.neg(),
|
||||
material, alpha, beta, frontFace
|
||||
));
|
||||
}
|
||||
|
||||
protected abstract boolean isInterior(double alpha, double beta);
|
||||
|
140
src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Box.java
Normal file
140
src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Box.java
Normal file
@ -0,0 +1,140 @@
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record Box(@NotNull AABB box, @NotNull Material material) implements Hittable {
|
||||
|
||||
public Box {
|
||||
Objects.requireNonNull(box, "box");
|
||||
Objects.requireNonNull(material, "material");
|
||||
}
|
||||
|
||||
public Box(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Material material) {
|
||||
this(new AABB(a, b), material);
|
||||
}
|
||||
|
||||
@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) {
|
||||
boolean frontFace;
|
||||
double t;
|
||||
if (range.surrounds(tmin)) {
|
||||
frontFace = true;
|
||||
t = tmin;
|
||||
} else if (range.surrounds(tmax)) {
|
||||
frontFace = false;
|
||||
t = tmax;
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
var side = frontFace ? entry : exit;
|
||||
var normal = frontFace ? side.normal : side.normal.neg();
|
||||
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, material, u, v, frontFace));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull AABB getBoundingBox() {
|
||||
return box;
|
||||
}
|
||||
|
||||
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());
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNul
|
||||
if (hitDistance > distance) return Optional.empty();
|
||||
|
||||
var t = tmin + hitDistance / length;
|
||||
return Optional.of(new HitResult(t, ray.at(t), Vec3.UNIT_X, material, true)); // arbitrary normal and frontFace
|
||||
return Optional.of(new HitResult(t, ray.at(t), Vec3.UNIT_X, material, 0, 0, true)); // arbitrary normal and frontFace
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -49,9 +49,25 @@ public final class Sphere implements Hittable {
|
||||
if (!range.surrounds(t)) return Optional.empty();
|
||||
|
||||
var position = ray.at(t);
|
||||
var normal = position.minus(center);
|
||||
var normal = position.minus(center).div(radius);
|
||||
var frontFace = normal.times(ray.direction()) < 0;
|
||||
return Optional.of(new HitResult(t, position, frontFace ? normal : normal.times(-1), material, frontFace));
|
||||
|
||||
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(),
|
||||
material, u, v, frontFace
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -79,7 +79,7 @@ public final class RotateY extends Transform {
|
||||
-sin * normal.x() + cos * normal.z()
|
||||
);
|
||||
|
||||
return new HitResult(result.t(), newPosition, newNormal, result.material(), result.frontFace());
|
||||
return result.withPositionAndNormal(newPosition, newNormal);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -29,13 +29,7 @@ public final class Translate extends Transform {
|
||||
|
||||
@Override
|
||||
protected @NotNull HitResult transform(@NotNull HitResult result) {
|
||||
return new HitResult(
|
||||
result.t(),
|
||||
result.position().plus(offset),
|
||||
result.normal(),
|
||||
result.material(),
|
||||
result.frontFace()
|
||||
);
|
||||
return result.withPositionAndNormal(result.position().plus(offset), result.normal());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -47,7 +47,7 @@ public final class HittableBinaryTree extends HittableCollection {
|
||||
|
||||
@Override
|
||||
public void hit(@NotNull Ray ray, @NotNull State state) {
|
||||
if (!bbox.hit(ray)) return;
|
||||
if (!bbox.hit(ray, state.getRange())) return;
|
||||
if (left instanceof HittableCollection coll) {
|
||||
coll.hit(ray, state);
|
||||
} else if (left != null) {
|
||||
|
@ -41,6 +41,10 @@ public abstract class HittableCollection implements Hittable {
|
||||
this.range = Objects.requireNonNull(range);
|
||||
}
|
||||
|
||||
public @NotNull Range getRange() {
|
||||
return range;
|
||||
}
|
||||
|
||||
private @NotNull Optional<HitResult> getResult() {
|
||||
return Optional.ofNullable(result);
|
||||
}
|
||||
|
@ -1,253 +0,0 @@
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
BIN
src/main/resources/earthmap.jpg
Normal file
BIN
src/main/resources/earthmap.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 158 KiB |
@ -1,6 +1,6 @@
|
||||
package eu.jonahbauer.raytracing.render.canvas;
|
||||
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import eu.jonahbauer.raytracing.render.ImageFormat;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
@ -3,7 +3,7 @@ package eu.jonahbauer.raytracing.scene.hittable3d;
|
||||
import eu.jonahbauer.raytracing.math.Range;
|
||||
import eu.jonahbauer.raytracing.math.Ray;
|
||||
import eu.jonahbauer.raytracing.math.Vec3;
|
||||
import eu.jonahbauer.raytracing.render.Color;
|
||||
import eu.jonahbauer.raytracing.render.texture.Color;
|
||||
import eu.jonahbauer.raytracing.render.material.LambertianMaterial;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user