adjust rendering pipeline for spectral rendering

feature/spectral
jbb01 5 months ago
parent ddc861138a
commit 00fbf4e4f1

@ -1,8 +1,11 @@
package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpaces;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBIlluminantSpectrum;
import eu.jonahbauer.raytracing.render.texture.CheckerTexture;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
import eu.jonahbauer.raytracing.render.material.*;
import eu.jonahbauer.raytracing.render.texture.ImageTexture;
@ -49,13 +52,14 @@ public class Examples {
public static @NotNull Example getSimpleScene(int height) {
if (height <= 0) height = 675;
var cs = ColorSpaces.sRGB;
return new Example(
new Scene(getSkyBox(), List.of(
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, -100.5, -1.0), 100.0, new LambertianMaterial(new ColorRGB(0.8, 0.8, 0.0), cs)),
new Sphere(new Vec3(0, 0, -1.2), 0.5, new LambertianMaterial(new ColorRGB(0.1, 0.2, 0.5), cs)),
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.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 ColorRGB(0.8, 0.6, 0.2), cs, 0.0))
)),
SimpleCamera.builder()
.withImage(height * 16 / 9, height)
@ -66,11 +70,16 @@ public class Examples {
public static @NotNull Example getSpheres(int height) {
if (height <= 0) height = 675;
var cs = ColorSpaces.sRGB;
var rng = new Random(1);
var objects = new ArrayList<Hittable>();
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)))
new LambertianMaterial(new CheckerTexture(0.32,
new RGBAlbedoSpectrum(cs, new ColorRGB(.2, .3, .1)),
new RGBAlbedoSpectrum(cs, new ColorRGB(.9, .9, .9))
))
));
for (int a = -11; a < 11; a++) {
@ -82,13 +91,13 @@ public class Examples {
var rnd = rng.nextDouble();
if (rnd < 0.8) {
// diffuse
var albedo = Color.random(rng).times(Color.random(rng));
material = new LambertianMaterial(albedo);
var albedo = ColorRGB.random(rng).times(ColorRGB.random(rng));
material = new LambertianMaterial(albedo, cs);
} else if (rnd < 0.95) {
// metal
var albedo = Color.random(rng, 0.5, 1.0);
var albedo = ColorRGB.random(rng, 0.5, 1.0);
var fuzz = rng.nextDouble() * 0.5;
material = new MetallicMaterial(albedo, fuzz);
material = new MetallicMaterial(albedo, cs, fuzz);
} else {
// glass
material = new DielectricMaterial(1.5);
@ -99,8 +108,8 @@ public class Examples {
}
objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5)));
objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1))));
objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5))));
objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new ColorRGB(0.4, 0.2, 0.1), cs)));
objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new ColorRGB(0.7, 0.6, 0.5), cs)));
var camera = SimpleCamera.builder()
.withImage(height * 16 / 9, height)
@ -116,13 +125,14 @@ public class Examples {
public static @NotNull Example getSquares(int height) {
if (height <= 0) height = 600;
var cs = ColorSpaces.sRGB;
return new Example(
new Scene(getSkyBox(), List.of(
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(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, 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(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(new ColorRGB(1.0, 0.2, 0.2), cs)),
new Parallelogram(new Vec3(-2, -2, 0), new Vec3(4, 0, 0), new Vec3(0, 4, 0), new LambertianMaterial(new ColorRGB(0.2, 1.0, 0.2), cs)),
new Parallelogram(new Vec3(3, -2, 1), new Vec3(0, 0, 4), new Vec3(0, 4, 0), new LambertianMaterial(new ColorRGB(0.2, 0.2, 1.0), cs)),
new Parallelogram(new Vec3(-2, 3, 1), new Vec3(4, 0, 0), new Vec3(0, 0, 4), new LambertianMaterial(new ColorRGB(1.0, 0.5, 0.0), cs)),
new Parallelogram(new Vec3(-2, -3, 5), new Vec3(4, 0, 0), new Vec3(0, 0, -4), new LambertianMaterial(new ColorRGB(0.2, 0.8, 0.8), cs))
)),
SimpleCamera.builder()
.withImage(height, height)
@ -135,12 +145,13 @@ public class Examples {
public static @NotNull Example getLight(int height) {
if (height <= 0) height = 225;
var cs = ColorSpaces.sRGB;
return new Example(
new Scene(List.of(
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 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, -1000, 0), 1000, new LambertianMaterial(new ColorRGB(0.2, 0.2, 0.2), cs)),
new Sphere(new Vec3(0, 2, 0), 2, new LambertianMaterial(new ColorRGB(0.2, 0.2, 0.2), cs)),
new Parallelogram(new Vec3(3, 1, -2), new Vec3(2, 0, 0), new Vec3(0, 2, 0), new DiffuseLight(new ColorRGB(4.0, 4.0, 4.0), cs)),
new Sphere(new Vec3(0, 7, 0), 2, new DiffuseLight(new ColorRGB(4.0, 4.0, 4.0), cs))
)),
SimpleCamera.builder()
.withImage(height * 16 / 9, height)
@ -154,10 +165,11 @@ public class Examples {
public static @NotNull Example getCornellBox(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(15.0, 15.0, 15.0));
var cs = ColorSpaces.sRGB;
var red = new LambertianMaterial(new ColorRGB(.65, .05, .05), cs);
var white = new LambertianMaterial(new ColorRGB(.73, .73, .73), cs);
var green = new LambertianMaterial(new ColorRGB(.12, .45, .15), cs);
var light = new DiffuseLight(new ColorRGB(15.0, 15.0, 15.0), cs);
return new Example(
new Scene(List.of(
@ -181,10 +193,11 @@ public class Examples {
public static @NotNull Example getCornellBoxSmoke(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 cs = ColorSpaces.sRGB;
var red = new LambertianMaterial(new ColorRGB(.65, .05, .05), cs);
var white = new LambertianMaterial(new ColorRGB(.73, .73, .73), cs);
var green = new LambertianMaterial(new ColorRGB(.12, .45, .15), cs);
var light = new DiffuseLight(new ColorRGB(15.0, 15.0, 15.0), cs);
return new Example(
new Scene(List.of(
@ -194,13 +207,13 @@ public class Examples {
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)
0.01, new IsotropicMaterial(ColorRGB.BLACK, cs)
),
new ConstantMedium(
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)
0.01, new IsotropicMaterial(ColorRGB.WHITE, cs)
)
)),
SimpleCamera.builder()
@ -215,10 +228,11 @@ public class Examples {
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 cs = ColorSpaces.sRGB;
var red = new LambertianMaterial(new ColorRGB(.65, .05, .05), cs);
var white = new LambertianMaterial(new ColorRGB(.73, .73, .73), cs);
var green = new LambertianMaterial(new ColorRGB(.12, .45, .15), cs);
var light = new DiffuseLight(new ColorRGB(7.0, 7.0, 7.0), cs);
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);
@ -242,17 +256,18 @@ public class Examples {
public static @NotNull Example getDiagramm(int height) {
if (height <= 0) height = 450;
record Partei(String name, Color color, double stimmen) { }
var cs = ColorSpaces.sRGB;
record Partei(String name, ColorRGB color, double stimmen) { }
var data = List.of(
new Partei("CDU", new Color(0x00, 0x4B, 0x76), 18.9),
new Partei("SPD", new Color(0xC0, 0x00, 0x3C), 25.7),
new Partei("AfD", new Color(0x80, 0xCD, 0xEC), 10.3),
new Partei("FDP", new Color(0xF7, 0xBB, 0x3D), 11.5),
new Partei("DIE LINKE", new Color(0x5F, 0x31, 0x6E), 4.9),
new Partei("GRÜNE", new Color(0x00, 0x85, 0x4A), 14.8),
new Partei("CSU", new Color(0x00, 0x77, 0xB6), 5.2)
new Partei("CDU", new ColorRGB(0x00, 0x4B, 0x76), 18.9),
new Partei("SPD", new ColorRGB(0xC0, 0x00, 0x3C), 25.7),
new Partei("AfD", new ColorRGB(0x80, 0xCD, 0xEC), 10.3),
new Partei("FDP", new ColorRGB(0xF7, 0xBB, 0x3D), 11.5),
new Partei("DIE LINKE", new ColorRGB(0x5F, 0x31, 0x6E), 4.9),
new Partei("GRÜNE", new ColorRGB(0x00, 0x85, 0x4A), 14.8),
new Partei("CSU", new ColorRGB(0x00, 0x77, 0xB6), 5.2)
);
var white = new LambertianMaterial(new Color(.99, .99, .99));
var white = new LambertianMaterial(new ColorRGB(.99, .99, .99), cs);
var count = data.size();
var size = 75d;
@ -272,12 +287,12 @@ public class Examples {
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())
new DielectricMaterial(1.5, partei.color(), cs)
));
}
return new Example(
new Scene(new Color(1.25, 1.25, 1.25), objects),
new Scene(cs.illuminant().scale(1.25), objects),
SimpleCamera.builder()
.withImage(height * 16 / 9, height)
.withPosition(new Vec3(700, 250, 800))
@ -292,7 +307,7 @@ public class Examples {
return new Example(
new Scene(getSkyBox(), List.of(
new Sphere(Vec3.ZERO, 2, new LambertianMaterial(new ImageTexture("/eu/jonahbauer/raytracing/textures/earthmap.jpg")))
new Sphere(Vec3.ZERO, 2, new LambertianMaterial(new ImageTexture("earthmap.jpg", ColorSpaces.sRGB)))
)),
SimpleCamera.builder()
.withImage(height * 16 / 9, height)
@ -325,12 +340,13 @@ public class Examples {
public static @NotNull Example getFinal(int height) {
if (height <= 0) height = 400;
var cs = ColorSpaces.sRGB;
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));
var ground = new LambertianMaterial(new ColorRGB(0.48, 0.83, 0.53), cs);
for (int i = 0; i < 20; i++) {
for (int j = 0; j < 20; j++) {
var w = 100.0;
@ -348,31 +364,31 @@ public class Examples {
// 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.))
new DiffuseLight(new ColorRGB(7., 7., 7.), cs)
));
// 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(400, 400, 200), 50, new LambertianMaterial(new ColorRGB(0.7, 0.3, 0.1), cs)));
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)));
objects.add(new Sphere(new Vec3(0, 150, 145), 50, new MetallicMaterial(new ColorRGB(0.8, 0.8, 0.9), cs, 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))));
objects.add(new ConstantMedium(boundary, 0.2, new IsotropicMaterial(new ColorRGB(0.2, 0.4, 0.9), cs)));
// 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.))
0.0001, new IsotropicMaterial(new ColorRGB(1., 1., 1.), cs)
));
// textures spheres
objects.add(new Sphere(new Vec3(400, 200, 400), 100, new LambertianMaterial(new ImageTexture("/eu/jonahbauer/raytracing/textures/earthmap.jpg"))));
objects.add(new Sphere(new Vec3(400, 200, 400), 100, new LambertianMaterial(new ImageTexture("earthmap.jpg", cs))));
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 white = new LambertianMaterial(new ColorRGB(.73, .73, .73), cs);
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));
@ -391,6 +407,9 @@ public class Examples {
}
private static @NotNull SkyBox getSkyBox() {
return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE);
return SkyBox.gradient(
new RGBIlluminantSpectrum(ColorSpaces.sRGB, new ColorRGB(0.5, 0.7, 1.0)),
ColorSpaces.sRGB.illuminant()
);
}
}

@ -2,9 +2,12 @@ package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.render.ImageFormat;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.canvas.Image;
import eu.jonahbauer.raytracing.render.canvas.LiveCanvas;
import eu.jonahbauer.raytracing.render.canvas.XYZCanvas;
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpaces;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
@ -30,11 +33,11 @@ public class Main {
Canvas canvas;
if (config.preview) {
var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight()));
var image = new LiveCanvas(new XYZCanvas(camera.getWidth(), camera.getHeight()), ColorSpaces.sRGB);
image.preview();
canvas = image;
} else {
canvas = new Image(camera.getWidth(), camera.getHeight());
canvas = new XYZCanvas(camera.getWidth(), camera.getHeight());
}
long time = System.nanoTime();

@ -1,13 +1,19 @@
package eu.jonahbauer.raytracing.math;
import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction, @NotNull SampledWavelengths lambda) {
public Ray {
Objects.requireNonNull(origin, "origin");
Objects.requireNonNull(direction, "direction");
Objects.requireNonNull(lambda, "lambda");
}
public Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
this(origin, direction, SampledWavelengths.EMPTY);
}
public @NotNull Vec3 at(double t) {

@ -1,7 +1,7 @@
package eu.jonahbauer.raytracing.render;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.canvas.Image;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpaces;
import org.jetbrains.annotations.NotNull;
import java.io.*;
@ -11,7 +11,6 @@ import java.nio.file.Path;
import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public enum ImageFormat {
PPM {
@ -24,9 +23,9 @@ public enum ImageFormat {
writer.write(String.valueOf(image.getHeight()));
writer.write("\n255\n");
var it = image.pixels().iterator();
while (it.hasNext()) {
var color = it.next();
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
var color = image.getRGB(x, y, ColorSpaces.sRGB);
writer.write(String.valueOf(color.red()));
writer.write(" ");
writer.write(String.valueOf(color.green()));
@ -36,6 +35,7 @@ public enum ImageFormat {
}
}
}
}
},
PNG {
private static final byte[] MAGIC = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
@ -84,15 +84,16 @@ public enum ImageFormat {
idat.writeInt(IDAT_TYPE);
try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) {
var pixels = image.pixels().iterator();
for (int i = 0; pixels.hasNext(); i = (i + 1) % image.getWidth()) {
if (i == 0) deflate.writeByte(0); // filter type
var pixel = pixels.next();
for (int y = 0; y < image.getHeight(); y++) {
deflate.writeByte(0); // filter type
for (int x = 0; x < image.getWidth(); x++) {
var pixel = image.getRGB(x, y, ColorSpaces.sRGB);
deflate.writeByte(pixel.red());
deflate.writeByte(pixel.green());
deflate.writeByte(pixel.blue());
}
}
}
var bytes = baos.toByteArray();
data.writeInt(bytes.length - 4); // don't include type in length

@ -2,6 +2,7 @@ package eu.jonahbauer.raytracing.render.camera;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -93,7 +94,7 @@ public final class SimpleCamera implements Camera {
var origin = getRayOrigin(random);
var target = getRayTarget(x, y, i, j, n, random);
return new Ray(origin, target.minus(origin));
return new Ray(origin, target.minus(origin), SampledWavelengths.uniform(random.nextDouble()));
}
/**

@ -1,22 +1,37 @@
package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public interface Canvas {
/**
* {@return the width of this canvas}
*/
int getWidth();
/**
* {@return the height of this canvas}
*/
int getHeight();
void set(int x, int y, @NotNull Color color);
@NotNull Color get(int x, int y);
/**
* Adds a sample to this canvas
* @param x the pixel x coordinate
* @param y the pixel y coordinate
* @param n the index of the sample
* @param spectrum the sampled spectrum
* @param lambda the sampled wavelengths
*/
void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda);
default @NotNull Stream<Color> pixels() {
return IntStream.range(0, getHeight())
.mapToObj(y -> IntStream.range(0, getWidth()).mapToObj(x -> get(x, y)))
.flatMap(Function.identity());
}
/**
* {@return the color at a given pixel}
* @param x the pixel x coordinate
* @param y the pixel y coordinate
* @param cs the color space of the output
*/
@NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs);
}

@ -1,58 +0,0 @@
package eu.jonahbauer.raytracing.render.canvas;
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 {
private final int width;
private final int height;
private final Color[][] data;
public Image(int width, int height) {
this.width = width;
this.height = height;
if (width <= 0) throw new IllegalArgumentException("width must be positive");
if (height <= 0) throw new IllegalArgumentException("height must be positive");
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;
}
@Override
public int getHeight() {
return height;
}
@Override
public @NotNull Color get(int x, int y) {
Objects.checkIndex(x, width);
Objects.checkIndex(y, height);
return Objects.requireNonNullElse(this.data[y][x], Color.BLACK);
}
@Override
public void set(int x, int y, @NotNull Color color) {
Objects.checkIndex(x, width);
Objects.checkIndex(y, height);
this.data[y][x] = Objects.requireNonNull(color);
}
}

@ -1,6 +1,9 @@
package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@ -12,10 +15,12 @@ import java.awt.image.BufferedImage;
public final class LiveCanvas implements Canvas {
private final @NotNull Canvas delegate;
private final @NotNull BufferedImage image;
private final @NotNull ColorSpace cs;
public LiveCanvas(@NotNull Canvas delegate) {
public LiveCanvas(@NotNull Canvas delegate, @NotNull ColorSpace cs) {
this.delegate = delegate;
this.image = new BufferedImage(delegate.getWidth(), delegate.getHeight(), BufferedImage.TYPE_INT_RGB);
this.cs = cs;
}
@Override
@ -29,15 +34,16 @@ public final class LiveCanvas implements Canvas {
}
@Override
public void set(int x, int y, @NotNull Color color) {
delegate.set(x, y, color);
public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) {
delegate.add(x, y, n, spectrum, lambda);
var color = ColorRGB.gamma(delegate.getRGB(x, y, cs));
var rgb = color.red() << 16 | color.green() << 8 | color.blue();
image.setRGB(x, y, rgb);
}
@Override
public @NotNull Color get(int x, int y) {
return delegate.get(x, y);
public @NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs) {
return delegate.getRGB(x, y, cs);
}
public @NotNull Thread preview() {

@ -0,0 +1,76 @@
package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import org.jetbrains.annotations.NotNull;
import java.awt.image.BufferedImage;
import java.util.Objects;
public final class RGBCanvas implements Canvas {
private final int width;
private final int height;
private final @NotNull ColorSpace cs;
private final @NotNull ColorRGB[][] data;
public RGBCanvas(int width, int height, @NotNull ColorSpace cs) {
this.width = width;
this.height = height;
this.cs = Objects.requireNonNull(cs);
if (width <= 0) throw new IllegalArgumentException("width must be positive");
if (height <= 0) throw new IllegalArgumentException("height must be positive");
this.data = new ColorRGB[height][width];
}
public RGBCanvas(@NotNull BufferedImage image, @NotNull ColorSpace cs) {
this(image.getWidth(), image.getHeight(), cs);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
this.data[y][x] = new ColorRGB(image.getRGB(x, y));
}
}
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
@Override
public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) {
assert x < width;
assert y < height;
var rgb = spectrum.toRGB(lambda, cs);
data[y][x] = ColorRGB.average(data[y][x], rgb, n);
}
@Override
public @NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs) {
if (cs == this.cs) return get(x, y);
return cs.toRGB(this.cs.toXYZ(get(x, y)));
}
public @NotNull ColorRGB get(int x, int y) {
assert x < width;
assert y < height;
return Objects.requireNonNullElse(data[y][x], ColorRGB.BLACK);
}
public void set(int x, int y, @NotNull ColorRGB color) {
assert x < width;
assert y < height;
data[y][x] = Objects.requireNonNull(color);
}
}

@ -0,0 +1,74 @@
package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ;
import org.jetbrains.annotations.NotNull;
import java.awt.image.BufferedImage;
import java.util.Objects;
public final class XYZCanvas implements Canvas {
private final int width;
private final int height;
private final @NotNull ColorXYZ[][] data;
public XYZCanvas(int width, int height) {
this.width = width;
this.height = height;
if (width <= 0) throw new IllegalArgumentException("width must be positive");
if (height <= 0) throw new IllegalArgumentException("height must be positive");
this.data = new ColorXYZ[height][width];
}
public XYZCanvas(@NotNull BufferedImage image, @NotNull ColorSpace cs) {
this(image.getWidth(), image.getHeight());
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
data[y][x] = cs.toXYZ(new ColorRGB(image.getRGB(x, y)));
}
}
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
@Override
public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) {
assert x < width;
assert y < height;
var xyz = spectrum.toXYZ(lambda);
data[y][x] = ColorXYZ.average(get(x, y), xyz, n);
}
public @NotNull ColorXYZ get(int x, int y) {
assert x < width;
assert y < height;
return Objects.requireNonNullElse(data[y][x], ColorXYZ.BLACK);
}
@Override
public @NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs) {
return cs.toRGB(get(x, y));
}
public void set(int x, int y, @NotNull ColorXYZ color) {
assert x < width;
assert y < height;
data[y][x] = Objects.requireNonNull(color);
}
}

