add probability density function based materials

main
jbb01 6 months ago
parent 5f1e816edd
commit 6b47f44ad2

@ -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) {

@ -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 {

@ -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<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()));
return Optional.of(new PdfScatterResult(albedo(), new SphereProbabilityDensityFunction()));
}
@Override

@ -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<ScatterResult> 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())));
}
}

@ -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<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random);
/**
* {@return the color emitted for a given hit}
* @implSpec the default implementation returns {@linkplain Color#BLACK black}, i.e. no emission
*/
default @NotNull Color emitted(@NotNull HitResult hit) {
return Color.BLACK;
}
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");
}
}
}

@ -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)));
}
}

@ -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;

@ -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;
}
}

@ -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);
}
}
}

@ -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);
}

@ -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);
}
}
Loading…
Cancel
Save