diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java index 6813920..8667966 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java @@ -32,7 +32,7 @@ public record DielectricMaterial(double refractionIndex, @NotNull Texture textur .orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal())); var attenuation = texture.get(hit); - return Optional.of(new ScatterResult(new Ray(hit.position(), newDirection), attenuation)); + return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection))); } private double reflectance(double cos) { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java index 05c04ba..091c802 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java @@ -18,6 +18,7 @@ public final class DirectionalMaterial implements Material { 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( @@ -39,7 +40,7 @@ public final class DirectionalMaterial implements Material { if (back != null) return back.scatter(ray, hit, random); } // let the ray pass through without obstruction - return Optional.of(new ScatterResult(new Ray(ray.at(hit.t()), ray.direction()), Color.WHITE)); + return Optional.of(new SpecularScatterResult(Color.WHITE, new Ray(ray.at(hit.t()), ray.direction()))); } @Override @@ -49,7 +50,7 @@ public final class DirectionalMaterial implements Material { } else { if (back != null) return back.emitted(hit); } - return Color.BLACK; + return Material.super.emitted(hit); } private record DirectionalTexture(@Nullable Texture front, @Nullable Texture back) implements Texture { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java index 4ab3d88..08b0f29 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java @@ -1,7 +1,7 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; -import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.render.renderer.pdf.SphereProbabilityDensityFunction; import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; @@ -13,7 +13,7 @@ import java.util.random.RandomGenerator; public record IsotropicMaterial(@NotNull Color albedo) implements Material { @Override public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { - return Optional.of(new ScatterResult(new Ray(hit.position(), Vec3.random(random, true)), albedo())); + return Optional.of(new PdfScatterResult(albedo(), new SphereProbabilityDensityFunction())); } @Override diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java index cd701d7..ffa218e 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java @@ -1,7 +1,7 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; -import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.render.renderer.pdf.CosineProbabilityDensityFunction; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; @@ -17,11 +17,7 @@ public record LambertianMaterial(@NotNull Texture texture) implements Material { @Override public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { - 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, attenuation)); + return Optional.of(new PdfScatterResult(attenuation, new CosineProbabilityDensityFunction(hit.normal()))); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java b/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java index 7b62125..f5f8ba6 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java @@ -1,6 +1,7 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; +import eu.jonahbauer.raytracing.render.renderer.pdf.ProbabilityDensityFunction; import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; @@ -12,18 +13,59 @@ import java.util.random.RandomGenerator; public interface Material { + /** + * {@return the texture associated with this material} + */ @NotNull Texture texture(); + /** + * Scatters a light ray after it hit a surface. + * @param ray the incoming light ray + * @param hit information about the light ray hitting some object + * @param random a random number generator + * @return a {@code ScatterResult} if the ray is scattered or an {@linkplain Optional#empty() empty optional} if the + * ray is absorbed. + */ @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random); + /** + * {@return the color emitted for a given hit} + * @implSpec the default implementation returns {@linkplain Color#BLACK black}, i.e. no emission + */ default @NotNull Color emitted(@NotNull HitResult hit) { return Color.BLACK; } - record ScatterResult(@NotNull Ray ray, @NotNull Color attenuation) { - public ScatterResult { + /** + * The result of a {@linkplain Material#scatter(Ray, HitResult, RandomGenerator) scattering operation}. + */ + sealed interface ScatterResult {} + + /** + * The result of a specular {@linkplain #scatter(Ray, HitResult, RandomGenerator) scattering operation}. A + * specular is a scattering operation with a very small number of possible scattered rays (like a + * perfect reflection which only has one possible scattered ray). + * @param attenuation the attenuation of the scattered light ray + * @param ray the scattered light ray + */ + record SpecularScatterResult(@NotNull Color attenuation, @NotNull Ray ray) implements ScatterResult { + public SpecularScatterResult { + Objects.requireNonNull(attenuation, "attenuation"); Objects.requireNonNull(ray, "ray"); + } + } + + /** + * The result of a probability density function based + * {@linkplain #scatter(Ray, HitResult, RandomGenerator) scattering operation}. A probability density function + * based scattering operation uses a probability density function to determine the scatter direction. + * @param attenuation the attenuation of the scattered light ray + * @param pdf the probability density function + */ + record PdfScatterResult(@NotNull Color attenuation, @NotNull ProbabilityDensityFunction pdf) implements ScatterResult { + public PdfScatterResult { Objects.requireNonNull(attenuation, "attenuation"); + Objects.requireNonNull(pdf, "pdf"); } } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java index a89b0a0..82387ff 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java @@ -28,6 +28,6 @@ public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements 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), attenuation)); + return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection))); } } 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 d525ac4..961091f 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java @@ -2,6 +2,9 @@ package eu.jonahbauer.raytracing.render.renderer; import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; +import eu.jonahbauer.raytracing.render.material.Material; +import eu.jonahbauer.raytracing.render.renderer.pdf.HittableProbabilityDensityFunction; +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.canvas.Canvas; @@ -131,12 +134,24 @@ public final class SimpleRenderer implements Renderer { var hit = optional.get(); var material = hit.material(); var emitted = material.emitted(hit); - var scatter = material.scatter(ray, hit, random); + var result = material.scatter(ray, hit, random); color = Color.add(color, Color.multiply(attenuation, emitted)); if (scatter.isEmpty()) break; attenuation = Color.multiply(attenuation, scatter.get().attenuation()); ray = scatter.get().ray(); + if (result.isEmpty()) break; + + switch (result.get()) { + case Material.SpecularScatterResult(var a, var scattered) -> { + attenuation = Color.multiply(attenuation, a); + ray = scattered; + } + case Material.PdfScatterResult(var a, var pdf) -> { + attenuation = Color.multiply(attenuation, a); + ray = new Ray(hit.position(), pdf.generate(random)); + } + } } return color; diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/CosineProbabilityDensityFunction.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/CosineProbabilityDensityFunction.java new file mode 100644 index 0000000..306547e --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/CosineProbabilityDensityFunction.java @@ -0,0 +1,27 @@ +package eu.jonahbauer.raytracing.render.renderer.pdf; + +import eu.jonahbauer.raytracing.math.Vec3; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.random.RandomGenerator; + +public record CosineProbabilityDensityFunction(@NotNull Vec3 normal) implements ProbabilityDensityFunction { + + public CosineProbabilityDensityFunction { + Objects.requireNonNull(normal, "normal"); + normal = normal.unit(); + } + + @Override + public double value(@NotNull Vec3 direction) { + var cos = normal.times(direction.unit()); + return Math.max(0, cos / Math.PI); + } + + @Override + public @NotNull Vec3 generate(@NotNull RandomGenerator random) { + var out = normal().plus(Vec3.random(random, true)); + return out.isNearZero() ? normal() : out; + } +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/MixtureProbabilityDensityFunction.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/MixtureProbabilityDensityFunction.java new file mode 100644 index 0000000..d45a915 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/MixtureProbabilityDensityFunction.java @@ -0,0 +1,37 @@ +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 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) { + return weight * a.value(direction) + (1 - weight) * b.value(direction); + } + + @Override + public @NotNull Vec3 generate(@NotNull RandomGenerator random) { + if (random.nextDouble() < weight) { + return a.generate(random); + } else { + return b.generate(random); + } + } +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/ProbabilityDensityFunction.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/ProbabilityDensityFunction.java new file mode 100644 index 0000000..3200c1a --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/ProbabilityDensityFunction.java @@ -0,0 +1,11 @@ +package eu.jonahbauer.raytracing.render.renderer.pdf; + +import eu.jonahbauer.raytracing.math.Vec3; +import org.jetbrains.annotations.NotNull; + +import java.util.random.RandomGenerator; + +public interface ProbabilityDensityFunction { + double value(@NotNull Vec3 direction); + @NotNull Vec3 generate(@NotNull RandomGenerator random); +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/SphereProbabilityDensityFunction.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/SphereProbabilityDensityFunction.java new file mode 100644 index 0000000..30727bb --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/SphereProbabilityDensityFunction.java @@ -0,0 +1,19 @@ +package eu.jonahbauer.raytracing.render.renderer.pdf; + +import eu.jonahbauer.raytracing.math.Vec3; +import org.jetbrains.annotations.NotNull; + +import java.util.random.RandomGenerator; + +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, true); + } +}