diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java index 8111102..699743a 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Main.java +++ b/src/main/java/eu/jonahbauer/raytracing/Main.java @@ -13,6 +13,8 @@ import java.nio.file.Path; import java.util.function.IntFunction; public class Main { + public static final boolean DEBUG = false; + public static void main(String[] args) throws IOException { var config = Config.parse(args); var example = config.example; diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java b/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java index a0b728a..7870fc5 100644 --- a/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java +++ b/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java @@ -178,4 +178,9 @@ public record Vec3(double x, double y, double z) { public @NotNull Vec3 withZ(double z) { return new Vec3(x, y, z); } + + @Override + public @NotNull String toString() { + return "(" + x + "," + y + "," + z + ")"; + } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java index 54dc744..e3c6a1e 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java @@ -16,6 +16,8 @@ import java.util.SplittableRandom; import java.util.random.RandomGenerator; import java.util.stream.IntStream; +import static eu.jonahbauer.raytracing.Main.DEBUG; + public final class SimpleRenderer implements Renderer { private final int sqrtSamplesPerPixel; private final int maxDepth; @@ -104,6 +106,9 @@ public final class SimpleRenderer implements Renderer { 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); } @@ -127,36 +132,75 @@ public final class SimpleRenderer implements Renderer { while (depth-- > 0) { var optional = scene.hit(ray, new Range(0.001, Double.POSITIVE_INFINITY)); if (optional.isEmpty()) { - color = Color.add(color, Color.multiply(attenuation, scene.getBackgroundColor(ray))); + var background = scene.getBackgroundColor(ray); + color = Color.add(color, Color.multiply(attenuation, background)); + if (DEBUG) { + System.out.println(" Hit background: " + background); + } break; } var hit = optional.get(); + if (DEBUG) { + System.out.println(" Hit " + hit.target() + " at t=" + hit.t() + " (" + hit.position() + ")"); + } var material = hit.material(); var emitted = material.emitted(hit); + if (DEBUG && !Color.BLACK.equals(emitted)) { + System.out.println(" Emitted: " + emitted); + } + var result = material.scatter(ray, hit, random); color = Color.add(color, Color.multiply(attenuation, emitted)); - if (result.isEmpty()) break; + if (result.isEmpty()) { + if (DEBUG) { + System.out.println(" Absorbed"); + } + break; + } switch (result.get()) { case Material.SpecularScatterResult(var a, var scattered) -> { attenuation = Color.multiply(attenuation, a); ray = scattered; + + if (DEBUG) { + System.out.println(" Specular scattering with albedo " + a); + } } case Material.PdfScatterResult(var a, var pdf) -> { if (scene.getLights() == 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.getLights()), pdf); + var mixed = new MixtureProbabilityDensityFunction(new TargetingProbabilityDensityFunction(hit.position(), scene.getLights()), pdf, 0.5); var direction = mixed.generate(random); var factor = pdf.value(direction) / mixed.value(direction); 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; diff --git a/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java b/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java index 991badf..511d72f 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java @@ -5,6 +5,8 @@ import org.jetbrains.annotations.NotNull; import java.util.Random; +import static eu.jonahbauer.raytracing.Main.DEBUG; + 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); @@ -82,6 +84,17 @@ public record Color(double r, double g, double b) implements Texture { 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() { return toInt(r); } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/HitResult.java b/src/main/java/eu/jonahbauer/raytracing/scene/HitResult.java index 1483782..cf21baf 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/HitResult.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/HitResult.java @@ -7,7 +7,7 @@ import org.jetbrains.annotations.NotNull; import java.util.Objects; public record HitResult( - double t, @NotNull Vec3 position, @NotNull Vec3 normal, + double t, @NotNull Vec3 position, @NotNull Vec3 normal, @NotNull Hittable target, @NotNull Material material, double u, double v, boolean isFrontFace ) implements Comparable { public HitResult { @@ -16,7 +16,7 @@ public record HitResult( } public @NotNull HitResult withPositionAndNormal(@NotNull Vec3 position, @NotNull Vec3 normal) { - return new HitResult(t, position, normal, material, u, v, isFrontFace); + return new HitResult(t, position, normal, target, material, u, v, isFrontFace); } @Override diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java index 74bb50a..23282b6 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java @@ -52,10 +52,15 @@ public abstract class Hittable2D implements Hittable { var frontFace = denominator < 0; return Optional.of(new HitResult( - t, position, frontFace ? normal : normal.neg(), + t, position, frontFace ? normal : normal.neg(), this, material, alpha, beta, frontFace )); } protected abstract boolean isInterior(double alpha, double beta); + + @Override + public @NotNull String toString() { + return this.getClass().getSimpleName() + "(origin=" + origin + ", u=" + u + ", v=" + v + ")"; + } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Box.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Box.java index 14169af..8df9f0d 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Box.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Box.java @@ -110,7 +110,7 @@ public final class Box implements Hittable, Target { 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)); + return Optional.of(new HitResult(t, position, normal, this, material, u, v, frontFace)); } @Override @@ -173,6 +173,11 @@ public final class Box implements Hittable, Target { && 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()), diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/ConstantMedium.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/ConstantMedium.java index bf104a4..eb0613e 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/ConstantMedium.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/ConstantMedium.java @@ -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, 0, 0, true)); // arbitrary normal, u, v and isFrontFace + return Optional.of(new HitResult(t, ray.at(t), Vec3.UNIT_X, this, material, 0, 0, true)); // arbitrary normal, u, v and isFrontFace } @Override diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java index c1d0ecf..1802dd6 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java @@ -67,7 +67,7 @@ public final class Sphere implements Hittable, Target { } return Optional.of(new HitResult( - t, position, frontFace ? normal : normal.neg(), + t, position, frontFace ? normal : normal.neg(), this, material, u, v, frontFace )); } @@ -97,4 +97,9 @@ public final class Sphere implements Hittable, Target { return target.times(radius).plus(center).minus(origin); } + + @Override + public @NotNull String toString() { + return "Sphere(center=" + center + ", radius=" + radius + ")"; + } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java b/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java index 0084202..b250185 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java @@ -89,6 +89,11 @@ public sealed class RotateY extends Transform { 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) { diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java b/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java index 8eb2cb7..db8a805 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java @@ -48,6 +48,11 @@ public sealed class Translate extends Transform { 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);