@ -2,7 +2,10 @@ 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.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
@ -13,13 +16,17 @@ import java.util.random.RandomGenerator;
public record DielectricMaterial(double refractionIndex, @NotNull Texture texture) implements Material {
public DielectricMaterial(double refractionIndex) {
this(refractionIndex, Color.WHITE);
this(refractionIndex, Spectra.WHITE);
}
public DielectricMaterial {
Objects.requireNonNull(texture, "texture");
}
public DielectricMaterial(double refractionIndex, @NotNull ColorRGB color, @NotNull ColorSpace cs) {
this(refractionIndex, new RGBAlbedoSpectrum(cs, color));
}
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var ri = hit.isFrontFace() ? (1 / refractionIndex) : refractionIndex;
@ -32,7 +39,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 SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection)));
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection, ray.lambda())));
}
private double reflectance(double cos) {

@ -1,22 +1,34 @@
package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBIlluminantSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
import java.util.random.RandomGenerator;
public record DiffuseLight(@NotNull Texture texture) implements Material {
public DiffuseLight {
Objects.requireNonNull(texture, "texture");
}
public DiffuseLight(@NotNull ColorRGB color, @NotNull ColorSpace cs) {
this(new RGBIlluminantSpectrum(cs, color));
}
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
return Optional.empty();
}
@Override
public @NotNull Color emitted(@NotNull HitResult hit) {
public @NotNull Spectrum emitted(@NotNull HitResult hit) {
return texture.get(hit);
}
}

@ -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.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
@ -40,11 +41,11 @@ 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 SpecularScatterResult(Color.WHITE, new Ray(ray.at(hit.t()), ray.direction())));
return Optional.of(new SpecularScatterResult(Spectra.WHITE, new Ray(ray.at(hit.t()), ray.direction(), ray.lambda())));
}
@Override
public @NotNull Color emitted(@NotNull HitResult hit) {
public @NotNull Spectrum emitted(@NotNull HitResult hit) {
if (hit.isFrontFace()) {
if (front != null) return front.emitted(hit);
} else {
@ -56,7 +57,7 @@ public final class DirectionalMaterial implements Material {
private record DirectionalTexture(@Nullable Texture front, @Nullable Texture back) implements Texture {
@Override
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
public @NotNull Spectrum get(double u, double v, @NotNull Vec3 p) {
throw new UnsupportedOperationException();
}

@ -2,15 +2,27 @@ package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.renderer.pdf.SphereProbabilityDensityFunction;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
import java.util.random.RandomGenerator;
public record IsotropicMaterial(@NotNull Color albedo) implements Material {
public record IsotropicMaterial(@NotNull Spectrum albedo) implements Material {
public IsotropicMaterial {
Objects.requireNonNull(albedo, "albedo");
}
public IsotropicMaterial(@NotNull ColorRGB color, @NotNull ColorSpace cs) {
this(new RGBAlbedoSpectrum(cs, color));
}
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
return Optional.of(new PdfScatterResult(albedo(), new SphereProbabilityDensityFunction()));

@ -2,6 +2,9 @@ package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.renderer.pdf.CosineProbabilityDensityFunction;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum;
import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
@ -15,6 +18,10 @@ public record LambertianMaterial(@NotNull Texture texture) implements Material {
Objects.requireNonNull(texture, "texture");
}
public LambertianMaterial(@NotNull ColorRGB color, @NotNull ColorSpace cs) {
this(new RGBAlbedoSpectrum(cs, color));
}
@Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var attenuation = texture.get(hit);

@ -2,7 +2,9 @@ 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.spectral.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
@ -30,10 +32,10 @@ public interface Material {
/**
* {@return the color emitted for a given hit}
* @implSpec the default implementation returns {@linkplain Color#BLACK black}, i.e. no emission
* @implSpec the default implementation returns {@linkplain ColorRGB#BLACK black}, i.e. no emission
*/
default @NotNull Color emitted(@NotNull HitResult hit) {
return Color.BLACK;
default @NotNull Spectrum emitted(@NotNull HitResult hit) {
return Spectra.BLACK;
}
/**
@ -48,7 +50,7 @@ public interface Material {
* @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 {
record SpecularScatterResult(@NotNull Spectrum attenuation, @NotNull Ray ray) implements ScatterResult {
public SpecularScatterResult {
Objects.requireNonNull(attenuation, "attenuation");
Objects.requireNonNull(ray, "ray");
@ -62,7 +64,7 @@ public interface Material {
* @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 {
record PdfScatterResult(@NotNull Spectrum attenuation, @NotNull ProbabilityDensityFunction pdf) implements ScatterResult {
public PdfScatterResult {
Objects.requireNonNull(attenuation, "attenuation");
Objects.requireNonNull(pdf, "pdf");

@ -2,6 +2,9 @@ package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum;
import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
@ -16,6 +19,14 @@ public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements
this(texture, 0);
}
public MetallicMaterial(@NotNull ColorRGB color, @NotNull ColorSpace cs) {
this(color, cs, 0);
}
public MetallicMaterial(@NotNull ColorRGB color, @NotNull ColorSpace cs, double fuzz) {
this(new RGBAlbedoSpectrum(cs, color), fuzz);
}
public MetallicMaterial {
Objects.requireNonNull(texture, "texture");
if (fuzz < 0 || !Double.isFinite(fuzz)) throw new IllegalArgumentException("fuzz must be non-negative");
@ -28,6 +39,6 @@ public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements
newDirection = Vec3.fma(fuzz, Vec3.random(random), newDirection.unit());
}
var attenuation = texture.get(hit);
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection)));
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection, ray.lambda())));
}
}

@ -2,13 +2,13 @@ package eu.jonahbauer.raytracing.render.renderer;
import eu.jonahbauer.raytracing.render.camera.Camera;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.canvas.Image;
import eu.jonahbauer.raytracing.render.canvas.XYZCanvas;
import eu.jonahbauer.raytracing.scene.Scene;
import org.jetbrains.annotations.NotNull;
public interface Renderer {
default @NotNull Image render(@NotNull Camera camera, @NotNull Scene scene) {
var image = new Image(camera.getWidth(), camera.getHeight());
default @NotNull Canvas render(@NotNull Camera camera, @NotNull Scene scene) {
var image = new XYZCanvas(camera.getWidth(), camera.getHeight());
render(camera, scene, image);
return image;
}

@ -4,7 +4,7 @@ import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.material.Material;
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.spectral.SampledSpectrum;
import eu.jonahbauer.raytracing.render.camera.Camera;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.scene.Scene;
@ -76,18 +76,11 @@ public final class SimpleRenderer implements Renderer {
for (int x = 0; x < camera.getWidth(); x++) {
var ray = camera.cast(x, y, sif, sjf, sqrtSamplesPerPixel, random);
var c = getColor(scene, ray, random);
canvas.set(x, y, Color.average(canvas.get(x, y), c, sample));
canvas.add(x, y, sample, c, ray.lambda());
}
});
}
}
// apply gamma correction
getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
for (int x = 0; x < camera.getWidth(); x++) {
canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma));
}
});
}
/**
@ -100,7 +93,6 @@ public final class SimpleRenderer implements Renderer {
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++) {
@ -109,10 +101,9 @@ public final class SimpleRenderer implements Renderer {
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.add(x, y, ++i, c, ray.lambda());
}
}
canvas.set(x, y, Color.gamma(color, gamma));
}
});
}
@ -120,19 +111,19 @@ public final class SimpleRenderer implements Renderer {
/**
* {@return the color of the given ray in the given scene}
*/
private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray, @NotNull RandomGenerator random) {
private @NotNull SampledSpectrum getColor(@NotNull Scene scene, @NotNull Ray ray, @NotNull RandomGenerator random) {
return getColor0(scene, ray, maxDepth, random);
}
private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) {
var color = Color.BLACK;
var attenuation = Color.WHITE;
private @NotNull SampledSpectrum getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) {
var color = SampledSpectrum.BLACK;
var attenuation = SampledSpectrum.WHITE;
while (depth-- > 0) {
var optional = scene.hit(ray);
if (optional.isEmpty()) {
var background = scene.getBackgroundColor(ray);
color = Color.fma(attenuation, background, color);
color = SampledSpectrum.fma(attenuation, background, color);
if (DEBUG) {
System.out.println(" Hit background: " + background);
}
@ -144,13 +135,13 @@ public final class SimpleRenderer implements Renderer {
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)) {
var emitted = material.emitted(hit).sample(ray.lambda());
if (DEBUG && !SampledSpectrum.BLACK.equals(emitted)) {
System.out.println(" Emitted: " + emitted);
}
var result = material.scatter(ray, hit, random);
color = Color.fma(attenuation, emitted, color);
color = SampledSpectrum.fma(attenuation, emitted, color);
if (result.isEmpty()) {
if (DEBUG) {
@ -161,7 +152,7 @@ public final class SimpleRenderer implements Renderer {
switch (result.get()) {
case Material.SpecularScatterResult(var a, var scattered) -> {
attenuation = attenuation.times(a);
attenuation = attenuation.times(a.sample(ray.lambda()));
ray = scattered;
if (DEBUG) {
@ -170,8 +161,8 @@ public final class SimpleRenderer implements Renderer {
}
case Material.PdfScatterResult(var a, var pdf) -> {
if (scene.getTargets() == null) {
attenuation = attenuation.times(a);
ray = new Ray(hit.position(), pdf.generate(random));
attenuation = attenuation.times(a.sample(ray.lambda()));
ray = new Ray(hit.position(), pdf.generate(random), ray.lambda());
if (DEBUG) {
System.out.println(" Pdf scattering with albedo " + a);
@ -186,8 +177,8 @@ public final class SimpleRenderer implements Renderer {
var factor = idealPdf / actualPdf;
attenuation = attenuation.times(a.times(factor));
ray = new Ray(hit.position(), direction);
attenuation = attenuation.times(a.sample(ray.lambda()).times(factor));
ray = new Ray(hit.position(), direction, ray.lambda());
if (DEBUG) {
System.out.println(" Pdf scattering with albedo " + a + " and factor " + factor);
@ -215,7 +206,7 @@ public final class SimpleRenderer implements Renderer {
* are encoded in the longs lower and upper 32 bits respectively.
*/
private static @NotNull IntStream getScanlineStream(int height, boolean parallel) {
var stream = IntStream.range(0, height);
var stream = IntStream.range(0, height).map(i -> height - i - 1);
return parallel ? stream.parallel() : stream;
}

@ -4,11 +4,12 @@ import eu.jonahbauer.raytracing.math.IVec;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
// TODO use Vector API to parallelize operations
public final class SampledSpectrum implements IVec<SampledSpectrum> {
public static final SampledSpectrum BLACK;
public static final SampledSpectrum WHITE;
@ -137,7 +138,7 @@ public final class SampledSpectrum implements IVec<SampledSpectrum> {
return lambdas.toXYZ(this);
}
public @NotNull Color toRGB(@NotNull SampledWavelengths lambdas, @NotNull ColorSpace cs) {
public @NotNull ColorRGB toRGB(@NotNull SampledWavelengths lambdas, @NotNull ColorSpace cs) {
return cs.toRGB(toXYZ(lambdas));
}
}

@ -0,0 +1,153 @@
package eu.jonahbauer.raytracing.render.spectral.colors;
import eu.jonahbauer.raytracing.math.IVec3;
import eu.jonahbauer.raytracing.math.Vec3;
import org.jetbrains.annotations.NotNull;
import java.util.Random;
import static eu.jonahbauer.raytracing.Main.DEBUG;
public record ColorRGB(double r, double g, double b) implements IVec3<ColorRGB> {
public static final @NotNull ColorRGB BLACK = new ColorRGB(0.0, 0.0, 0.0);
public static final @NotNull ColorRGB WHITE = new ColorRGB(1.0, 1.0, 1.0);
public static @NotNull ColorRGB random(@NotNull Random random) {
return new ColorRGB(random.nextDouble(), random.nextDouble(), random.nextDouble());
}
public static @NotNull ColorRGB random(@NotNull Random random, double min, double max) {
var span = max - min;
return new ColorRGB(
Math.fma(random.nextDouble(), span, min),
Math.fma(random.nextDouble(), span, min),
Math.fma(random.nextDouble(), span, min)
);
}
public ColorRGB(int rgb) {
this((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF);
}
public ColorRGB(int red, int green, int blue) {
this(red / 255f, green / 255f, blue / 255f);
}
public ColorRGB {
if (DEBUG && (!Double.isFinite(r) || !Double.isFinite(g) || !Double.isFinite(b))) {
throw new IllegalArgumentException("r, g and b must be finite");
}
}
/*
* Math
*/
public static @NotNull ColorRGB average(@NotNull ColorRGB current, @NotNull ColorRGB next, int index) {
return lerp(current, next, 1d / index);
}
public static @NotNull ColorRGB lerp(@NotNull ColorRGB a, @NotNull ColorRGB b, double t) {
if (t < 0) return a;
if (t > 1) return b;
return new ColorRGB(
Math.fma(t, b.r - a.r, a.r),
Math.fma(t, b.g - a.g, a.g),
Math.fma(t, b.b - a.b, a.b)
);
}
public static @NotNull ColorRGB fma(@NotNull ColorRGB a, @NotNull ColorRGB b, @NotNull ColorRGB c) {
return new ColorRGB(
Math.fma(a.r, b.r, c.r),
Math.fma(a.g, b.g, c.g),
Math.fma(a.b, b.b, c.b)
);
}
public static @NotNull ColorRGB gamma(@NotNull ColorRGB color) {
return new ColorRGB(gamma(color.r), gamma(color.g), gamma(color.b));
}
public static @NotNull ColorRGB inverseGamma(@NotNull ColorRGB color) {
return new ColorRGB(inverseGamma(color.r), inverseGamma(color.g), inverseGamma(color.b));
}
private static double gamma(double value) {
if (value <= 0.0031308) return 12.92 * value;
return 1.055 * Math.pow(value, 1. / 2.4) - 0.055;
}
private static double inverseGamma(double value) {
if (value <= 0.04045) return value / 12.92;
return Math.pow((value + 0.055) / 1.055, 2.4d);
}
@Override
public @NotNull ColorRGB plus(@NotNull ColorRGB other) {
return new ColorRGB(r + other.r, g + other.g, b + other.b);
}
@Override
public @NotNull ColorRGB minus(@NotNull ColorRGB other) {
return new ColorRGB(r - other.r, g - other.g, b - other.b);
}
@Override
public @NotNull ColorRGB times(double d) {
return new ColorRGB(r * d, g * d, b * d);
}
@Override
public @NotNull ColorRGB times(@NotNull ColorRGB other) {
return new ColorRGB(r * other.r, g * other.g, b * other.b);
}
/*
* Vec3
*/
@Override
public @NotNull Vec3 toVec3() {
return new Vec3(r, g, b);
}
public static @NotNull ColorRGB fromVec3(@NotNull Vec3 vec) {
return new ColorRGB(vec.x(), vec.y(), vec.z());
}
/*
* Accessors
*/
public int red() {
return toInt(r);
}
public int green() {
return toInt(g);
}
public int blue() {
return toInt(b);
}
private static int toInt(double value) {
return Math.clamp((int) (255.99 * value), 0, 255);
}
@Override
public double component1() {
return r;
}
@Override
public double component2() {
return g;
}
@Override
public double component3() {
return b;
}
}

@ -4,7 +4,6 @@ import eu.jonahbauer.raytracing.math.Matrix3;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.spectrum.DenselySampledSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Color;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
@ -55,17 +54,17 @@ public final class ColorSpace {
this.RGBfromXYZ = XYZfromRGB.invert();
}
public @NotNull Color toRGB(@NotNull ColorXYZ xyz) {
public @NotNull ColorRGB toRGB(@NotNull ColorXYZ xyz) {
var out = RGBfromXYZ.times(xyz.toVec3());
return new Color(out.x(), out.y(), out.z());
return new ColorRGB(out.x(), out.y(), out.z());
}
public @NotNull ColorXYZ toXYZ(@NotNull Color rgb) {
public @NotNull ColorXYZ toXYZ(@NotNull ColorRGB rgb) {
var out = XYZfromRGB.times(rgb.toVec3());
return ColorXYZ.fromVec3(out);
}
public @NotNull Vec3 toCIELab(@NotNull Color rgb) {
public @NotNull Vec3 toCIELab(@NotNull ColorRGB rgb) {
return toCIELab(toXYZ(rgb));
}
@ -86,8 +85,8 @@ public final class ColorSpace {
}
}
public @NotNull SigmoidPolynomial toSpectrum(@NotNull Color rgb) {
return RGBtoSpectrumTable.get(new Color(
public @NotNull SigmoidPolynomial toSpectrum(@NotNull ColorRGB rgb) {
return RGBtoSpectrumTable.get(new ColorRGB(
Math.max(0, rgb.r()),
Math.max(0, rgb.g()),
Math.max(0, rgb.b())

@ -1,6 +1,5 @@
package eu.jonahbauer.raytracing.render.spectral.colors;
import eu.jonahbauer.raytracing.render.texture.Color;
import org.jetbrains.annotations.NotNull;
import java.io.*;
@ -86,7 +85,7 @@ public final class SpectrumTable {
}
}
public @NotNull SigmoidPolynomial get(@NotNull Color color) {
public @NotNull SigmoidPolynomial get(@NotNull ColorRGB color) {
// handle uniform rgb values
if (color.r() == color.g() && color.g() == color.b()) {
return new SigmoidPolynomial(0, 0, (color.r() - .5) / Math.sqrt(color.r() * (1 - color.r())));

@ -3,7 +3,6 @@ package eu.jonahbauer.raytracing.render.spectral.colors;
import eu.jonahbauer.raytracing.math.Matrix3;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Color;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
@ -81,7 +80,7 @@ public final class SpectrumTableGenerator {
return new SpectrumTable(resolution, scale, table);
}
private void generate(@NotNull Color rgb, double @NotNull[] c, double @NotNull[] out, int offset) {
private void generate(@NotNull ColorRGB rgb, double @NotNull[] c, double @NotNull[] out, int offset) {
gaussNewton(rgb, c, ITERATIONS);
double c0 = 360.0, c1 = 1.0 / (830.0 - 360.0);
double A = c[0], B = c[1], C = c[2];
@ -97,7 +96,7 @@ public final class SpectrumTableGenerator {
* @param c the coefficients, used as initial values and output
* @param it the number of iterations
*/
private void gaussNewton(@NotNull Color rgb, double @NotNull[] c, int it) {
private void gaussNewton(@NotNull ColorRGB rgb, double @NotNull[] c, int it) {
var bestQuality = Double.POSITIVE_INFINITY;
var bestCoefficients = new double[3];
@ -134,7 +133,7 @@ public final class SpectrumTableGenerator {
/**
* Calculates the Jacobian matrix of the {@code polynomial}.
*/
private @NotNull Matrix3 getJacobian(@NotNull Color rgb, @NotNull SigmoidPolynomial polynomial) {
private @NotNull Matrix3 getJacobian(@NotNull ColorRGB rgb, @NotNull SigmoidPolynomial polynomial) {
var jac = new double[3][3];
// central finite difference coefficients for first derivative with sixth-order accuracy
@ -171,7 +170,7 @@ public final class SpectrumTableGenerator {
* the given coefficients, illuminating it with the color space's standard illuminant, and converting it back to an
* RBG color. The output is a vector in CIE Lab color space.
*/
private @NotNull Vec3 getResidual(@NotNull Color rgb, @NotNull SigmoidPolynomial polynomial) {
private @NotNull Vec3 getResidual(@NotNull ColorRGB rgb, @NotNull SigmoidPolynomial polynomial) {
var out = new SigmoidPolynomialSpectrum(polynomial, cs).toXYZ();
return cs.toCIELab(rgb).minus(cs.toCIELab(out));
}
@ -181,12 +180,12 @@ public final class SpectrumTableGenerator {
return x * x * (3.0 - 2.0 * x);
}
private static @NotNull Color getColor(int l, double x, double y, double z) {
private static @NotNull ColorRGB getColor(int l, double x, double y, double z) {
var rgb = new double[3];
rgb[l] = z;
rgb[(l + 1) % 3] = x * z;
rgb[(l + 2) % 3] = y * z;
return new Color(rgb[0], rgb[1], rgb[2]);
return new ColorRGB(rgb[0], rgb[1], rgb[2]);
}
private record SigmoidPolynomialSpectrum(@NotNull SigmoidPolynomial polynomial, @NotNull ColorSpace cs) implements Spectrum {

@ -37,14 +37,6 @@ public final class DenselySampledSpectrum implements Spectrum {
this.max = Arrays.stream(this.samples).max().orElseThrow();
}
public @NotNull DenselySampledSpectrum scale(double scale) {
var s = Arrays.copyOf(samples, samples.length);
for (int i = 0; i < s.length; i++) {
s[i] *= scale;
}
return new DenselySampledSpectrum(s, min);
}
@Override
public double max() {
return max;

@ -1,7 +1,5 @@
package eu.jonahbauer.raytracing.render.spectral.spectrum;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public final class PiecewiseLinearSpectrum implements Spectrum {
@ -31,14 +29,6 @@ public final class PiecewiseLinearSpectrum implements Spectrum {
this.max = max;
}
public @NotNull PiecewiseLinearSpectrum scale(double scale) {
var v = Arrays.copyOf(values, values.length);
for (int i = 0; i < v.length; i++) {
v[i] *= scale;
}
return new PiecewiseLinearSpectrum(lambdas, v);
}
@Override
public double max() {
return max;

@ -2,13 +2,13 @@ package eu.jonahbauer.raytracing.render.spectral.spectrum;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import org.jetbrains.annotations.NotNull;
public final class RGBAlbedoSpectrum implements Spectrum {
private final @NotNull SigmoidPolynomial polynomial;
public RGBAlbedoSpectrum(@NotNull ColorSpace cs, @NotNull Color rgb) {
public RGBAlbedoSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) {
if (rgb.r() < 0 || rgb.r() > 1 || rgb.g() < 0 || rgb.g() > 1 || rgb.b() < 0 || rgb.b() > 1) {
throw new IllegalArgumentException();
}

@ -2,7 +2,7 @@ package eu.jonahbauer.raytracing.render.spectral.spectrum;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import org.jetbrains.annotations.NotNull;
/**
@ -14,13 +14,13 @@ public final class RGBIlluminantSpectrum implements Spectrum {
private final @NotNull SigmoidPolynomial polynomial;
private final @NotNull Spectrum illuminant;
public RGBIlluminantSpectrum(@NotNull ColorSpace cs, @NotNull Color rgb) {
public RGBIlluminantSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) {
if (rgb.r() < 0 || rgb.g() < 0 || rgb.b() < 0) {
throw new IllegalArgumentException();
}
var max = Math.max(rgb.r(), Math.max(rgb.g(), rgb.b()));
this.scale = 2 * max;
this.polynomial = cs.toSpectrum(scale == 0 ? rgb.div(scale) : Color.BLACK);
this.polynomial = cs.toSpectrum(scale != 0 ? rgb.div(scale) : ColorRGB.BLACK);
this.illuminant = cs.illuminant();
}

@ -2,20 +2,20 @@ package eu.jonahbauer.raytracing.render.spectral.spectrum;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import org.jetbrains.annotations.NotNull;
public final class RGBUnboundedSpectrum implements Spectrum {
private final double scale;
private final @NotNull SigmoidPolynomial polynomial;
public RGBUnboundedSpectrum(@NotNull ColorSpace cs, @NotNull Color rgb) {
public RGBUnboundedSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) {
if (rgb.r() < 0 || rgb.g() < 0 || rgb.b() < 0) {
throw new IllegalArgumentException();
}
var max = Math.max(rgb.r(), Math.max(rgb.g(), rgb.b()));
this.scale = 2 * max;
this.polynomial = cs.toSpectrum(scale == 0 ? rgb.div(scale) : Color.BLACK);
this.polynomial = cs.toSpectrum(scale == 0 ? rgb.div(scale) : ColorRGB.BLACK);
}
@Override

@ -0,0 +1,15 @@
package eu.jonahbauer.raytracing.render.spectral.spectrum;
import org.jetbrains.annotations.NotNull;
public record ScaledSpectrum(@NotNull Spectrum spectrum, double scale) implements Spectrum {
@Override
public double max() {
return spectrum.max() * scale;
}
@Override
public double get(double lambda) {
return spectrum.get(lambda) * scale;
}
}

@ -36,6 +36,9 @@ public final class Spectra {
*/
public static final Spectrum D65 = read("CIE_std_illum_D65.csv", true);
public static final Spectrum BLACK = new ConstantSpectrum(0);
public static final Spectrum WHITE = new ConstantSpectrum(1);
private static @NotNull Spectrum read(@NotNull String path, boolean normalize) {
var lambda = new ArrayList<Double>();
var values = new ArrayList<Double>();

@ -1,13 +1,18 @@
package eu.jonahbauer.raytracing.render.spectral.spectrum;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ;
import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.SkyBox;
import org.jetbrains.annotations.NotNull;
public interface Spectrum {
public interface Spectrum extends Texture, SkyBox {
int LAMBDA_MIN = 360;
int LAMBDA_MAX = 830;
@ -22,6 +27,10 @@ public interface Spectrum {
*/
double get(double lambda);
default @NotNull Spectrum scale(double scale) {
return new ScaledSpectrum(this, scale);
}
default @NotNull SampledSpectrum sample(@NotNull SampledWavelengths lambdas) {
return new SampledSpectrum(lambdas, this);
}
@ -34,8 +43,35 @@ public interface Spectrum {
);
}
default @NotNull Color toRGB(@NotNull ColorSpace cs) {
default @NotNull ColorRGB toRGB(@NotNull ColorSpace cs) {
return cs.toRGB(toXYZ());
}
/*
* Texture
*/
@Override
default @NotNull Spectrum get(@NotNull HitResult hit) {
return this;
}
@Override
default @NotNull Spectrum get(double u, double v, @NotNull Vec3 p) {
return this;
}
@Override
default boolean isUVRequired() {
return false;
}
/*
* SkyBox
*/
@Override
default @NotNull SampledSpectrum getColor(@NotNull Ray ray) {
return this.sample(ray.lambda());
}
}

@ -1,12 +1,13 @@
package eu.jonahbauer.raytracing.render.texture;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
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) {
public @NotNull Spectrum 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);

@ -1,178 +0,0 @@
package eu.jonahbauer.raytracing.render.texture;
import eu.jonahbauer.raytracing.math.IVec3;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.SkyBox;
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, SkyBox, IVec3<Color> {
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 @NotNull Color random(@NotNull Random random) {
return new Color(random.nextDouble(), random.nextDouble(), random.nextDouble());
}
public static @NotNull Color random(@NotNull Random random, double min, double max) {
var span = max - min;
return new Color(
Math.fma(random.nextDouble(), span, min),
Math.fma(random.nextDouble(), span, min),
Math.fma(random.nextDouble(), span, min)
);
}
public static @NotNull Color gamma(@NotNull Color color, double gamma) {
if (gamma == 1.0) {
return color;
} else if (gamma == 2.0) {
return new Color(
Math.sqrt(color.r()),
Math.sqrt(color.g()),
Math.sqrt(color.b())
);
} else {
return new Color(
Math.pow(color.r(), 1 / gamma),
Math.pow(color.g(), 1 / gamma),
Math.pow(color.b(), 1 / gamma)
);
}
}
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);
}
public Color {
if (DEBUG && (!Double.isFinite(r) || !Double.isFinite(g) || !Double.isFinite(b))) {
throw new IllegalArgumentException("r, g and b must be finite");
}
}
/*
* Math
*/
public static @NotNull Color average(@NotNull Color current, @NotNull Color next, int index) {
return lerp(current, next, 1d / index);
}
public static @NotNull Color lerp(@NotNull Color a, @NotNull Color b, double t) {
if (t < 0) return a;
if (t > 1) return b;
return new Color(
Math.fma(t, b.r - a.r, a.r),
Math.fma(t, b.g - a.g, a.g),
Math.fma(t, b.b - a.b, a.b)
);
}
public static @NotNull Color fma(@NotNull Color a, @NotNull Color b, @NotNull Color c) {
return new Color(
Math.fma(a.r, b.r, c.r),
Math.fma(a.g, b.g, c.g),
Math.fma(a.b, b.b, c.b)
);
}
@Override
public @NotNull Color plus(@NotNull Color other) {
return new Color(r + other.r, g + other.g, b + other.b);
}
@Override
public @NotNull Color minus(@NotNull Color other) {
return new Color(r - other.r, g - other.g, b - other.b);
}
@Override
public @NotNull Color times(double d) {
return new Color(r * d, g * d, b * d);
}
@Override
public @NotNull Color times(@NotNull Color other) {
return new Color(r * other.r, g * other.g, b * other.b);
}
/*
* Vec3
*/
@Override
public @NotNull Vec3 toVec3() {
return new Vec3(r, g, b);
}
public static @NotNull Color fromVec3(@NotNull Vec3 vec) {
return new Color(vec.x(), vec.y(), vec.z());
}
/*
* Accessors
*/
public int red() {
return toInt(r);
}
public int green() {
return toInt(g);
}
public int blue() {
return toInt(b);
}
private static int toInt(double value) {
return Math.clamp((int) (255.99 * value), 0, 255);
}
@Override
public double component1() {
return r;
}
@Override
public double component2() {
return g;
}
@Override
public double component3() {
return b;
}
/*
* Texture
*/
@Override
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
return this;
}
@Override
public boolean isUVRequired() {
return false;
}
/*
* SkyBox
*/
@Override
public @NotNull Color getColor(@NotNull Ray ray) {
return this;
}
}

@ -1,7 +1,11 @@
package eu.jonahbauer.raytracing.render.texture;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.canvas.Image;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBIlluminantSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import org.jetbrains.annotations.NotNull;
import javax.imageio.ImageIO;
@ -10,22 +14,33 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
public record ImageTexture(@NotNull Image image) implements Texture {
public final class ImageTexture implements Texture {
private static final String PATH_PREFIX = "/eu/jonahbauer/raytracing/textures/";
public ImageTexture {
Objects.requireNonNull(image, "image");
}
private final int width;
private final int height;
private final @NotNull Spectrum[][] spectra;
public ImageTexture(@NotNull BufferedImage image, @NotNull ColorSpace cs, @NotNull Type type, boolean gamma) {
this.width = image.getWidth();
this.height = image.getHeight();
this.spectra = new Spectrum[height][width];
public ImageTexture(@NotNull BufferedImage image) {
this(new Image(image));
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
var rgb = new ColorRGB(image.getRGB(x, y));
if (gamma) rgb = ColorRGB.inverseGamma(rgb);
spectra[y][x] = type.newSpectrum(cs, rgb);
}
}
}
public ImageTexture(@NotNull String path) {
this(read(path));
public ImageTexture(@NotNull String path, @NotNull ColorSpace cs) {
this(read(path), cs, Type.ALBEDO, true);
}
private static @NotNull BufferedImage read(@NotNull String path) {
try (var in = Objects.requireNonNull(ImageTexture.class.getResourceAsStream(path))) {
try (var in = Objects.requireNonNull(ImageTexture.class.getResourceAsStream(PATH_PREFIX + path))) {
return ImageIO.read(in);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
@ -33,11 +48,29 @@ public record ImageTexture(@NotNull Image image) implements Texture {
}
@Override
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
public @NotNull Spectrum 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);
int x = (int) (u * (width - 1));
int y = (int) (v * (height - 1));
return spectra[y][x];
}
public enum Type {
ALBEDO {
@Override
protected @NotNull Spectrum newSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) {
return new RGBAlbedoSpectrum(cs, rgb);
}
},
ILLUMINANT {
@Override
protected @NotNull Spectrum newSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) {
return new RGBIlluminantSpectrum(cs, rgb);
}
},
;
protected abstract @NotNull Spectrum newSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb);
}
}

@ -1,6 +1,8 @@
package eu.jonahbauer.raytracing.render.texture;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
@ -11,11 +13,11 @@ 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 static final @NotNull DoubleFunction<Spectrum> GREYSCALE = Spectra.WHITE::scale;
private final double scale;
private final int turbulence;
private final @NotNull DoubleFunction<Color> color;
private final @NotNull DoubleFunction<Spectrum> color;
private final int mask;
private final Vec3[] randvec;
@ -35,12 +37,12 @@ public final class PerlinTexture implements Texture {
this(scale, turbulence, GREYSCALE);
}
public PerlinTexture(double scale, int turbulence, @NotNull DoubleFunction<Color> color) {
public PerlinTexture(double scale, int turbulence, @NotNull DoubleFunction<Spectrum> color) {
this(scale, turbulence, color, POINT_COUNT, RANDOM);
}
public PerlinTexture(
double scale, int turbulence, @NotNull DoubleFunction<Color> color,
double scale, int turbulence, @NotNull DoubleFunction<Spectrum> color,
int count, @NotNull RandomGenerator random
) {
if ((count & (count - 1)) != 0) throw new IllegalArgumentException("count must be a power of two");
@ -118,7 +120,7 @@ public final class PerlinTexture implements Texture {
}
@Override
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
public @NotNull Spectrum 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);

@ -1,6 +1,7 @@
package eu.jonahbauer.raytracing.render.texture;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull;
@ -8,7 +9,7 @@ public interface Texture {
/**
* {@return the color of <code>this</code> texture for a hit}
*/
default @NotNull Color get(@NotNull HitResult hit) {
default @NotNull Spectrum get(@NotNull HitResult hit) {
return get(hit.u(), hit.v(), hit.position());
}
@ -18,7 +19,7 @@ public interface Texture {
* @param v the texture v coordinate
* @param p the position
*/
@NotNull Color get(double u, double v, @NotNull Vec3 p);
@NotNull Spectrum get(double u, double v, @NotNull Vec3 p);
/**
* Returns whether {@link #get(double, double, Vec3)} uses the {@code u} and/or {@code v} parameters.

@ -2,7 +2,8 @@ package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra;
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree;
import eu.jonahbauer.raytracing.scene.util.HittableCollection;
import org.jetbrains.annotations.NotNull;
@ -23,7 +24,7 @@ public final class Scene extends HittableCollection {
}
public Scene(@NotNull List<? extends @NotNull Hittable> objects, @Nullable List<? extends @NotNull Target> targets) {
this(Color.BLACK, objects, targets);
this(Spectra.BLACK, objects, targets);
}
public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects) {
@ -54,7 +55,7 @@ public final class Scene extends HittableCollection {
return targets;
}
public @NotNull Color getBackgroundColor(@NotNull Ray ray) {
public @NotNull SampledSpectrum getBackgroundColor(@NotNull Ray ray) {
return background.getColor(ray);
}
}

@ -1,21 +1,27 @@
package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
import org.jetbrains.annotations.NotNull;
@FunctionalInterface
public interface SkyBox {
@NotNull Color getColor(@NotNull Ray ray);
@NotNull SampledSpectrum getColor(@NotNull Ray ray);
static @NotNull SkyBox gradient(@NotNull Color top, @NotNull Color bottom) {
static @NotNull SkyBox gradient(@NotNull Spectrum top, @NotNull Spectrum bottom) {
return ray -> {
// altitude from -pi/2 to pi/2
var alt = Math.copySign(
Math.acos(ray.direction().withY(0).unit().dot(ray.direction().unit())),
ray.direction().y()
);
return Color.lerp(bottom, top, alt / Math.PI + 0.5);
return SampledSpectrum.lerp(
top.sample(ray.lambda()),
bottom.sample(ray.lambda()),
alt / Math.PI + 0.5
);
};
}
}

@ -62,7 +62,7 @@ public sealed class RotateY extends Transform {
var newOrigin = transform(origin);
var newDirection = transform(direction);
return new Ray(newOrigin, newDirection);
return new Ray(newOrigin, newDirection, ray.lambda());
}
@Override

@ -35,7 +35,7 @@ public sealed class Translate extends Transform {
@Override
protected final @NotNull Ray transform(@NotNull Ray ray) {
return new Ray(ray.origin().minus(offset), ray.direction());
return new Ray(ray.origin().minus(offset), ray.direction(), ray.lambda());
}
@Override

@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.ImageFormat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -13,11 +13,11 @@ import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ImageTest {
class RGBCanvasTest {
@Test
void test(@TempDir Path dir) throws IOException {
var image = new Image(256, 256);
var image = new RGBCanvas(256, 256);
for (var y = 0; y < image.getHeight(); y++) {
for (var x = 0; x < image.getWidth(); x++) {
@ -25,7 +25,7 @@ class ImageTest {
var g = (double) y / (image.getHeight() - 1);
var b = 0;
image.set(x, y, new Color(r, g, b));
image.set(x, y, new ColorRGB(r, g, b));
}
}
@ -35,7 +35,7 @@ class ImageTest {
String expected;
String actual;
try (var in = Objects.requireNonNull(ImageTest.class.getResourceAsStream("simple_image.ppm"))) {
try (var in = Objects.requireNonNull(RGBCanvasTest.class.getResourceAsStream("simple_image.ppm"))) {
expected = new String(in.readAllBytes(), StandardCharsets.US_ASCII);
}

@ -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.texture.Color;
import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB;
import eu.jonahbauer.raytracing.render.material.LambertianMaterial;
import org.junit.jupiter.api.Test;
@ -15,7 +15,7 @@ class SphereTest {
void hit() {
var center = new Vec3(1, 2, 3);
var radius = 5;
var sphere = new Sphere(center, radius, new LambertianMaterial(Color.WHITE));
var sphere = new Sphere(center, radius, new LambertianMaterial(ColorRGB.WHITE));
var origin = new Vec3(6, 7, 8);
var direction = new Vec3(-1, -1, -1);

Loading…
Cancel
Save