Compare commits

..

12 Commits

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

@ -1,9 +1,11 @@
package eu.jonahbauer.raytracing; package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
import eu.jonahbauer.raytracing.render.spectrum.RGBAlbedoSpectrum;
import eu.jonahbauer.raytracing.render.spectrum.RGBIlluminantSpectrum;
import eu.jonahbauer.raytracing.render.texture.CheckerTexture; import eu.jonahbauer.raytracing.render.texture.CheckerTexture;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.camera.SimpleCamera; import eu.jonahbauer.raytracing.render.camera.SimpleCamera;
import eu.jonahbauer.raytracing.render.material.*; import eu.jonahbauer.raytracing.render.material.*;
import eu.jonahbauer.raytracing.render.texture.ImageTexture; import eu.jonahbauer.raytracing.render.texture.ImageTexture;
@ -50,13 +52,14 @@ public class Examples {
public static @NotNull Example getSimpleScene(int height) { public static @NotNull Example getSimpleScene(int height) {
if (height <= 0) height = 675; if (height <= 0) height = 675;
var cs = ColorSpaces.sRGB;
return new Example( return new Example(
new Scene(getSkyBox(), List.of( 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, -100.5, -1.0), 100.0, new LambertianMaterial(cs.albedo(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, 0, -1.2), 0.5, new LambertianMaterial(cs.albedo(0.1, 0.2, 0.5))),
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.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.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(cs.albedo(0.8, 0.6, 0.2)))
)), )),
SimpleCamera.builder() SimpleCamera.builder()
.withImage(height * 16 / 9, height) .withImage(height * 16 / 9, height)
@ -67,11 +70,16 @@ public class Examples {
public static @NotNull Example getSpheres(int height) { public static @NotNull Example getSpheres(int height) {
if (height <= 0) height = 675; if (height <= 0) height = 675;
var cs = ColorSpaces.sRGB;
var rng = new Random(1); var rng = new Random(1);
var objects = new ArrayList<Hittable>(); var objects = new ArrayList<Hittable>();
objects.add(new Sphere( objects.add(new Sphere(
new Vec3(0, -1000, 0), 1000, 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++) { for (int a = -11; a < 11; a++) {
@ -83,13 +91,13 @@ public class Examples {
var rnd = rng.nextDouble(); var rnd = rng.nextDouble();
if (rnd < 0.8) { if (rnd < 0.8) {
// diffuse // diffuse
var albedo = Color.multiply(Color.random(rng), Color.random(rng)); var albedo = ColorRGB.random(rng).times(ColorRGB.random(rng));
material = new LambertianMaterial(albedo); material = new LambertianMaterial(cs.albedo(albedo));
} else if (rnd < 0.95) { } else if (rnd < 0.95) {
// metal // 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; var fuzz = rng.nextDouble() * 0.5;
material = new MetallicMaterial(albedo, fuzz); material = new MetallicMaterial(cs.albedo(albedo), fuzz);
} else { } else {
// glass // glass
material = new DielectricMaterial(1.5); material = new DielectricMaterial(1.5);
@ -100,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(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 LambertianMaterial(cs.albedo(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 MetallicMaterial(cs.albedo(0.7, 0.6, 0.5))));
var camera = SimpleCamera.builder() var camera = SimpleCamera.builder()
.withImage(height * 16 / 9, height) .withImage(height * 16 / 9, height)
@ -117,13 +125,14 @@ public class Examples {
public static @NotNull Example getSquares(int height) { public static @NotNull Example getSquares(int height) {
if (height <= 0) height = 600; if (height <= 0) height = 600;
var cs = ColorSpaces.sRGB;
return new Example( return new Example(
new Scene(getSkyBox(), List.of( 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(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(cs.albedo(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(-2, -2, 0), new Vec3(4, 0, 0), new Vec3(0, 4, 0), new LambertianMaterial(cs.albedo(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(3, -2, 1), new Vec3(0, 0, 4), new Vec3(0, 4, 0), new LambertianMaterial(cs.albedo(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, 1), new Vec3(4, 0, 0), new Vec3(0, 0, 4), new LambertianMaterial(cs.albedo(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(-2, -3, 5), new Vec3(4, 0, 0), new Vec3(0, 0, -4), new LambertianMaterial(cs.albedo(0.2, 0.8, 0.8)))
)), )),
SimpleCamera.builder() SimpleCamera.builder()
.withImage(height, height) .withImage(height, height)
@ -136,12 +145,13 @@ public class Examples {
public static @NotNull Example getLight(int height) { public static @NotNull Example getLight(int height) {
if (height <= 0) height = 225; if (height <= 0) height = 225;
var cs = ColorSpaces.sRGB;
return new Example( return new Example(
new Scene(List.of( 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, -1000, 0), 1000, new LambertianMaterial(cs.albedo(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 Sphere(new Vec3(0, 2, 0), 2, new LambertianMaterial(cs.albedo(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 Parallelogram(new Vec3(3, 1, -2), new Vec3(2, 0, 0), new Vec3(0, 2, 0), new DiffuseLight(cs.illuminant(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, 7, 0), 2, new DiffuseLight(cs.illuminant(4.0)))
)), )),
SimpleCamera.builder() SimpleCamera.builder()
.withImage(height * 16 / 9, height) .withImage(height * 16 / 9, height)
@ -155,10 +165,11 @@ public class Examples {
public static @NotNull Example getCornellBox(int height) { public static @NotNull Example getCornellBox(int height) {
if (height <= 0) height = 600; if (height <= 0) height = 600;
var red = new LambertianMaterial(new Color(.65, .05, .05)); var cs = ColorSpaces.sRGB;
var white = new LambertianMaterial(new Color(.73, .73, .73)); var red = new LambertianMaterial(cs.albedo(.65, .05, .05));
var green = new LambertianMaterial(new Color(.12, .45, .15)); var white = new LambertianMaterial(cs.albedo(.73, .73, .73));
var light = new DiffuseLight(new Color(15.0, 15.0, 15.0)); var green = new LambertianMaterial(cs.albedo(.12, .45, .15));
var light = new DiffuseLight(cs.illuminant(15.0));
return new Example( return new Example(
new Scene(List.of( new Scene(List.of(
@ -182,10 +193,11 @@ public class Examples {
public static @NotNull Example getCornellBoxSmoke(int height) { public static @NotNull Example getCornellBoxSmoke(int height) {
if (height <= 0) height = 600; if (height <= 0) height = 600;
var red = new LambertianMaterial(new Color(.65, .05, .05)); var cs = ColorSpaces.sRGB;
var white = new LambertianMaterial(new Color(.73, .73, .73)); var red = new LambertianMaterial(cs.albedo(.65, .05, .05));
var green = new LambertianMaterial(new Color(.12, .45, .15)); var white = new LambertianMaterial(cs.albedo(.73, .73, .73));
var light = new DiffuseLight(new Color(7.0, 7.0, 7.0)); var green = new LambertianMaterial(cs.albedo(.12, .45, .15));
var light = new DiffuseLight(cs.illuminant(15.0));
return new Example( return new Example(
new Scene(List.of( new Scene(List.of(
@ -195,13 +207,13 @@ public class Examples {
new Box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white) new Box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white)
.rotateY(Math.toRadians(15)) .rotateY(Math.toRadians(15))
.translate(new Vec3(265, 0, 295)), .translate(new Vec3(265, 0, 295)),
0.01, new IsotropicMaterial(Color.BLACK) 0.01, new IsotropicMaterial(cs.albedo(ColorRGB.BLACK))
), ),
new ConstantMedium( new ConstantMedium(
new Box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white) new Box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white)
.rotateY(Math.toRadians(-18)) .rotateY(Math.toRadians(-18))
.translate(new Vec3(130, 0, 65)), .translate(new Vec3(130, 0, 65)),
0.01, new IsotropicMaterial(Color.WHITE) 0.01, new IsotropicMaterial(cs.albedo(ColorRGB.WHITE))
) )
)), )),
SimpleCamera.builder() SimpleCamera.builder()
@ -216,10 +228,11 @@ public class Examples {
public static @NotNull Example getCornellBoxSphere(int height) { public static @NotNull Example getCornellBoxSphere(int height) {
if (height <= 0) height = 600; if (height <= 0) height = 600;
var red = new LambertianMaterial(new Color(.65, .05, .05)); var cs = ColorSpaces.sRGB;
var white = new LambertianMaterial(new Color(.73, .73, .73)); var red = new LambertianMaterial(cs.albedo(.65, .05, .05));
var green = new LambertianMaterial(new Color(.12, .45, .15)); var white = new LambertianMaterial(cs.albedo(.73, .73, .73));
var light = new DiffuseLight(new Color(7.0, 7.0, 7.0)); var green = new LambertianMaterial(cs.albedo(.12, .45, .15));
var light = new DiffuseLight(cs.illuminant(7.0));
var glass = new DielectricMaterial(1.5); 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); var room = new Box(new Vec3(0, 0, 0), new Vec3(555, 555, 555), white, white, red, green, white, null);
@ -243,17 +256,18 @@ public class Examples {
public static @NotNull Example getDiagramm(int height) { public static @NotNull Example getDiagramm(int height) {
if (height <= 0) height = 450; 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( var data = List.of(
new Partei("CDU", new Color(0x00, 0x4B, 0x76), 18.9), new Partei("CDU", new ColorRGB(0x004B76), 18.9),
new Partei("SPD", new Color(0xC0, 0x00, 0x3C), 25.7), new Partei("SPD", new ColorRGB(0xC0003C), 25.7),
new Partei("AfD", new Color(0x80, 0xCD, 0xEC), 10.3), new Partei("AfD", new ColorRGB(0x80CDEC), 10.3),
new Partei("FDP", new Color(0xF7, 0xBB, 0x3D), 11.5), new Partei("FDP", new ColorRGB(0xF7BB3D), 11.5),
new Partei("DIE LINKE", new Color(0x5F, 0x31, 0x6E), 4.9), new Partei("DIE LINKE", new ColorRGB(0x5F316E), 4.9),
new Partei("GRÜNE", new Color(0x00, 0x85, 0x4A), 14.8), new Partei("GRÜNE", new ColorRGB(0x00854A), 14.8),
new Partei("CSU", new Color(0x00, 0x77, 0xB6), 5.2) new Partei("CSU", new ColorRGB(0x0077B6), 5.2)
); );
var white = new LambertianMaterial(new Color(.99, .99, .99)); var white = new LambertianMaterial(cs.albedo(.99, .99, .99));
var count = data.size(); var count = data.size();
var size = 75d; var size = 75d;
@ -273,12 +287,12 @@ public class Examples {
objects.add(new Box( objects.add(new Box(
new Vec3((i + 1) * spacing + i * size, 0, spacing), new Vec3((i + 1) * spacing + i * size, 0, spacing),
new Vec3((i + 1) * spacing + (i + 1) * size, partei.stimmen() * 15, spacing + size), new Vec3((i + 1) * spacing + (i + 1) * size, partei.stimmen() * 15, spacing + size),
new DielectricMaterial(1.5, partei.color()) new DielectricMaterial(1.5, cs.albedo(partei.color()))
)); ));
} }
return new Example( return new Example(
new Scene(new Color(1.25, 1.25, 1.25), objects), new Scene(cs.illuminant().scale(1.25), objects),
SimpleCamera.builder() SimpleCamera.builder()
.withImage(height * 16 / 9, height) .withImage(height * 16 / 9, height)
.withPosition(new Vec3(700, 250, 800)) .withPosition(new Vec3(700, 250, 800))
@ -293,7 +307,7 @@ public class Examples {
return new Example( return new Example(
new Scene(getSkyBox(), List.of( new Scene(getSkyBox(), List.of(
new Sphere(Vec3.ZERO, 2, new LambertianMaterial(new ImageTexture("/earthmap.jpg"))) new Sphere(Vec3.ZERO, 2, new LambertianMaterial(new ImageTexture("earthmap.jpg", ColorSpaces.sRGB)))
)), )),
SimpleCamera.builder() SimpleCamera.builder()
.withImage(height * 16 / 9, height) .withImage(height * 16 / 9, height)
@ -326,12 +340,13 @@ public class Examples {
public static @NotNull Example getFinal(int height) { public static @NotNull Example getFinal(int height) {
if (height <= 0) height = 400; if (height <= 0) height = 400;
var cs = ColorSpaces.sRGB;
var objects = new ArrayList<Hittable>(); var objects = new ArrayList<Hittable>();
var random = new Random(1); var random = new Random(1);
// boxes // boxes
var boxes = new ArrayList<Hittable>(); var boxes = new ArrayList<Hittable>();
var ground = new LambertianMaterial(new Color(0.48, 0.83, 0.53)); var ground = new LambertianMaterial(cs.albedo(0.48, 0.83, 0.53));
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
for (int j = 0; j < 20; j++) { for (int j = 0; j < 20; j++) {
var w = 100.0; var w = 100.0;
@ -349,31 +364,31 @@ public class Examples {
// light // light
objects.add(new Parallelogram( objects.add(new Parallelogram(
new Vec3(123, 554, 147), new Vec3(300, 0, 0), new Vec3(0, 0, 265), new Vec3(123, 554, 147), new Vec3(300, 0, 0), new Vec3(0, 0, 265),
new DiffuseLight(new Color(7., 7., 7.)) new DiffuseLight(cs.illuminant(7.0))
)); ));
// spheres with different materials // 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(cs.albedo(0.7, 0.3, 0.1))));
objects.add(new Sphere(new Vec3(260, 150, 45), 50, new DielectricMaterial(1.5))); objects.add(new Sphere(new Vec3(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(cs.albedo(0.8, 0.8, 0.9), 1.0)));
// glass sphere filled with gas // glass sphere filled with gas
var boundary = new Sphere(new Vec3(360, 150, 145), 70, new DielectricMaterial(1.5)); var boundary = new Sphere(new Vec3(360, 150, 145), 70, new DielectricMaterial(1.5));
objects.add(boundary); 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(cs.albedo(0.2, 0.4, 0.9))));
// put the world in a glass sphere // put the world in a glass sphere
objects.add(new ConstantMedium( objects.add(new ConstantMedium(
new Sphere(new Vec3(0, 0, 0), 5000, new DielectricMaterial(1.5)), 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(cs.albedo(1.0, 1.0, 1.0))
)); ));
// textures spheres // textures spheres
objects.add(new Sphere(new Vec3(400, 200, 400), 100, new LambertianMaterial(new ImageTexture("/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)))); objects.add(new Sphere(new Vec3(220, 280, 300), 80, new LambertianMaterial(new PerlinTexture(0.2))));
// box from spheres // box from spheres
var white = new LambertianMaterial(new Color(.73, .73, .73)); var white = new LambertianMaterial(cs.albedo(.73, .73, .73));
var spheres = new ArrayList<Hittable>(); var spheres = new ArrayList<Hittable>();
for (int j = 0; j < 1000; j++) { for (int j = 0; j < 1000; j++) {
spheres.add(new Sphere(new Vec3(random.nextDouble(165), random.nextDouble(165), random.nextDouble(165)), 10, white)); spheres.add(new Sphere(new Vec3(random.nextDouble(165), random.nextDouble(165), random.nextDouble(165)), 10, white));
@ -392,6 +407,9 @@ public class Examples {
} }
private static @NotNull SkyBox getSkyBox() { 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()
);
} }
} }

@ -1,10 +1,11 @@
package eu.jonahbauer.raytracing; package eu.jonahbauer.raytracing;
import eu.jonahbauer.raytracing.render.ImageFormat;
import eu.jonahbauer.raytracing.render.canvas.Canvas; 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.LiveCanvas;
import eu.jonahbauer.raytracing.render.canvas.XYZCanvas;
import eu.jonahbauer.raytracing.render.image.PNGImageWriter;
import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer; import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer;
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
@ -23,6 +24,7 @@ public class Main {
var renderer = SimpleRenderer.builder() var renderer = SimpleRenderer.builder()
.withSamplesPerPixel(config.samples) .withSamplesPerPixel(config.samples)
.withSpectralSamples(config.spectralSamples)
.withMaxDepth(config.depth) .withMaxDepth(config.depth)
.withIterative(config.iterative) .withIterative(config.iterative)
.withParallel(config.parallel) .withParallel(config.parallel)
@ -30,21 +32,21 @@ public class Main {
Canvas canvas; Canvas canvas;
if (config.preview) { 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(); image.preview();
canvas = image; canvas = image;
} else { } else {
canvas = new Image(camera.getWidth(), camera.getHeight()); canvas = new XYZCanvas(camera.getWidth(), camera.getHeight());
} }
long time = System.nanoTime(); long time = System.nanoTime();
renderer.render(camera, scene, canvas); renderer.render(camera, scene, canvas);
System.out.printf("rendering finished after %dms", (System.nanoTime() - time) / 1_000_000); System.out.printf("rendering finished after %dms", (System.nanoTime() - time) / 1_000_000);
ImageFormat.PNG.write(canvas, config.path); PNGImageWriter.sRGB.write(canvas, config.path);
} }
private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, boolean parallel, int samples, int depth) { private record Config(@NotNull Example example, @NotNull Path path, boolean preview, boolean iterative, boolean parallel, int samples, int spectralSamples, int depth) {
public static @NotNull Config parse(@NotNull String @NotNull[] args) { public static @NotNull Config parse(@NotNull String @NotNull[] args) {
IntFunction<Example> example = null; IntFunction<Example> example = null;
Path path = null; Path path = null;
@ -52,6 +54,7 @@ public class Main {
boolean iterative = false; boolean iterative = false;
boolean parallel = false; boolean parallel = false;
int samples = 1000; int samples = 1000;
int spectralSamples = 4;
int depth = 50; int depth = 50;
int height = -1; int height = -1;
@ -80,6 +83,15 @@ public class Main {
throw fail("value " + args[i] + " is not a valid integer"); throw fail("value " + args[i] + " is not a valid integer");
} }
} }
case "--spectral-samples" -> {
if (i + 1 == args.length) throw fail("missing value for parameter --spectral-samples");
try {
spectralSamples = Integer.parseInt(args[++i]);
if (spectralSamples <= 0) throw fail("spectral samples must be positive");
} catch (NumberFormatException ex) {
throw fail("value " + args[i] + " is not a valid integer");
}
}
case "--depth" -> { case "--depth" -> {
if (i + 1 == args.length) throw fail("missing value for parameter --depth"); if (i + 1 == args.length) throw fail("missing value for parameter --depth");
try { try {
@ -111,7 +123,7 @@ public class Main {
if (example == null) example = Examples::getCornellBoxSmoke; if (example == null) example = Examples::getCornellBoxSmoke;
if (path == null) path = Path.of("scene-" + System.currentTimeMillis() + ".png"); if (path == null) path = Path.of("scene-" + System.currentTimeMillis() + ".png");
return new Config(example.apply(height), path, preview, iterative, parallel, samples, depth); return new Config(example.apply(height), path, preview, iterative, parallel, samples, spectralSamples, depth);
} }
private static @NotNull RuntimeException fail(@NotNull String message) { private static @NotNull RuntimeException fail(@NotNull String message) {

@ -79,33 +79,37 @@ public record AABB(@NotNull Vec3 min, @NotNull Vec3 max) {
* @return {@code true} iff the ray intersects this bounding box, {@code false} otherwise * @return {@code true} iff the ray intersects this bounding box, {@code false} otherwise
*/ */
public boolean hit(@NotNull Ray ray, @NotNull Range range) { public boolean hit(@NotNull Ray ray, @NotNull Range range) {
var origin = ray.origin(); var invDirection = ray.getInvDirection();
var direction = ray.direction(); var negInvOrigin = ray.getNegInvOrigin();
var invDirection = direction.inv();
var tminX = Math.fma(min.x(), invDirection.x(), negInvOrigin.x());
// calculate t values for intersection points of ray with planes through min var tminY = Math.fma(min.y(), invDirection.y(), negInvOrigin.y());
var tmin = intersect(min(), origin, invDirection); var tminZ = Math.fma(min.z(), invDirection.z(), negInvOrigin.z());
// calculate t values for intersection points of ray with planes through max
var tmax = intersect(max(), origin, invDirection); var tmaxX = Math.fma(max.x(), invDirection.x(), negInvOrigin.x());
var tmaxY = Math.fma(max.y(), invDirection.y(), negInvOrigin.y());
// determine range of t for which the ray is inside this voxel var tmaxZ = Math.fma(max.z(), invDirection.z(), negInvOrigin.z());
double tlmax = Double.NEGATIVE_INFINITY; // lower limit maximum
double tumin = Double.POSITIVE_INFINITY; // upper limit minimum var tlmax = max(
Math.min(tminX, tmaxX),
for (int i = 0; i < 3; i++) { Math.min(tminY, tmaxY),
// classify t values as lower or upper limit based on ray direction Math.min(tminZ, tmaxZ)
if (direction.get(i) >= 0) { );
// min is lower limit and max is upper limit var tumin = min(
if (tmin[i] > tlmax) tlmax = tmin[i]; Math.max(tminX, tmaxX),
if (tmax[i] < tumin) tumin = tmax[i]; Math.max(tminY, tmaxY),
} else { Math.max(tminZ, tmaxZ)
// max is lower limit and min is upper limit );
if (tmax[i] > tlmax) tlmax = tmax[i];
if (tmin[i] < tumin) tumin = tmin[i]; return tlmax < tumin && tumin >= range.min() && tlmax <= range.max();
} }
private static double max(double a, double b, double c) {
return Math.max(a, Math.max(b, c));
} }
return tlmax < tumin && tumin >= range.min() && tlmax <= range.max(); private static double min(double a, double b, double c) {
return Math.min(a, Math.min(b, c));
} }
/** /**

@ -0,0 +1,33 @@
package eu.jonahbauer.raytracing.math;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
/**
* A vector-like object that implements the standard mathematical operations
* @param <T> the type
*/
public interface IVec<T extends IVec<T>> {
@Contract(pure = true)
double get(int i);
@Contract(pure = true)
@NotNull T plus(@NotNull T other);
@Contract(pure = true)
@NotNull T minus(@NotNull T other);
@Contract(pure = true)
@NotNull T times(@NotNull T other);
@Contract(pure = true)
@NotNull T times(double d);
@Contract(pure = true)
default @NotNull T div(double d) {
return times(1 / d);
}
@Contract(pure = true)
double @NotNull[] toArray();
}

@ -0,0 +1,47 @@
package eu.jonahbauer.raytracing.math;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
/**
* A vector-like object with three components.
* @param <T> the type
*/
public interface IVec3<T extends Record & IVec3<T>> extends IVec<T> {
@Contract(pure = true)
default double component1() {
return toVec3().x();
}
@Contract(pure = true)
default double component2() {
return toVec3().y();
}
@Contract(pure = true)
default double component3() {
return toVec3().z();
}
@Override
@Contract(pure = true)
default double get(int i) {
return switch (i) {
case 0 -> component1();
case 1 -> component2();
case 2 -> component3();
default -> throw new IndexOutOfBoundsException(i);
};
}
@Contract(pure = true)
default @NotNull Vec3 toVec3() {
return new Vec3(component1(), component2(), component3());
}
@Override
@Contract(pure = true)
default double @NotNull [] toArray() {
return new double[] {component1(), component2(), component3()};
}
}

@ -0,0 +1,255 @@
package eu.jonahbauer.raytracing.math;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public record Matrix3(
double a11, double a12, double a13,
double a21, double a22, double a23,
double a31, double a32, double a33
) {
public static @NotNull Matrix3 fromRows(@NotNull Vec3 @NotNull[] rows) {
if (rows.length != 3) throw new IllegalArgumentException();
return fromRows(rows[0], rows[1], rows[2]);
}
public static @NotNull Matrix3 fromRows(@NotNull Vec3 row0, @NotNull Vec3 row1, @NotNull Vec3 row2) {
return new Matrix3(
row0.x(), row0.y(), row0.z(),
row1.x(), row1.y(), row1.z(),
row2.x(), row2.y(), row2.z()
);
}
public static @NotNull Matrix3 fromColumns(@NotNull Vec3 @NotNull[] cols) {
if (cols.length != 3) throw new IllegalArgumentException();
return fromColumns(cols[0], cols[1], cols[2]);
}
public static @NotNull Matrix3 fromColumns(@NotNull Vec3 col0, @NotNull Vec3 col1, @NotNull Vec3 col2) {
return new Matrix3(
col0.x(), col1.x(), col2.x(),
col0.y(), col1.y(), col2.y(),
col0.z(), col1.z(), col2.z()
);
}
public static @NotNull Matrix3 fromArray(double @NotNull[] @NotNull[] array) {
return new Matrix3(
array[0][0], array[0][1], array[0][2],
array[1][0], array[1][1], array[1][2],
array[2][0], array[2][1], array[2][2]
);
}
public Matrix3() {
this(1, 1, 1);
}
public Matrix3(double a11, double a22, double a33) {
this(a11, 0, 0, 0, a22, 0, 0, 0, a33);
}
public @NotNull Matrix3 times(@NotNull Matrix3 other) {
return new Matrix3(
a11 * other.a11 + a12 * other.a21 + a13 * other.a31,
a11 * other.a12 + a12 * other.a22 + a13 * other.a32,
a11 * other.a13 + a12 * other.a23 + a13 * other.a33,
a21 * other.a11 + a22 * other.a21 + a23 * other.a31,
a21 * other.a12 + a22 * other.a22 + a23 * other.a32,
a21 * other.a13 + a22 * other.a23 + a23 * other.a33,
a31 * other.a11 + a32 * other.a21 + a33 * other.a31,
a31 * other.a12 + a32 * other.a22 + a33 * other.a32,
a31 * other.a13 + a32 * other.a23 + a33 * other.a33
);
}
public @NotNull Matrix3 times(double other) {
return new Matrix3(
a11 * other, a12 * other, a13 * other,
a21 * other, a22 * other, a23 * other,
a31 * other, a32 * other, a33 * other
);
}
public @NotNull Vec3 times(@NotNull Vec3 other) {
return new Vec3(
a11 * other.x() + a12 * other.y() + a13 * other.z(),
a21 * other.x() + a22 * other.y() + a23 * other.z(),
a31 * other.x() + a32 * other.y() + a33 * other.z()
);
}
public @NotNull Matrix3 plus(@NotNull Matrix3 other) {
return new Matrix3(
a11 + other.a11, a12 + other.a12, a13 + other.a13,
a21 + other.a21, a22 + other.a22, a23 + other.a23,
a31 + other.a31, a32 + other.a32, a33 + other.a33
);
}
public double det() {
return a11 * a22 * a33 + a12 * a23 * a31 + a13 * a21 * a32
- a13 * a22 * a31 - a23 * a32 * a11 - a33 * a12 * a21;
}
public @NotNull Matrix3 invert() {
var det = det();
if (det == 0) throw new IllegalStateException();
var t = 1 / det;
return new Matrix3(
t * (Math.fma( a22, a33, -a23 * a32)),
t * (Math.fma(-a12, a33, a13 * a32)),
t * (Math.fma( a12, a23, -a13 * a22)),
t * (Math.fma(-a21, a33, a23 * a31)),
t * (Math.fma( a11, a33, -a13 * a31)),
t * (Math.fma(-a11, a23, a13 * a21)),
t * (Math.fma( a21, a32, -a22 * a31)),
t * (Math.fma(-a11, a32, a12 * a31)),
t * (Math.fma( a11, a22, -a12 * a21))
);
}
public @NotNull Vec3 column(int i) {
return switch (i) {
case 0 -> new Vec3(a11, a21, a31);
case 1 -> new Vec3(a12, a22, a32);
case 2 -> new Vec3(a13, a23, a33);
default -> throw new IndexOutOfBoundsException(i);
};
}
public @NotNull Vec3 @NotNull[] columns() {
return new Vec3[] {
new Vec3(a11, a21, a31),
new Vec3(a12, a22, a32),
new Vec3(a13, a23, a33)
};
}
public @NotNull Vec3 row(int i) {
return switch (i) {
case 0 -> new Vec3(a11, a12, a13);
case 1 -> new Vec3(a21, a22, a23);
case 2 -> new Vec3(a31, a32, a33);
default -> throw new IndexOutOfBoundsException(i);
};
}
public @NotNull Vec3 @NotNull[] rows() {
return new Vec3[] {
new Vec3(a11, a12, a13),
new Vec3(a21, a22, a23),
new Vec3(a31, a32, a33)
};
}
public double @NotNull[] @NotNull[] toArray() {
return new double[][] {
{a11, a12, a13},
{a21, a22, a23},
{a31, a32, a33}
};
}
public double get(int i, int j) {
Objects.checkIndex(i, 3);
Objects.checkIndex(j, 3);
var idx = 3 * i + j;
return switch (idx) {
case 0 -> a11;
case 1 -> a12;
case 2 -> a13;
case 3 -> a21;
case 4 -> a22;
case 5 -> a23;
case 6 -> a31;
case 7 -> a32;
case 8 -> a33;
default -> throw new AssertionError();
};
}
/**
* Performs lower-upper decomposition with partial pivoting (LUP decomposition) on {@code this} matrix.
* @param tolerance a small tolerance number to detect failure when the matrix is near degenerate
* @see <a href="https://en.wikipedia.org/w/index.php?title=LU_decomposition&oldid=1213102558#C_code_example">LU decomposition Wikipedia, The Free Encyclopedia</a>
*/
public @NotNull LUPDecomposition decompose(double tolerance) {
// unit permutation matrix
var perm = new int[] {0, 1, 2, 3};
var A = toArray();
var N = 3;
for (int i = 0; i < N; i++) {
double maxA = 0.0;
int imax = i;
for (int k = i; k < N; k++) {
double absA = Math.abs(A[k][i]);
if (absA > maxA) {
maxA = absA;
imax = k;
}
}
if (maxA < tolerance) throw new IllegalArgumentException("matrix is degenerate");
if (imax != i) {
// pivoting P
int j = perm[i];
perm[i] = perm[imax];
perm[imax] = j;
// pivoting rows of A
var ptr = A[i];
A[i] = A[imax];
A[imax] = ptr;
// counting pivots starting from N (for determinant)
perm[3]++;
}
for (int j = i + 1; j < N; j++) {
A[j][i] /= A[i][i];
for (int k = i + 1; k < N; k++) {
A[j][k] -= A[j][i] * A[i][k];
}
}
}
return new LUPDecomposition(fromArray(A), perm);
}
public record LUPDecomposition(@NotNull Matrix3 matrix, int @NotNull[] permutation) {
/**
* Solves the equation {@code Ax = b} where {@code A} is the matrix that {@code this} decomposition was derived
* from.
* @param b the right hand side vector
* @return the solution vector
*/
public @NotNull Vec3 solve(@NotNull Vec3 b) {
var N = 3;
var x = new double[N];
for (int i = 0; i < N; i++) {
x[i] = b.get(permutation[i]);
for (int k = 0; k < i; k++) {
x[i] -= matrix.get(i, k) * x[k];
}
}
for (int i = N - 1; i >= 0; i--) {
for (int k = i + 1; k < N; k++) {
x[i] -= matrix.get(i, k) * x[k];
}
x[i] /= matrix.get(i, i);
}
return new Vec3(x[0], x[1], x[2]);
}
}
}

@ -1,16 +1,97 @@
package eu.jonahbauer.raytracing.math; package eu.jonahbauer.raytracing.math;
import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths;
import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) { public final class Ray {
public Ray { private final @NotNull Vec3 origin;
Objects.requireNonNull(origin, "origin"); private final @NotNull Vec3 direction;
Objects.requireNonNull(direction, "direction"); private final @NotNull SampledWavelengths lambda;
private final @NotNull Vec3 inv;
private final @NotNull Vec3 negInvOrigin;
public Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) {
this(origin, direction, SampledWavelengths.EMPTY);
}
public Ray(@NotNull Vec3 origin, @NotNull Vec3 direction, @NotNull SampledWavelengths lambda) {
this.origin = Objects.requireNonNull(origin, "origin");
this.direction = Objects.requireNonNull(direction, "direction");
this.lambda = Objects.requireNonNull(lambda, "lambda");
this.inv = direction.inv();
this.negInvOrigin = inv.neg().times(origin);
}
private Ray(@NotNull Vec3 origin, @NotNull Vec3 direction, @NotNull SampledWavelengths lambda, @NotNull Vec3 inv, @NotNull Vec3 negInvOrigin) {
this.origin = origin;
this.direction = direction;
this.lambda = lambda;
this.inv = inv;
this.negInvOrigin = negInvOrigin;
} }
public @NotNull Vec3 at(double t) { public @NotNull Vec3 at(double t) {
return Vec3.fma(t, direction, origin); return Vec3.fma(t, direction, origin);
} }
public @NotNull Ray with(@NotNull HitResult hit, @NotNull Vec3 direction) {
return new Ray(hit.position(), direction, lambda);
}
public @NotNull Ray with(@NotNull Vec3 origin, @NotNull Vec3 direction) {
return new Ray(origin, direction, lambda);
}
public @NotNull Ray with(@NotNull SampledWavelengths lambda) {
return new Ray(origin, direction, lambda, inv, negInvOrigin);
}
public @NotNull Vec3 origin() {
return origin;
}
public @NotNull Vec3 direction() {
return direction;
}
public @NotNull SampledWavelengths lambda() {
return lambda;
}
public @NotNull Vec3 getInvDirection() {
return inv;
}
public @NotNull Vec3 getNegInvOrigin() {
return negInvOrigin;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Ray) obj;
return Objects.equals(this.origin, that.origin) &&
Objects.equals(this.direction, that.direction) &&
Objects.equals(this.lambda, that.lambda);
}
@Override
public int hashCode() {
return Objects.hash(origin, direction, lambda);
}
@Override
public @NotNull String toString() {
return "Ray[" +
"origin=" + origin + ", " +
"direction=" + direction + ", " +
"lambda=" + lambda + ']';
}
} }

@ -7,10 +7,8 @@ import java.util.random.RandomGenerator;
import static eu.jonahbauer.raytracing.Main.DEBUG; import static eu.jonahbauer.raytracing.Main.DEBUG;
public record Vec3(double x, double y, double z) { public record Vec3(double x, double y, double z) implements IVec3<Vec3> {
public static final Vec3 ZERO = new Vec3(0, 0, 0); public static final Vec3 ZERO = new Vec3(0, 0, 0);
public static final Vec3 MAX = new Vec3(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
public static final Vec3 MIN = new Vec3(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE);
public static final Vec3 UNIT_X = new Vec3(1, 0, 0); public static final Vec3 UNIT_X = new Vec3(1, 0, 0);
public static final Vec3 UNIT_Y = new Vec3(0, 1, 0); public static final Vec3 UNIT_Y = new Vec3(0, 1, 0);
public static final Vec3 UNIT_Z = new Vec3(0, 0, 1); public static final Vec3 UNIT_Z = new Vec3(0, 0, 1);
@ -62,7 +60,7 @@ public record Vec3(double x, double y, double z) {
* @return the reflected vector * @return the reflected vector
*/ */
public static @NotNull Vec3 reflect(@NotNull Vec3 vec, @NotNull Vec3 normal) { public static @NotNull Vec3 reflect(@NotNull Vec3 vec, @NotNull Vec3 normal) {
var factor = - 2 * normal.times(vec); var factor = - 2 * normal.dot(vec);
return Vec3.fma(factor, normal, vec); return Vec3.fma(factor, normal, vec);
} }
@ -75,7 +73,7 @@ public record Vec3(double x, double y, double z) {
*/ */
public static @NotNull Optional<Vec3> refract(@NotNull Vec3 vec, @NotNull Vec3 normal, double ri) { public static @NotNull Optional<Vec3> refract(@NotNull Vec3 vec, @NotNull Vec3 normal, double ri) {
vec = vec.unit(); vec = vec.unit();
var cosTheta = Math.min(- vec.times(normal), 1.0); var cosTheta = Math.min(- vec.dot(normal), 1.0);
var sinTheta = Math.sqrt(1 - cosTheta * cosTheta); var sinTheta = Math.sqrt(1 - cosTheta * cosTheta);
if (ri * sinTheta > 1) return Optional.empty(); if (ri * sinTheta > 1) return Optional.empty();
@ -162,10 +160,22 @@ public record Vec3(double x, double y, double z) {
); );
} }
public static @NotNull Vec3 fma(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Vec3 c) {
return new Vec3(
Math.fma(a.x(), b.x(), c.x()),
Math.fma(a.y(), b.y(), c.y()),
Math.fma(a.z(), b.z(), c.z())
);
}
public static double tripleProduct(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Vec3 c) { public static double tripleProduct(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Vec3 c) {
return a.x * b.y * c.z + a.y * b.z * c.x + a.z * b.x * c.y - c.x * b.y * a.z - c.y * b.z * a.x - c.z * b.x * a.y; return a.x * b.y * c.z + a.y * b.z * c.x + a.z * b.x * c.y - c.x * b.y * a.z - c.y * b.z * a.x - c.z * b.x * a.y;
} }
/*
* Math
*/
public @NotNull Vec3 plus(double x, double y, double z) { public @NotNull Vec3 plus(double x, double y, double z) {
return new Vec3(this.x + x, this.y + y, this.z + z); return new Vec3(this.x + x, this.y + y, this.z + z);
} }
@ -179,6 +189,7 @@ public record Vec3(double x, double y, double z) {
* @param other a vector * @param other a vector
* @return the sum of this and the other vector * @return the sum of this and the other vector
*/ */
@Override
public @NotNull Vec3 plus(@NotNull Vec3 other) { public @NotNull Vec3 plus(@NotNull Vec3 other) {
return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z); return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z);
} }
@ -188,49 +199,58 @@ public record Vec3(double x, double y, double z) {
* @param other a vector * @param other a vector
* @return the difference of this and the other vector * @return the difference of this and the other vector
*/ */
@Override
public @NotNull Vec3 minus(@NotNull Vec3 other) { public @NotNull Vec3 minus(@NotNull Vec3 other) {
return new Vec3(this.x - other.x, this.y - other.y, this.z - other.z); return new Vec3(this.x - other.x, this.y - other.y, this.z - other.z);
} }
/**
* Computes the scalar product of this and another vector
* @param other a vector
* @return the scalar product
*/
public double times(@NotNull Vec3 other) {
return this.x * other.x + this.y * other.y + this.z * other.z;
}
/** /**
* Multiplies this vector with a scalar * Multiplies this vector with a scalar
* @param t a scalar * @param t a scalar
* @return the product of this vector and the scalar * @return the product of this vector and the scalar
*/ */
@Override
public @NotNull Vec3 times(double t) { public @NotNull Vec3 times(double t) {
return new Vec3(this.x * t, this.y * t, this.z * t); return new Vec3(this.x * t, this.y * t, this.z * t);
} }
/** /**
* Negates this vector. * Multiplies this vector with another vector component-wise.
* {@return the negated vector} * @param other a vector
* @return the component-wise product of this vector and the other vector
*/ */
public @NotNull Vec3 neg() { @Override
return new Vec3(-x, -y, -z); public @NotNull Vec3 times(@NotNull Vec3 other) {
return new Vec3(this.x * other.x, this.y * other.y, this.z * other.z);
} }
/** /**
* Inverts each component of this vector. * Divides this vector by a scalar
* @return the inverted vector. * @param t a scalar
* @return this vector divided by the scalar
*/ */
public @NotNull Vec3 inv() { @Override
return new Vec3(1 / x, 1 / y, 1 / z); public @NotNull Vec3 div(double t) {
return times(1 / t);
} }
/** /**
* Computes the cross-product of this and another vector * Computes the scalar product of this and another vector
* @param other a vector * @param other a vector
* @return the cross-product * @return the scalar product
*/ */
public double dot(@NotNull Vec3 other) {
return this.x * other.x + this.y * other.y + this.z * other.z;
}
public @NotNull Vec3 neg() {
return new Vec3(-x, -y, -z);
}
public @NotNull Vec3 inv() {
return new Vec3(1 / x, 1 / y, 1 / z);
}
public @NotNull Vec3 cross(@NotNull Vec3 other) { public @NotNull Vec3 cross(@NotNull Vec3 other) {
return new Vec3( return new Vec3(
Math.fma(this.y, other.z, - other.y * this.z), Math.fma(this.y, other.z, - other.y * this.z),
@ -239,15 +259,6 @@ public record Vec3(double x, double y, double z) {
); );
} }
/**
* Divides this vector by a scalar
* @param t a scalar
* @return this vector divided by the scalar
*/
public @NotNull Vec3 div(double t) {
return new Vec3(this.x / t, this.y / t, this.z / t);
}
/** /**
* {@return the squared length of this vector} * {@return the squared length of this vector}
*/ */
@ -279,17 +290,28 @@ public record Vec3(double x, double y, double z) {
return div(Math.sqrt(squared)); return div(Math.sqrt(squared));
} }
/** /*
* {@return the n-th component of this vector} * Accessors
* @param axis the component index
*/ */
public double get(int axis) {
return switch (axis) { @Override
case 0 -> x; public double component1() {
case 1 -> y; return x;
case 2 -> z; }
default -> throw new IndexOutOfBoundsException(axis);
}; @Override
public double component2() {
return y;
}
@Override
public double component3() {
return z;
}
@Override
public @NotNull Vec3 toVec3() {
return this;
} }
public @NotNull Vec3 withX(double x) { public @NotNull Vec3 withX(double x) {

@ -1,134 +0,0 @@
package eu.jonahbauer.raytracing.render;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.canvas.Image;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
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 {
@Override
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) {
writer.write("P3\n");
writer.write(String.valueOf(image.getWidth()));
writer.write(" ");
writer.write(String.valueOf(image.getHeight()));
writer.write("\n255\n");
var it = image.pixels().iterator();
while (it.hasNext()) {
var color = it.next();
writer.write(String.valueOf(color.red()));
writer.write(" ");
writer.write(String.valueOf(color.green()));
writer.write(" ");
writer.write(String.valueOf(color.blue()));
writer.write("\n");
}
}
}
},
PNG {
private static final byte[] MAGIC = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private static final int IHDR_LENGTH = 13;
private static final int IHDR_TYPE = 0x49484452;
private static final int IDAT_TYPE = 0x49444154;
private static final int IEND_TYPE = 0x49454E44;
private static final int IEND_CRC = 0xAE426082;
@Override
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
try (var data = new NoCloseDataOutputStream(out); var _ = data.closeable()) {
data.write(MAGIC);
writeIHDR(image, data);
writeIDAT(image, data);
writeIEND(image, data);
}
}
private void writeIHDR(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
data.writeInt(IHDR_LENGTH);
try (
var crc = new CheckedOutputStream(data, new CRC32());
var ihdr = new DataOutputStream(crc)
) {
ihdr.writeInt(IHDR_TYPE);
ihdr.writeInt(image.getWidth()); // image width
ihdr.writeInt(image.getHeight()); // image height
ihdr.writeByte(8); // bit depth
ihdr.writeByte(2); // color type
ihdr.writeByte(0); // compression method
ihdr.writeByte(0); // filter method
ihdr.writeByte(0); // interlace method
ihdr.flush();
data.writeInt((int) crc.getChecksum().getValue());
}
}
private void writeIDAT(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
try (
var baos = new ByteArrayOutputStream();
var crc = new CheckedOutputStream(baos, new CRC32());
var idat = new DataOutputStream(crc)
) {
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();
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
data.write(bytes);
data.writeInt((int) crc.getChecksum().getValue());
}
}
private void writeIEND(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
data.writeInt(0);
data.writeInt(IEND_TYPE);
data.writeInt(IEND_CRC);
}
private static class NoCloseDataOutputStream extends DataOutputStream {
public NoCloseDataOutputStream(OutputStream out) {
super(out);
}
@Override
public void close() {
// do nothing
}
public Closeable closeable() {
return super::close;
}
}
},
;
public void write(@NotNull Canvas image, @NotNull Path path) throws IOException {
try (var out = Files.newOutputStream(path)) {
write(image, out);
}
}
public abstract void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException;
}

@ -1,22 +1,37 @@
package eu.jonahbauer.raytracing.render.canvas; package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths;
import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public interface Canvas { public interface Canvas {
/**
* {@return the width of this canvas}
*/
int getWidth(); int getWidth();
/**
* {@return the height of this canvas}
*/
int getHeight(); 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()) * {@return the color at a given pixel}
.mapToObj(y -> IntStream.range(0, getWidth()).mapToObj(x -> get(x, y))) * @param x the pixel x coordinate
.flatMap(Function.identity()); * @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; package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths;
import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.swing.*; import javax.swing.*;
@ -12,10 +15,12 @@ import java.awt.image.BufferedImage;
public final class LiveCanvas implements Canvas { public final class LiveCanvas implements Canvas {
private final @NotNull Canvas delegate; private final @NotNull Canvas delegate;
private final @NotNull BufferedImage image; 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.delegate = delegate;
this.image = new BufferedImage(delegate.getWidth(), delegate.getHeight(), BufferedImage.TYPE_INT_RGB); this.image = new BufferedImage(delegate.getWidth(), delegate.getHeight(), BufferedImage.TYPE_INT_RGB);
this.cs = cs;
} }
@Override @Override
@ -29,15 +34,16 @@ public final class LiveCanvas implements Canvas {
} }
@Override @Override
public void set(int x, int y, @NotNull Color color) { public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) {
delegate.set(x, y, color); delegate.add(x, y, n, spectrum, lambda);
var color = cs.encode(delegate.getRGB(x, y, cs));
var rgb = color.red() << 16 | color.green() << 8 | color.blue(); var rgb = color.red() << 16 | color.green() << 8 | color.blue();
image.setRGB(x, y, rgb); image.setRGB(x, y, rgb);
} }
@Override @Override
public @NotNull Color get(int x, int y) { public @NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs) {
return delegate.get(x, y); return delegate.getRGB(x, y, cs);
} }
public @NotNull Thread preview() { public @NotNull Thread preview() {

@ -0,0 +1,76 @@
package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.spectrum.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths;
import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.color.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.spectrum.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths;
import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.color.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);
}
}

@ -0,0 +1,9 @@
package eu.jonahbauer.raytracing.render.color;
/**
* A pair of chromaticity coordinates in the xyY color space
* @param x the x coordinate
* @param y the y coordinate
*/
public record Chromaticity(double x, double y) {
}

@ -0,0 +1,115 @@
package eu.jonahbauer.raytracing.render.color;
import eu.jonahbauer.raytracing.math.IVec3;
import org.jetbrains.annotations.NotNull;
import java.util.Random;
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) / 255d, ((rgb >> 8) & 0xFF) / 255d, (rgb & 0xFF) / 255d);
}
public ColorRGB {
assert Double.isFinite(r) : "r must be finite";
assert Double.isFinite(g) : "g must be finite";
assert Double.isFinite(b) : "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)
);
}
@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);
}
/*
* 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;
}
}

@ -0,0 +1,164 @@
package eu.jonahbauer.raytracing.render.color;
import eu.jonahbauer.raytracing.math.Matrix3;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectrum.*;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
* An RGB color space.
*/
public final class ColorSpace {
private final @NotNull Chromaticity r;
private final @NotNull Chromaticity g;
private final @NotNull Chromaticity b;
private final @NotNull Chromaticity w;
private final @NotNull DenselySampledSpectrum illuminant;
private final @NotNull ColorXYZ R;
private final @NotNull ColorXYZ G;
private final @NotNull ColorXYZ B;
private final @NotNull ColorXYZ W;
private final @NotNull Matrix3 XYZfromRGB;
private final @NotNull Matrix3 RGBfromXYZ;
private final @NotNull SpectrumTable RGBtoSpectrumTable;
private final @NotNull TransferFunction transferFunction;
public ColorSpace(
@NotNull Chromaticity r, @NotNull Chromaticity g, @NotNull Chromaticity b,
@NotNull Spectrum illuminant, @NotNull SpectrumTable table, @NotNull TransferFunction transferFunction
) {
this.r = Objects.requireNonNull(r, "r");
this.g = Objects.requireNonNull(g, "g");
this.b = Objects.requireNonNull(b, "b");
this.illuminant = new DenselySampledSpectrum(illuminant);
this.RGBtoSpectrumTable = table; // no null-check
this.transferFunction = transferFunction; // no null-check
this.W = illuminant.toXYZ();
this.w = W.xy();
this.R = new ColorXYZ(r);
this.G = new ColorXYZ(g);
this.B = new ColorXYZ(b);
var rgb = new Matrix3(
R.x(), G.x(), B.x(),
R.y(), G.y(), B.y(),
R.z(), G.z(), B.z()
);
var C = rgb.invert().times(W.toVec3());
this.XYZfromRGB = rgb.times(new Matrix3(C.x(), C.y(), C.z()));
this.RGBfromXYZ = XYZfromRGB.invert();
}
/*
* Conversions
*/
public @NotNull ColorRGB toRGB(@NotNull ColorXYZ xyz) {
var out = RGBfromXYZ.times(xyz.toVec3());
return new ColorRGB(out.x(), out.y(), out.z());
}
public @NotNull ColorXYZ toXYZ(@NotNull ColorRGB rgb) {
var out = XYZfromRGB.times(rgb.toVec3());
return new ColorXYZ(out);
}
public @NotNull Vec3 toCIELab(@NotNull ColorRGB rgb) {
return toCIELab(toXYZ(rgb));
}
public @NotNull Vec3 toCIELab(@NotNull ColorXYZ xyz) {
return new Vec3(
116 * cieLabCbrt(xyz.y() / W.y()) - 16,
500 * (cieLabCbrt(xyz.x() / W.x()) - cieLabCbrt(xyz.y() / W.y())),
200 * (cieLabCbrt(xyz.y() / W.y()) - cieLabCbrt(xyz.z() / W.z()))
);
}
private static double cieLabCbrt(double x) {
var delta = 6.0 / 29.0;
if (x > delta * delta * delta) {
return Math.cbrt(x);
} else {
return x / (delta * delta * 3.0) + (4.0 / 29.0);
}
}
public @NotNull ColorRGB encode(@NotNull ColorRGB rgb) {
return transferFunction.encode(rgb);
}
public @NotNull ColorRGB decode(@NotNull ColorRGB rgb) {
return transferFunction.decode(rgb);
}
/*
* Spectrum
*/
public @NotNull SigmoidPolynomial toPolynomial(@NotNull ColorRGB rgb) {
return RGBtoSpectrumTable.get(new ColorRGB(
Math.max(0, rgb.r()),
Math.max(0, rgb.g()),
Math.max(0, rgb.b())
));
}
public @NotNull Spectrum toSpectrum(@NotNull ColorRGB rgb, @NotNull Spectrum.Type type) {
return switch (type) {
case ALBEDO -> new RGBAlbedoSpectrum(this, rgb);
case ILLUMINANT -> new RGBIlluminantSpectrum(this, rgb);
case UNBOUNDED -> new RGBUnboundedSpectrum(this, rgb);
};
}
public @NotNull Spectrum albedo(double r, double g, double b) {
return albedo(new ColorRGB(r, g, b));
}
public @NotNull Spectrum albedo(@NotNull ColorRGB rgb) {
return toSpectrum(rgb, Spectrum.Type.ALBEDO);
}
public @NotNull Spectrum illuminant(double intensity) {
return illuminant.scale(intensity);
}
public @NotNull Spectrum illuminant(double r, double g, double b) {
return illuminant(new ColorRGB(r, g, b));
}
public @NotNull Spectrum illuminant(@NotNull ColorRGB rgb) {
return toSpectrum(rgb, Spectrum.Type.ILLUMINANT);
}
/*
* Accessors
*/
public @NotNull Chromaticity r() {
return r;
}
public @NotNull Chromaticity g() {
return g;
}
public @NotNull Chromaticity b() {
return b;
}
public @NotNull Chromaticity w() {
return w;
}
public @NotNull DenselySampledSpectrum illuminant() {
return illuminant;
}
}

@ -0,0 +1,44 @@
package eu.jonahbauer.raytracing.render.color;
import eu.jonahbauer.raytracing.render.spectrum.Spectra;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
public final class ColorSpaces {
// Rec. ITU-R BT.709.3
public static final @NotNull ColorSpace sRGB = new ColorSpace(
new Chromaticity(0.6400, 0.3300),
new Chromaticity(0.3000, 0.6000),
new Chromaticity(0.1500, 0.0600),
Spectra.D65, read("sRGB_spectrum.bin"), TransferFunctions.sRGB
);
// P3-D65 (display)
public static final @NotNull ColorSpace DCI_P3 = new ColorSpace(
new Chromaticity(0.680, 0.320),
new Chromaticity(0.265, 0.690),
new Chromaticity(0.150, 0.060),
Spectra.D65, read("DCI_P3_spectrum.bin"), TransferFunctions.sRGB
);
// ITU-R Rec BT.2020
public static final @NotNull ColorSpace Rec2020 = new ColorSpace(
new Chromaticity(0.708, 0.292),
new Chromaticity(0.170, 0.797),
new Chromaticity(0.131, 0.046),
Spectra.D65, read("Rec2020_spectrum.bin"), null
);
private static @NotNull SpectrumTable read(@NotNull String name) {
try (var in = ColorSpaces.class.getResourceAsStream("/eu/jonahbauer/raytracing/colorspace/" + name)) {
return SpectrumTable.read(Objects.requireNonNull(in));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private ColorSpaces() {
throw new UnsupportedOperationException();
}
}

@ -0,0 +1,111 @@
package eu.jonahbauer.raytracing.render.color;
import eu.jonahbauer.raytracing.math.IVec3;
import eu.jonahbauer.raytracing.math.Vec3;
import org.jetbrains.annotations.NotNull;
/**
* A CIE XYZ color
*/
public record ColorXYZ(double x, double y, double z) implements IVec3<ColorXYZ> {
public static final double CIE_Y_INTEGRAL = 106.85689500000002;
public static final @NotNull ColorXYZ BLACK = new ColorXYZ(0, 0, 0);
public static final @NotNull ColorXYZ WHITE = new ColorXYZ(0, 1, 0);
public ColorXYZ(@NotNull Chromaticity chromaticity) {
this(chromaticity, 1);
}
public ColorXYZ(@NotNull Chromaticity chromaticity, double Y) {
this(
chromaticity.y() == 0 ? 0 : Y * chromaticity.x() / chromaticity.y(),
chromaticity.y() == 0 ? 0 : Y,
chromaticity.y() == 0 ? 0 : Y * (1 - chromaticity.x() - chromaticity.y()) / chromaticity.y()
);
}
public ColorXYZ(@NotNull Vec3 vec) {
this(vec.x(), vec.y(), vec.z());
}
public ColorXYZ {
assert Double.isFinite(x) : "x must be finite";
assert Double.isFinite(y) : "y must be finite";
assert Double.isFinite(z) : "z must be finite";
}
/*
* Math
*/
public static @NotNull ColorXYZ average(@NotNull ColorXYZ a, @NotNull ColorXYZ b, int index) {
return lerp(a, b, 1d / index);
}
public static @NotNull ColorXYZ lerp(@NotNull ColorXYZ a, @NotNull ColorXYZ b, double t) {
if (t < 0) return a;
if (t > 1) return b;
return new ColorXYZ(
Math.fma(t, b.x - a.x, a.x),
Math.fma(t, b.y - a.y, a.y),
Math.fma(t, b.z - a.z, a.z)
);
}
public static @NotNull ColorXYZ fma(@NotNull ColorXYZ a, @NotNull ColorXYZ b, @NotNull ColorXYZ c) {
return new ColorXYZ(
Math.fma(a.x, b.x, c.x),
Math.fma(a.y, b.y, c.y),
Math.fma(a.z, b.z, c.z)
);
}
@Override
public @NotNull ColorXYZ plus(@NotNull ColorXYZ other) {
return new ColorXYZ(x + other.x, y + other.y, z + other.z);
}
@Override
public @NotNull ColorXYZ minus(@NotNull ColorXYZ other) {
return new ColorXYZ(x - other.x, y - other.y, z - other.z);
}
@Override
public @NotNull ColorXYZ times(@NotNull ColorXYZ other) {
return new ColorXYZ(x * other.x, y * other.y, z * other.z);
}
@Override
public @NotNull ColorXYZ times(double d) {
return new ColorXYZ(x * d, y * d, z * d);
}
/*
* Accessors
*/
public double average() {
return (x + y + z) / 3;
}
public @NotNull Chromaticity xy() {
var factor = 1 / (x + y + z);
return new Chromaticity(factor * x, factor * y);
}
@Override
public double component1() {
return x;
}
@Override
public double component2() {
return y;
}
@Override
public double component3() {
return z;
}
}

@ -0,0 +1,34 @@
package eu.jonahbauer.raytracing.render.color;
import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
/**
* A function of the form {@code s(p(x))} where {@code p} is a polynomial of second degree and {@code s} is the sigmoid
* function <code>s(x) = 0.5 + x / (2 * sqrt(1 + x^2))</code>.
* <p>
* A function of this form is used to generate a {@link Spectrum} from an RGB value.
*
* @param c0 the coefficient of the quadratic term
* @param c1 the coefficient of the linear term
* @param c2 the coefficient of the constant term
*/
public record SigmoidPolynomial(double c0, double c1, double c2) {
public double get(double x) {
var p = Math.fma(Math.fma(c0, x, c1), x, c2);
if (!Double.isFinite(p)) return p > 0 ? 1 : 0;
return Math.fma(.5 * p, 1 / Math.sqrt(Math.fma(p, p, 1)), .5);
}
public double max() {
// evaluate at the edges
var result = Math.max(get(Spectrum.LAMBDA_MIN), get(Spectrum.LAMBDA_MAX));
var lambda = -c1 / (2 * c0);
if (lambda >= 360 && lambda <= 830) {
// evaluate at the vertex
return Math.max(result, get(lambda));
} else {
return result;
}
}
}

@ -0,0 +1,142 @@
package eu.jonahbauer.raytracing.render.color;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.Arrays;
/**
* A table of sigmoid polynomials used to convert between RGB values and spectra.
* <p>
* The rgb values are renormalized to xyz coordinates with {@code z} being the largest of the rgb components, and
* {@code x} and {@code y} being the other two rgb components divided by {@code z}. By this construction, {@code x},
* {@code y} and {@code z} are all in the range [0, 1] which allows for better use of the samples in a fixed grid.
* The {@code z} coordinate is additionally remapped using {@link #zNodes} to improve sampling at both ends of the scale.
* <p>
* The coefficients of the sigmoid functions are stored in a flattened five-dimensional array with indices as described
* in {@link #coefficients}.
*/
public final class SpectrumTable {
private final int resolution;
/**
* the remapped {@code z} values
*/
private final double[] zNodes;
/**
* the stored coefficients as a flattened five-dimensional array with the following indices
* <ol>
* <li>the component index of the biggest rgb component</li>
* <li>the {@code z} coordinate</li>
* <li>the {@code y} coordinate</li>
* <li>the {@code x} coordinate</li>
* <li>the coefficient index</li>
* </ol>
*/
private final double[] coefficients;
public static void write(@NotNull SpectrumTable table, @NotNull OutputStream out) throws IOException {
var dos = new DataOutputStream(out);
dos.writeInt(table.resolution);
for (double z : table.zNodes) {
dos.writeDouble(z);
}
for (double c : table.coefficients) {
dos.writeDouble(c);
}
dos.flush();
}
public static @NotNull SpectrumTable read(@NotNull InputStream in) throws IOException {
var dis = new DataInputStream(in);
var resolution = dis.readInt();
var nodes = new double[resolution];
for (int i = 0; i < resolution; i++) {
nodes[i] = dis.readDouble();
}
var table = new double[3 * resolution * resolution * resolution * 3];
for (int i = 0; i < table.length; i++) {
table[i] = dis.readDouble();
}
return new SpectrumTable(resolution, nodes, table);
}
SpectrumTable(int resolution, double @NotNull[] zNodes, double[] coefficients) {
this.resolution = resolution;
this.zNodes = zNodes;
this.coefficients = coefficients;
// check input array lengths
if (zNodes.length != resolution) {
throw new IllegalArgumentException("length of zNodes must be equal to the RESOLUTION");
}
if (coefficients.length != 3 * resolution * resolution * resolution * 3) {
throw new IllegalArgumentException("coefficients length must be 3 * RESOLUTION * RESOLUTION * RESOLUTION * 3");
}
// check ascending zNodes
for (int i = 1; i < resolution; i++) {
if (zNodes[i - 1] >= zNodes[i]) {
throw new IllegalArgumentException("zNodes must be in increasing order");
}
}
if (zNodes[0] != 0.0 || zNodes[zNodes.length - 1] != 1.0) {
throw new IllegalArgumentException("zNodes must start with 0.0 and end with 1.0");
}
}
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())));
}
// find maximum component and compute remapped component values
var max = color.r() > color.g()
? (color.r() > color.b() ? 0 : 2)
: (color.g() > color.b() ? 1 : 2);
var z = color.get(max);
var x = color.get((max + 1) % 3) * (resolution - 1) / z;
var y = color.get((max + 2) % 3) * (resolution - 1) / z;
// compute integer indices and offsets for coefficient interpolation
int xi = Math.min((int) x, resolution - 2);
int yi = Math.min((int) y, resolution - 2);
int zi = Arrays.binarySearch(zNodes, z);
if (zi < 0) {
zi = -zi - 2;
} else if (zi > 0) {
zi = zi - 1;
}
var dx = x - xi;
var dy = y -yi;
var dz = (z - zNodes[zi]) / (zNodes[zi + 1] - zNodes[zi]);
// trilinearly interpolate sigmoid polynomial coefficients
var c = new double[3];
for (int i = 0; i < 3; i++) {
c[i] = lerp(dz,
lerp(dy,
lerp(dx, get(max, zi + 0, yi + 0, xi + 0, i), get(max, zi + 0, yi + 0, xi + 1, i)),
lerp(dx, get(max, zi + 0, yi + 1, xi + 0, i), get(max, zi + 0, yi + 1, xi + 1, i))
),
lerp(dy,
lerp(dx, get(max, zi + 1, yi + 0, xi + 0, i), get(max, zi + 1, yi + 0, xi + 1, i)),
lerp(dx, get(max, zi + 1, yi + 1, xi + 0, i), get(max, zi + 1, yi + 1, xi + 1, i))
)
);
}
return new SigmoidPolynomial(c[0], c[1], c[2]);
}
private double get(int l, int z, int y, int x, int i) {
return coefficients[(((l * resolution + z) * resolution + y) * resolution + x) * 3 + i];
}
private static double lerp(double t, double a, double b) {
return Math.fma(t, b, Math.fma(-t, a, a));
}
}

@ -0,0 +1,204 @@
package eu.jonahbauer.raytracing.render.color;
import eu.jonahbauer.raytracing.math.Matrix3;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.IntStream;
/**
* Generates a lookup table for RGB to spectrum conversion.
* <p>
* The spectrum for each RGB value is a {@link SigmoidPolynomial} with coefficients such that the round trip error
* from converting the RGB value to a spectrum and back is minimized.
* <p>
* <img src="doc-files/rgb2spectrum.png">
*/
public final class SpectrumTableGenerator {
private static final double EPSILON = 1e-4;
private static final int ITERATIONS = 15;
private final int resolution = 64;
private final @NotNull ColorSpace cs;
public static void main(String[] args) throws IOException {
var generator = new SpectrumTableGenerator(ColorSpaces.DCI_P3);
var table = generator.generate();
try (var out = Files.newOutputStream(Path.of("DCI_P3_spectrum.bin"))) {
SpectrumTable.write(table, out);
}
}
public SpectrumTableGenerator(@NotNull ColorSpace cs) {
this.cs = Objects.requireNonNull(cs);
}
public @NotNull SpectrumTable generate() {
var scale = new double[resolution];
for (int i = 0; i < scale.length; i++) {
var t = (double) i / (resolution - 1);
scale[i] = smoothstep(smoothstep(t));
}
var table = new double[3 * resolution * resolution * resolution * 3];
for (int l0 = 0; l0 < 3; l0++) {
var l = l0;
IntStream.range(0, resolution).parallel().forEach(i -> {
System.out.println("l = " + l + ", i = " + i);
var x = (double) i / (resolution - 1);
for (int j = 0; j < resolution; j++) {
var y = (double) j / (resolution - 1);
var start = resolution / 5;
var c = new double[3];
for (int k = start; k < resolution; k++) {
var z = scale[k];
var idx = ((((l * resolution + k) * resolution) + j) * resolution + i) * 3;
var color = getColor(l, x, y, z);
generate(color, c, table, idx);
}
Arrays.fill(c, 0);
for (int k = start; k >= 0; --k) {
var z = scale[k];
var idx = ((((l * resolution + k) * resolution) + j) * resolution + i) * 3;
var color = getColor(l, x, y, z);
generate(color, c, table, idx);
}
}
});
}
return new SpectrumTable(resolution, scale, table);
}
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];
out[offset] = A * c1 * c1;
out[offset + 1] = B * c1 - 2 * A * c0 * c1 * c1;
out[offset + 2] = C - B * c0 * c1 + A * c0 * c0 * c1 * c1;
}
/**
* Use Gauss-Newton algorithm to calculate coefficients {@code c} of a {@link SigmoidPolynomial} such that the round
* trip error from converting the {@code rgb} value to a spectrum and back is minimized.
* @param rgb the input color
* @param c the coefficients, used as initial values and output
* @param it the number of iterations
*/
private void gaussNewton(@NotNull ColorRGB rgb, double @NotNull[] c, int it) {
var bestQuality = Double.POSITIVE_INFINITY;
var bestCoefficients = new double[3];
for (int i = 0; i < it; ++i) {
var polynomial = new SigmoidPolynomial(c[0], c[1], c[2]);
var residual = getResidual(rgb, polynomial);
var jacobian = getJacobian(rgb, polynomial);
var delta = jacobian.decompose(1e-15).solve(residual);
for (int j = 0; j < 3; ++j) {
c[j] -= delta.get(j);
}
// catch runaway
double max = Math.max(Math.max(c[0], c[1]), c[2]);
if (max > 200) {
for (int j = 0; j < 3; ++j) {
c[j] *= 200 / max;
}
}
var quality = residual.squared();
if (quality <= 1e-6) {
return;
} else if (quality < bestQuality) {
bestQuality = quality;
System.arraycopy(c, 0, bestCoefficients, 0, 3);
}
}
System.arraycopy(bestCoefficients, 0, c, 0, 3);
}
/**
* Calculates the Jacobian matrix of the {@code 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
var factors = new double[] { -1d/60, 3d/20, -3d/4, 0, 3d/4, -3d/20, 1d/60 };
for (int i = 0; i < 3; i++) {
var derivative = Vec3.ZERO;
for (int d = - factors.length / 2, j = 0; j < factors.length; d++, j++) {
if (factors[j] == 0) continue;
var tmp = switch (i) {
case 0 -> new SigmoidPolynomial(polynomial.c0() + d * EPSILON, polynomial.c1(), polynomial.c2());
case 1 -> new SigmoidPolynomial(polynomial.c0(), polynomial.c1() + d * EPSILON, polynomial.c2());
case 2 -> new SigmoidPolynomial(polynomial.c0(), polynomial.c1(), polynomial.c2() + d * EPSILON);
default -> throw new AssertionError();
};
var r = getResidual(rgb, tmp);
derivative = Vec3.fma(factors[j], r, derivative);
}
for (int j = 0; j < 3; j++) {
jac[j][i] = derivative.get(j) / EPSILON;
}
}
return new Matrix3(
jac[0][0], jac[0][1], jac[0][2],
jac[1][0], jac[1][1], jac[1][2],
jac[2][0], jac[2][1], jac[2][2]
);
}
/**
* Calculates the difference between the RGB color and the result of converting the RGB color to a spectrum using
* 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 ColorRGB rgb, @NotNull SigmoidPolynomial polynomial) {
var out = new SigmoidPolynomialSpectrum(polynomial, cs).toXYZ();
return cs.toCIELab(rgb).minus(cs.toCIELab(out));
}
private static double smoothstep(double x) {
return x * x * (3.0 - 2.0 * x);
}
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 ColorRGB(rgb[0], rgb[1], rgb[2]);
}
private record SigmoidPolynomialSpectrum(@NotNull SigmoidPolynomial polynomial, @NotNull ColorSpace cs) implements Spectrum {
@Override
public double max() {
return polynomial.max();
}
@Override
public double get(double lambda) {
var l = (lambda - Spectrum.LAMBDA_MIN) / (Spectrum.LAMBDA_MAX - Spectrum.LAMBDA_MIN);
return polynomial.get(l) * cs.illuminant().get(lambda);
}
}
}

@ -0,0 +1,8 @@
package eu.jonahbauer.raytracing.render.color;
import org.jetbrains.annotations.NotNull;
public interface TransferFunction {
@NotNull ColorRGB decode(@NotNull ColorRGB rgb);
@NotNull ColorRGB encode(@NotNull ColorRGB rgb);
}

@ -0,0 +1,50 @@
package eu.jonahbauer.raytracing.render.color;
import org.jetbrains.annotations.NotNull;
public final class TransferFunctions {
public static final @NotNull TransferFunction sRGB = new ComponentTransferFunction() {
@Override
protected double encode(double value) {
if (value <= 0.0031308) return 12.92 * value;
return 1.055 * Math.pow(value, 1. / 2.4) - 0.055;
}
@Override
protected double decode(double value) {
if (value <= 0.04045) return value / 12.92;
return Math.pow((value + 0.055) / 1.055, 2.4d);
}
};
public static final @NotNull TransferFunction LINEAR = new TransferFunction() {
@Override
public @NotNull ColorRGB encode(@NotNull ColorRGB rgb) {
return rgb;
}
@Override
public @NotNull ColorRGB decode(@NotNull ColorRGB rgb) {
return rgb;
}
};
private TransferFunctions() {
throw new UnsupportedOperationException();
}
private abstract static class ComponentTransferFunction implements TransferFunction {
@Override
public final @NotNull ColorRGB decode(@NotNull ColorRGB rgb) {
return new ColorRGB(decode(rgb.r()), decode(rgb.g()), decode(rgb.b()));
}
@Override
public final @NotNull ColorRGB encode(@NotNull ColorRGB rgb) {
return new ColorRGB(encode(rgb.r()), encode(rgb.g()), encode(rgb.b()));
}
protected abstract double encode(double value);
protected abstract double decode(double value);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

@ -0,0 +1,19 @@
package eu.jonahbauer.raytracing.render.image;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public interface ImageWriter {
default void write(@NotNull Canvas image, @NotNull Path path) throws IOException {
try (var out = Files.newOutputStream(path)) {
write(image, out);
}
}
void write(@NotNull Canvas canvas, @NotNull OutputStream out) throws IOException;
}

@ -0,0 +1,107 @@
package eu.jonahbauer.raytracing.render.image;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.util.Objects;
import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.DeflaterOutputStream;
public class PNGImageWriter implements ImageWriter {
public static final @NotNull PNGImageWriter sRGB = new PNGImageWriter(ColorSpaces.sRGB);
private static final byte[] MAGIC = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private static final int IHDR_LENGTH = 13;
private static final int IHDR_TYPE = 0x49484452;
private static final int IDAT_TYPE = 0x49444154;
private static final int IEND_TYPE = 0x49454E44;
private static final int IEND_CRC = 0xAE426082;
private final @NotNull ColorSpace cs;
public PNGImageWriter(@NotNull ColorSpace cs) {
this.cs = Objects.requireNonNull(cs, "cs");
}
@Override
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
try (var data = new NoCloseDataOutputStream(out); var _ = data.closeable()) {
data.write(MAGIC);
writeIHDR(image, data);
writeIDAT(image, data);
writeIEND(image, data);
}
}
private void writeIHDR(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
data.writeInt(IHDR_LENGTH);
try (
var crc = new CheckedOutputStream(data, new CRC32());
var ihdr = new DataOutputStream(crc)
) {
ihdr.writeInt(IHDR_TYPE);
ihdr.writeInt(image.getWidth()); // image width
ihdr.writeInt(image.getHeight()); // image height
ihdr.writeByte(8); // bit depth
ihdr.writeByte(2); // color type
ihdr.writeByte(0); // compression method
ihdr.writeByte(0); // filter method
ihdr.writeByte(0); // interlace method
ihdr.flush();
data.writeInt((int) crc.getChecksum().getValue());
}
}
private void writeIDAT(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
try (
var baos = new ByteArrayOutputStream();
var crc = new CheckedOutputStream(baos, new CRC32());
var idat = new DataOutputStream(crc)
) {
idat.writeInt(IDAT_TYPE);
try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) {
for (int y = 0; y < image.getHeight(); y++) {
deflate.writeByte(0); // filter type
for (int x = 0; x < image.getWidth(); x++) {
var pixel = cs.encode(image.getRGB(x, y, cs));
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
data.write(bytes);
data.writeInt((int) crc.getChecksum().getValue());
}
}
private void writeIEND(@NotNull Canvas image, @NotNull DataOutputStream data) throws IOException {
data.writeInt(0);
data.writeInt(IEND_TYPE);
data.writeInt(IEND_CRC);
}
private static class NoCloseDataOutputStream extends DataOutputStream {
public NoCloseDataOutputStream(OutputStream out) {
super(out);
}
@Override
public void close() {
// do nothing
}
public Closeable closeable() {
return super::close;
}
}
}

@ -0,0 +1,46 @@
package eu.jonahbauer.raytracing.render.image;
import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class PPMImageWriter implements ImageWriter {
public static final PPMImageWriter sRGB = new PPMImageWriter(ColorSpaces.sRGB);
private final @NotNull ColorSpace cs;
public PPMImageWriter(@NotNull ColorSpace cs) {
this.cs = Objects.requireNonNull(cs, "cs");
}
@Override
public void write(@NotNull Canvas image, @NotNull OutputStream out) throws IOException {
try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) {
writer.write("P3\n");
writer.write(String.valueOf(image.getWidth()));
writer.write(" ");
writer.write(String.valueOf(image.getHeight()));
writer.write("\n255\n");
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
var color = cs.encode(image.getRGB(x, y, cs));
writer.write(String.valueOf(color.red()));
writer.write(" ");
writer.write(String.valueOf(color.green()));
writer.write(" ");
writer.write(String.valueOf(color.blue()));
writer.write("\n");
}
}
}
}
}

@ -2,7 +2,7 @@ package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -11,34 +11,75 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; import java.util.random.RandomGenerator;
public record DielectricMaterial(double refractionIndex, @NotNull Texture texture) implements Material { public record DielectricMaterial(@NotNull RefractiveIndex ri, @NotNull Texture texture) implements Material {
public DielectricMaterial(double refractionIndex) {
this(refractionIndex, Color.WHITE);
}
public DielectricMaterial { public DielectricMaterial {
Objects.requireNonNull(ri, "ri");
Objects.requireNonNull(texture, "texture"); Objects.requireNonNull(texture, "texture");
} }
public DielectricMaterial(@NotNull RefractiveIndex ri) {
this(ri, Spectra.WHITE);
}
public DielectricMaterial(double ri) {
this(new ConstantRefractiveIndex(ri), Spectra.WHITE);
}
public DielectricMaterial(double ri, @NotNull Texture texture) {
this(new ConstantRefractiveIndex(ri), texture);
}
@Override @Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
var ri = hit.isFrontFace() ? (1 / refractionIndex) : refractionIndex; var ri = switch (this.ri) {
case ConstantRefractiveIndex(var x) -> x;
case RefractiveIndex x -> x.get(ray.lambda().collapse());
};
if (hit.isFrontFace()) ri = 1 / ri;
var cosTheta = Math.min(- ray.direction().unit().times(hit.normal()), 1.0); var cosTheta = Math.min(- ray.direction().unit().dot(hit.normal()), 1.0);
var reflectance = reflectance(cosTheta); var reflectance = reflectance(cosTheta, ri);
var reflect = reflectance > random.nextDouble(); var reflect = reflectance > random.nextDouble();
var newDirection = (reflect ? Optional.<Vec3>empty() : Vec3.refract(ray.direction(), hit.normal(), ri)) var newDirection = (reflect ? Optional.<Vec3>empty() : Vec3.refract(ray.direction(), hit.normal(), ri))
.orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal())); .orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal()));
var attenuation = texture.get(hit); var attenuation = texture.get(hit);
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection))); return Optional.of(new SpecularScatterResult(attenuation, ray.with(hit, newDirection)));
} }
private double reflectance(double cos) { private double reflectance(double cos, double ri) {
// use schlick's approximation for reflectance // use schlick's approximation for reflectance
var r0 = (1 - refractionIndex) / (1 + refractionIndex); var r0 = (1 - ri) / (1 + ri);
r0 = r0 * r0; r0 = r0 * r0;
return r0 + (1 - r0) * (1 - cos) * (1 - cos) * (1 - cos) * (1 - cos) * (1 - cos); return r0 + (1 - r0) * (1 - cos) * (1 - cos) * (1 - cos) * (1 - cos) * (1 - cos);
} }
@FunctionalInterface
public interface RefractiveIndex {
double get(double lambda);
}
public record ConstantRefractiveIndex(double ri) implements RefractiveIndex {
@Override
public double get(double lambda) {
return ri;
}
}
public record SellmeierRefractiveIndex(
double B1, double B2, double B3,
double C1, double C2, double C3
) implements RefractiveIndex {
@Override
public double get(double lambda) {
var l2 = lambda * lambda * 1E-6; // square and convert to µm
var x = 1 + B1 * l2 / (l2 - C1)
+ B2 * l2 / (l2 - C2)
+ B3 * l2 / (l2 - C3);
return Math.sqrt(x);
}
}
} }

@ -1,22 +1,27 @@
package eu.jonahbauer.raytracing.render.material; package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; import java.util.random.RandomGenerator;
public record DiffuseLight(@NotNull Texture texture) implements Material { public record DiffuseLight(@NotNull Texture texture) implements Material {
public DiffuseLight {
Objects.requireNonNull(texture, "texture");
}
@Override @Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
return Optional.empty(); return Optional.empty();
} }
@Override @Override
public @NotNull Color emitted(@NotNull HitResult hit) { public @NotNull Spectrum emitted(@NotNull HitResult hit) {
return texture.get(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.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -40,11 +41,11 @@ public final class DirectionalMaterial implements Material {
if (back != null) return back.scatter(ray, hit, random); if (back != null) return back.scatter(ray, hit, random);
} }
// let the ray pass through without obstruction // 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, ray.with(hit, ray.direction())));
} }
@Override @Override
public @NotNull Color emitted(@NotNull HitResult hit) { public @NotNull Spectrum emitted(@NotNull HitResult hit) {
if (hit.isFrontFace()) { if (hit.isFrontFace()) {
if (front != null) return front.emitted(hit); if (front != null) return front.emitted(hit);
} else { } else {
@ -56,7 +57,7 @@ public final class DirectionalMaterial implements Material {
private record DirectionalTexture(@Nullable Texture front, @Nullable Texture back) implements Texture { private record DirectionalTexture(@Nullable Texture front, @Nullable Texture back) implements Texture {
@Override @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(); throw new UnsupportedOperationException();
} }

@ -2,15 +2,20 @@ package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.renderer.pdf.SphereProbabilityDensityFunction; import eu.jonahbauer.raytracing.render.renderer.pdf.SphereProbabilityDensityFunction;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator; 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");
}
@Override @Override
public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { public @NotNull Optional<ScatterResult> scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) {
return Optional.of(new PdfScatterResult(albedo(), new SphereProbabilityDensityFunction())); return Optional.of(new PdfScatterResult(albedo(), new SphereProbabilityDensityFunction()));

@ -2,7 +2,9 @@ package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.renderer.pdf.ProbabilityDensityFunction; import eu.jonahbauer.raytracing.render.renderer.pdf.ProbabilityDensityFunction;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.render.texture.Texture;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -30,10 +32,10 @@ public interface Material {
/** /**
* {@return the color emitted for a given hit} * {@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) { default @NotNull Spectrum emitted(@NotNull HitResult hit) {
return Color.BLACK; return Spectra.BLACK;
} }
/** /**
@ -48,7 +50,7 @@ public interface Material {
* @param attenuation the attenuation of the scattered light ray * @param attenuation the attenuation of the scattered light ray
* @param ray 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 { public SpecularScatterResult {
Objects.requireNonNull(attenuation, "attenuation"); Objects.requireNonNull(attenuation, "attenuation");
Objects.requireNonNull(ray, "ray"); Objects.requireNonNull(ray, "ray");
@ -62,7 +64,7 @@ public interface Material {
* @param attenuation the attenuation of the scattered light ray * @param attenuation the attenuation of the scattered light ray
* @param pdf the probability density function * @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 { public PdfScatterResult {
Objects.requireNonNull(attenuation, "attenuation"); Objects.requireNonNull(attenuation, "attenuation");
Objects.requireNonNull(pdf, "pdf"); Objects.requireNonNull(pdf, "pdf");

@ -0,0 +1,25 @@
package eu.jonahbauer.raytracing.render.material;
import eu.jonahbauer.raytracing.render.color.ColorSpaces;
import eu.jonahbauer.raytracing.render.material.DielectricMaterial.SellmeierRefractiveIndex;
import eu.jonahbauer.raytracing.render.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.texture.CheckerTexture;
import org.jetbrains.annotations.NotNull;
public final class Materials {
public static final @NotNull Material GLASS = new DielectricMaterial(new SellmeierRefractiveIndex(
1.0361212, 0.231792344, 1.01046945,
6.00069867E-3, 2.00179144E-2, 1.03560653E2
));
public static final @NotNull Material MIRROR = new MetallicMaterial(ColorSpaces.sRGB.albedo(0.7, 0.7, 0.7));
public static final @NotNull Material DEBUG = new DirectionalMaterial(
new LambertianMaterial(new CheckerTexture(50.0, ColorSpaces.sRGB.albedo(1.0, 0.0, 1.0), Spectra.BLACK)),
new LambertianMaterial(new CheckerTexture(50.0, ColorSpaces.sRGB.albedo(1.0, 1.0, 1.0), Spectra.BLACK))
);
private Materials() {
throw new UnsupportedOperationException();
}
}

@ -28,6 +28,6 @@ public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements
newDirection = Vec3.fma(fuzz, Vec3.random(random), newDirection.unit()); newDirection = Vec3.fma(fuzz, Vec3.random(random), newDirection.unit());
} }
var attenuation = texture.get(hit); var attenuation = texture.get(hit);
return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection))); return Optional.of(new SpecularScatterResult(attenuation, ray.with(hit, newDirection)));
} }
} }

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

@ -1,13 +1,13 @@
package eu.jonahbauer.raytracing.render.renderer; package eu.jonahbauer.raytracing.render.renderer;
import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.render.renderer.pdf.TargetingProbabilityDensityFunction; import eu.jonahbauer.raytracing.render.renderer.pdf.TargetingProbabilityDensityFunction;
import eu.jonahbauer.raytracing.render.renderer.pdf.MixtureProbabilityDensityFunction; import eu.jonahbauer.raytracing.render.renderer.pdf.MixtureProbabilityDensityFunction;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.SampledSpectrum;
import eu.jonahbauer.raytracing.render.camera.Camera; import eu.jonahbauer.raytracing.render.camera.Camera;
import eu.jonahbauer.raytracing.render.canvas.Canvas; import eu.jonahbauer.raytracing.render.canvas.Canvas;
import eu.jonahbauer.raytracing.render.spectrum.SampledWavelengths;
import eu.jonahbauer.raytracing.scene.Scene; import eu.jonahbauer.raytracing.scene.Scene;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -21,7 +21,10 @@ import static eu.jonahbauer.raytracing.Main.DEBUG;
public final class SimpleRenderer implements Renderer { public final class SimpleRenderer implements Renderer {
private final int sqrtSamplesPerPixel; private final int sqrtSamplesPerPixel;
private final int maxDepth; private final int maxDepth;
private final double gamma;
private final int spectralSamples;
private final SampledSpectrum black;
private final SampledSpectrum white;
private final boolean parallel; private final boolean parallel;
private final boolean iterative; private final boolean iterative;
@ -37,7 +40,10 @@ public final class SimpleRenderer implements Renderer {
private SimpleRenderer(@NotNull Builder builder) { private SimpleRenderer(@NotNull Builder builder) {
this.sqrtSamplesPerPixel = (int) Math.sqrt(builder.samplesPerPixel); this.sqrtSamplesPerPixel = (int) Math.sqrt(builder.samplesPerPixel);
this.maxDepth = builder.maxDepth; this.maxDepth = builder.maxDepth;
this.gamma = builder.gamma;
this.spectralSamples = builder.spectralSamples;
this.black = new SampledSpectrum(spectralSamples, 0);
this.white = new SampledSpectrum(spectralSamples, 1);
this.parallel = builder.parallel; this.parallel = builder.parallel;
this.iterative = builder.iterative; this.iterative = builder.iterative;
@ -77,18 +83,11 @@ public final class SimpleRenderer implements Renderer {
for (int x = 0; x < camera.getWidth(); x++) { for (int x = 0; x < camera.getWidth(); x++) {
var ray = camera.cast(x, y, sif, sjf, sqrtSamplesPerPixel, random); var ray = camera.cast(x, y, sif, sjf, sqrtSamplesPerPixel, random);
var c = getColor(scene, ray, 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));
}
});
} }
/** /**
@ -101,19 +100,18 @@ public final class SimpleRenderer implements Renderer {
getScanlineStream(camera.getHeight(), parallel).forEach(y -> { getScanlineStream(camera.getHeight(), parallel).forEach(y -> {
var random = splittable.split(); var random = splittable.split();
for (int x = 0; x < camera.getWidth(); x++) { for (int x = 0; x < camera.getWidth(); x++) {
var color = Color.BLACK;
int i = 0; int i = 0;
for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) { for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) {
for (int si = 0; si < sqrtSamplesPerPixel; si++) { for (int si = 0; si < sqrtSamplesPerPixel; si++) {
var ray = camera.cast(x, y, si, sj, sqrtSamplesPerPixel, random); var lambda = SampledWavelengths.uniform(random.nextDouble(), spectralSamples);
var ray = camera.cast(x, y, si, sj, sqrtSamplesPerPixel, random).with(lambda);
if (DEBUG) { if (DEBUG) {
System.out.println("Casting ray " + ray + " through pixel (" + x + "," + y + ") at subpixel (" + si + "," + sj + ")..."); System.out.println("Casting ray " + ray + " through pixel (" + x + "," + y + ") at subpixel (" + si + "," + sj + ")...");
} }
var c = getColor(scene, ray, random); 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));
} }
}); });
} }
@ -121,19 +119,19 @@ public final class SimpleRenderer implements Renderer {
/** /**
* {@return the color of the given ray in the given scene} * {@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); return getColor0(scene, ray, maxDepth, random);
} }
private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) { private @NotNull SampledSpectrum getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) {
var color = Color.BLACK; var color = black;
var attenuation = Color.WHITE; var attenuation = white;
while (depth-- > 0) { while (depth-- > 0) {
var optional = scene.hit(ray); var optional = scene.hit(ray, random);
if (optional.isEmpty()) { if (optional.isEmpty()) {
var background = scene.getBackgroundColor(ray); var background = scene.getBackgroundColor(ray);
color = Color.add(color, Color.multiply(attenuation, background)); color = SampledSpectrum.fma(attenuation, background, color);
if (DEBUG) { if (DEBUG) {
System.out.println(" Hit background: " + background); System.out.println(" Hit background: " + background);
} }
@ -145,13 +143,13 @@ public final class SimpleRenderer implements Renderer {
System.out.println(" Hit " + hit.target() + " at t=" + hit.t() + " (" + hit.position() + ")"); System.out.println(" Hit " + hit.target() + " at t=" + hit.t() + " (" + hit.position() + ")");
} }
var material = hit.material(); var material = hit.material();
var emitted = material.emitted(hit); var emitted = material.emitted(hit).sample(ray.lambda());
if (DEBUG && !Color.BLACK.equals(emitted)) { if (DEBUG && !black.equals(emitted)) {
System.out.println(" Emitted: " + emitted); System.out.println(" Emitted: " + emitted);
} }
var result = material.scatter(ray, hit, random); var result = material.scatter(ray, hit, random);
color = Color.add(color, Color.multiply(attenuation, emitted)); color = SampledSpectrum.fma(attenuation, emitted, color);
if (result.isEmpty()) { if (result.isEmpty()) {
if (DEBUG) { if (DEBUG) {
@ -162,7 +160,7 @@ public final class SimpleRenderer implements Renderer {
switch (result.get()) { switch (result.get()) {
case Material.SpecularScatterResult(var a, var scattered) -> { case Material.SpecularScatterResult(var a, var scattered) -> {
attenuation = Color.multiply(attenuation, a); attenuation = attenuation.times(a.sample(ray.lambda()));
ray = scattered; ray = scattered;
if (DEBUG) { if (DEBUG) {
@ -171,8 +169,8 @@ public final class SimpleRenderer implements Renderer {
} }
case Material.PdfScatterResult(var a, var pdf) -> { case Material.PdfScatterResult(var a, var pdf) -> {
if (scene.getTargets() == null) { if (scene.getTargets() == null) {
attenuation = Color.multiply(attenuation, a); attenuation = attenuation.times(a.sample(ray.lambda()));
ray = new Ray(hit.position(), pdf.generate(random)); ray = new Ray(hit.position(), pdf.generate(random), ray.lambda());
if (DEBUG) { if (DEBUG) {
System.out.println(" Pdf scattering with albedo " + a); System.out.println(" Pdf scattering with albedo " + a);
@ -187,8 +185,8 @@ public final class SimpleRenderer implements Renderer {
var factor = idealPdf / actualPdf; var factor = idealPdf / actualPdf;
attenuation = Color.multiply(attenuation, Color.multiply(a, factor)); attenuation = attenuation.times(a.sample(ray.lambda()).times(factor));
ray = new Ray(hit.position(), direction); ray = new Ray(hit.position(), direction, ray.lambda());
if (DEBUG) { if (DEBUG) {
System.out.println(" Pdf scattering with albedo " + a + " and factor " + factor); System.out.println(" Pdf scattering with albedo " + a + " and factor " + factor);
@ -216,14 +214,14 @@ public final class SimpleRenderer implements Renderer {
* are encoded in the longs lower and upper 32 bits respectively. * are encoded in the longs lower and upper 32 bits respectively.
*/ */
private static @NotNull IntStream getScanlineStream(int height, boolean parallel) { 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; return parallel ? stream.parallel() : stream;
} }
public static class Builder { public static class Builder {
private int samplesPerPixel = 100; private int samplesPerPixel = 100;
private int maxDepth = 10; private int maxDepth = 10;
private double gamma = 2.0; private int spectralSamples = 4;
private boolean parallel = true; private boolean parallel = true;
private boolean iterative = false; private boolean iterative = false;
@ -239,9 +237,9 @@ public final class SimpleRenderer implements Renderer {
return this; return this;
} }
public @NotNull Builder withGamma(double gamma) { public @NotNull Builder withSpectralSamples(int samples) {
if (gamma <= 0 || !Double.isFinite(gamma)) throw new IllegalArgumentException("gamma must be positive"); if (samples <= 0) throw new IllegalArgumentException("samples must be positive");
this.gamma = gamma; this.spectralSamples = samples;
return this; return this;
} }

@ -15,7 +15,7 @@ public record CosineProbabilityDensityFunction(@NotNull Vec3 normal) implements
@Override @Override
public double value(@NotNull Vec3 direction) { public double value(@NotNull Vec3 direction) {
var cos = normal.times(direction); var cos = normal.dot(direction);
return Math.max(0, cos / Math.PI); return Math.max(0, cos / Math.PI);
} }

@ -0,0 +1,45 @@
package eu.jonahbauer.raytracing.render.spectrum;
public final class BlackbodySpectrum implements Spectrum {
/**
* the speed of light in m/s
*/
private static final double c = 299792458d;
/**
* the planck constant in m^2*kg/s
*/
private static final double h = 6.62607015E-34;
/**
* the boltzmann constant in m^2*kg/s^2/K
*/
private static final double k = 1.380649E-23;
/**
* wien's displacement constant in m*K
*/
private static final double b = 2.897771995E-3;
private final double T;
private final double factor;
public BlackbodySpectrum(double T) {
if (T < 0) throw new IllegalArgumentException("T must be non-negative");
this.T = T;
this.factor = 1 / get(b / T);
}
@Override
public double max() {
return 1;
}
@Override
public double get(double lambda) {
lambda *= 1E-9;
var l2 = lambda * lambda;
var x = h * c / (lambda * k * T);
return 2 * h * c * c / (l2 * l2 * lambda) / (Math.exp(x) - 1) * factor;
}
}

@ -0,0 +1,17 @@
package eu.jonahbauer.raytracing.render.spectrum;
/**
* A constant spectrum.
* @param c the constant value
*/
public record ConstantSpectrum(double c) implements Spectrum {
@Override
public double max() {
return c;
}
@Override
public double get(double lambda) {
return c;
}
}

@ -0,0 +1,51 @@
package eu.jonahbauer.raytracing.render.spectrum;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
/**
* A spectrum sampled in one nanometer intervals.
*/
public final class DenselySampledSpectrum implements Spectrum {
private final double[] samples;
private final int min;
private final double max;
public DenselySampledSpectrum(@NotNull Spectrum spectrum) {
this(spectrum, LAMBDA_MIN, LAMBDA_MAX);
}
public DenselySampledSpectrum(@NotNull Spectrum spectrum, int min, int max) {
if (max - min + 1 <= 0) throw new IllegalArgumentException("samples must not be empty");
this.samples = new double[max - min + 1];
var maxValue = 0d;
for (int lambda = min, i = 0; lambda <= max; lambda++, i++) {
var sample = spectrum.get(lambda);
if (sample > maxValue) maxValue = sample;
this.samples[i] = sample;
}
this.min = min;
this.max = maxValue;
}
public DenselySampledSpectrum(double @NotNull[] samples, int lambdaMin) {
if (samples.length == 0) throw new IllegalArgumentException("samples must not be empty");
this.samples = Arrays.copyOf(samples, samples.length);
this.min = lambdaMin;
this.max = Arrays.stream(this.samples).max().orElseThrow();
}
@Override
public double max() {
return max;
}
@Override
public double get(double lambda) {
int offset = (int) Math.round(lambda) - min;
if (offset < 0 || offset >= samples.length) return 0;
return samples[offset];
}
}

@ -0,0 +1,48 @@
package eu.jonahbauer.raytracing.render.spectrum;
import java.util.Arrays;
public final class PiecewiseLinearSpectrum implements Spectrum {
private final double[] lambdas;
private final double[] values;
private final double max;
public PiecewiseLinearSpectrum(double[] lambdas, double[] values) {
if (lambdas.length != values.length) {
throw new IllegalArgumentException("lambdas and values must have the same length");
}
this.lambdas = Arrays.copyOf(lambdas, lambdas.length);
this.values = Arrays.copyOf(values, values.length);
var max = 0d;
for (int i = 1; i < this.lambdas.length; i++) {
if (this.lambdas[i] <= this.lambdas[i - 1]) {
throw new IllegalArgumentException("lambdas must be in increasing order");
}
if (this.values[i] < 0) {
throw new IllegalArgumentException("values must be non-negative");
} else if (this.values[i] > max) {
max = this.values[i];
}
}
this.max = max;
}
@Override
public double max() {
return max;
}
@Override
public double get(double lambda) {
if (lambdas.length == 0 || lambda < lambdas[0] || lambda > lambdas[lambdas.length - 1]) return 0;
if (lambda == lambdas[lambdas.length - 1]) return values[values.length - 1];
var i = Arrays.binarySearch(lambdas, lambda);
if (i < 0) i = -i - 1;
var t = (lambda - lambdas[i]) / (lambdas[i + 1] - lambdas[i]);
return (1 - t) * values[i] + t * values[i + 1];
}
}

@ -0,0 +1,27 @@
package eu.jonahbauer.raytracing.render.spectrum;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.color.SigmoidPolynomial;
import eu.jonahbauer.raytracing.render.color.ColorRGB;
import org.jetbrains.annotations.NotNull;
public final class RGBAlbedoSpectrum implements Spectrum {
private final @NotNull SigmoidPolynomial polynomial;
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();
}
this.polynomial = cs.toPolynomial(rgb);
}
@Override
public double max() {
return polynomial.max();
}
@Override
public double get(double lambda) {
return polynomial.get(lambda);
}
}

@ -0,0 +1,36 @@
package eu.jonahbauer.raytracing.render.spectrum;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.color.SigmoidPolynomial;
import eu.jonahbauer.raytracing.render.color.ColorRGB;
import org.jetbrains.annotations.NotNull;
/**
* A spectrum based on an RGB color used as an illuminant. The spectrum is adjusted to account for the color space's
* standard illuminant.
*/
public final class RGBIlluminantSpectrum implements Spectrum {
private final double scale;
private final @NotNull SigmoidPolynomial polynomial;
private final @NotNull Spectrum illuminant;
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.toPolynomial(scale != 0 ? rgb.div(scale) : ColorRGB.BLACK);
this.illuminant = cs.illuminant();
}
@Override
public double max() {
return scale * polynomial.max() * illuminant.max();
}
@Override
public double get(double lambda) {
return scale * polynomial.get(lambda) * illuminant.get(lambda);
}
}

@ -0,0 +1,30 @@
package eu.jonahbauer.raytracing.render.spectrum;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.color.SigmoidPolynomial;
import eu.jonahbauer.raytracing.render.color.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 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.toPolynomial(scale != 0 ? rgb.div(scale) : ColorRGB.BLACK);
}
@Override
public double max() {
return scale * polynomial.max();
}
@Override
public double get(double lambda) {
return scale * polynomial.get(lambda);
}
}

@ -0,0 +1,139 @@
package eu.jonahbauer.raytracing.render.spectrum;
import eu.jonahbauer.raytracing.math.IVec;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.color.ColorXYZ;
import eu.jonahbauer.raytracing.render.color.ColorRGB;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
// TODO use Vector API to parallelize operations
public final class SampledSpectrum implements IVec<SampledSpectrum> {
private final double @NotNull[] values;
public SampledSpectrum(@NotNull SampledWavelengths lambdas, @NotNull Spectrum spectrum) {
var values = new double[lambdas.size()];
for (int i = 0; i < values.length; i++) {
values[i] = spectrum.get(lambdas.get(i));
}
this.values = values;
}
public SampledSpectrum(int count, double value) {
var values = new double[count];
Arrays.fill(values, value);
this.values = values;
}
private SampledSpectrum(double @NotNull[] values) {
this.values = values;
}
/*
* Math
*/
public static @NotNull SampledSpectrum fma(@NotNull SampledSpectrum a, @NotNull SampledSpectrum b, @NotNull SampledSpectrum c) {
var out = new double[a.values.length];
for (int i = 0; i < a.values.length; i++) {
out[i] = Math.fma(a.values[i], b.values[i], c.values[i]);
}
return new SampledSpectrum(out);
}
public static @NotNull SampledSpectrum lerp(@NotNull SampledSpectrum a, @NotNull SampledSpectrum b, double t) {
if (t < 0) return a;
if (t > 1) return b;
var out = new double[a.values.length];
for (int i = 0; i < a.values.length; i++) {
out[i] = Math.fma(t, b.values[i] - a.values[i], a.values[i]);
}
return new SampledSpectrum(out);
}
@Override
public @NotNull SampledSpectrum plus(@NotNull SampledSpectrum other) {
var out = new double[other.values.length];
for (int i = 0; i < other.values.length; i++) {
out[i] = values[i] + other.values[i];
}
return new SampledSpectrum(out);
}
@Override
public @NotNull SampledSpectrum minus(@NotNull SampledSpectrum other) {
var out = new double[other.values.length];
for (int i = 0; i < other.values.length; i++) {
out[i] = values[i] - other.values[i];
}
return new SampledSpectrum(out);
}
@Override
public @NotNull SampledSpectrum times(@NotNull SampledSpectrum other) {
var out = new double[other.values.length];
for (int i = 0; i < other.values.length; i++) {
out[i] = values[i] * other.values[i];
}
return new SampledSpectrum(out);
}
@Override
public @NotNull SampledSpectrum times(double d) {
var out = new double[values.length];
for (int i = 0; i < values.length; i++) {
out[i] = values[i] * d;
}
return new SampledSpectrum(out);
}
@Override
public double @NotNull [] toArray() {
return Arrays.copyOf(values, values.length);
}
/*
* Accessors
*/
@Override
public double get(int index) {
return values[index];
}
public int size() {
return values.length;
}
/*
* Object
*/
@Override
public boolean equals(Object obj) {
return obj instanceof SampledSpectrum o && Arrays.equals(values, o.values);
}
@Override
public int hashCode() {
return Arrays.hashCode(values);
}
@Override
public @NotNull String toString() {
return "SampledSpectrum[values=" + Arrays.toString(values) + "]";
}
/*
* Conversions
*/
public @NotNull ColorXYZ toXYZ(@NotNull SampledWavelengths lambdas) {
return lambdas.toXYZ(this);
}
public @NotNull ColorRGB toRGB(@NotNull SampledWavelengths lambdas, @NotNull ColorSpace cs) {
return cs.toRGB(toXYZ(lambdas));
}
}

@ -0,0 +1,120 @@
package eu.jonahbauer.raytracing.render.spectrum;
import eu.jonahbauer.raytracing.render.color.ColorXYZ;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
/**
* A set of sampled wavelength that can be tracked together.
*/
public final class SampledWavelengths {
public static final SampledWavelengths EMPTY = new SampledWavelengths(new double[0], new double[0]);
private final double @NotNull[] lambdas;
private final double @NotNull[] pdf;
public static @NotNull SampledWavelengths uniform(double rng, int count) {
return uniform(rng, count, Spectrum.LAMBDA_MIN, Spectrum.LAMBDA_MAX);
}
public static @NotNull SampledWavelengths uniform(double rng, int count, double min, double max) {
var lambdas = new double[count];
// choose first sample at random
lambdas[0] = (1 - rng) * min + rng * max;
// choose next samples in equal intervals, wrapping if necessary
var delta = (max - min) / count;
for (int i = 1; i < count; i++) {
lambdas[i] = lambdas[i - 1] + delta;
if (lambdas[i] > max) {
lambdas[i] = min + (lambdas[i] - max);
}
}
var pdf = new double[count];
Arrays.fill(pdf, 1 / (max - min));
return new SampledWavelengths(lambdas, pdf);
}
private SampledWavelengths(double @NotNull[] lambdas, double @NotNull[] pdf) {
this.lambdas = lambdas;
this.pdf = pdf;
}
@Contract(pure = true)
public double get(int index) {
return lambdas[index];
}
@Contract(pure = true)
public int size() {
return lambdas.length;
}
/**
* Terminates the secondary wavelengths. This method should be called after a wavelength-dependent operation like
* refraction that makes it incorrect to track multiple wavelengths together.
*/
@Contract(mutates = "this")
public double collapse() {
if (pdf.length >= 2 || pdf[1] != 0) {
Arrays.fill(pdf, 1, pdf.length, 0d);
pdf[0] /= pdf.length;
}
return lambdas[0];
}
/*
* Object
*/
@Override
public boolean equals(Object obj) {
return obj instanceof SampledWavelengths o && Arrays.equals(lambdas, o.lambdas) && Arrays.equals(pdf, o.pdf);
}
@Override
public int hashCode() {
return 31 * Arrays.hashCode(lambdas) + Arrays.hashCode(pdf);
}
@Override
public @NotNull String toString() {
return "SampledWavelengths[lambdas=" + Arrays.toString(lambdas) + ", pdf=" + Arrays.toString(pdf) + "]";
}
/*
* Conversions
*/
@NotNull
ColorXYZ toXYZ(@NotNull SampledSpectrum spectrum) {
var x = Spectra.X.sample(this);
var y = Spectra.Y.sample(this);
var z = Spectra.Z.sample(this);
return new ColorXYZ(
toXYZ0(spectrum, x) / ColorXYZ.CIE_Y_INTEGRAL,
toXYZ0(spectrum, y) / ColorXYZ.CIE_Y_INTEGRAL,
toXYZ0(spectrum, z) / ColorXYZ.CIE_Y_INTEGRAL
);
}
private double toXYZ0(@NotNull SampledSpectrum spectrum, @NotNull SampledSpectrum cie) {
var avg = 0d;
for (int i = 0; i < spectrum.size(); i++) {
var pdf = this.pdf[i];
double value;
if (pdf == 0) {
value = 0;
} else {
value = spectrum.get(i) * cie.get(i) / pdf;
}
avg = Math.fma(1d / (i + 1), value - avg, avg);
}
return avg;
}
}

@ -0,0 +1,15 @@
package eu.jonahbauer.raytracing.render.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;
}
}

@ -0,0 +1,410 @@
package eu.jonahbauer.raytracing.render.spectrum;
import eu.jonahbauer.raytracing.render.color.ColorXYZ;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
public final class Spectra {
private static final String PATH_PREFIX = "/eu/jonahbauer/raytracing/spectrum/";
/**
* the CIE XYZ color matching curve for X
*/
public static final Spectrum X = new DenselySampledSpectrum(new PiecewiseLinearSpectrum(CIE_XYZ.CIE_lambda, CIE_XYZ.CIE_X));
/**
* the CIE XYZ color matching curve for Y
*/
public static final Spectrum Y = new DenselySampledSpectrum(new PiecewiseLinearSpectrum(CIE_XYZ.CIE_lambda, CIE_XYZ.CIE_Y));;
/**
* the CIE XYZ color matching curve for Z
*/
public static final Spectrum Z = new DenselySampledSpectrum(new PiecewiseLinearSpectrum(CIE_XYZ.CIE_lambda, CIE_XYZ.CIE_Z));;
/**
* the CIE standard illuminant D50
* @see <a href="https://doi.org/10.25039/CIE.DS.hjfjmt59">CIE 2022, CIE standard illuminant D65, International Commission on Illumination (CIE), Vienna, Austria, DOI: 10.25039/CIE.DS.hjfjmt59</a>
*/
public static final Spectrum D50 = read("CIE_std_illum_D50.csv", true);
/**
* the CIE standard illuminant D65
* @see <a href="https://doi.org/10.25039/CIE.DS.etgmuqt5">CIE 2022, Relative spectral power distributions of CIE standard illuminants A, D65 and D50 (wavelengths in standard air) (data table), International Commission on Illumination (CIE), Vienna, Austria, DOI:10.25039/CIE.DS.etgmuqt5</a>
*/
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>();
try (
var is = Spectra.class.getResourceAsStream(PATH_PREFIX + path);
var in = new BufferedReader(new InputStreamReader(is, StandardCharsets.US_ASCII))
) {
String line;
while ((line = in.readLine()) != null) {
var parts = line.split(",");
lambda.add(Double.parseDouble(parts[0]));
values.add(Double.parseDouble(parts[1]));
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
var pls = new PiecewiseLinearSpectrum(
lambda.stream().mapToDouble(Double::doubleValue).toArray(),
values.stream().mapToDouble(Double::doubleValue).toArray()
);
if (normalize) {
return pls.scale(ColorXYZ.CIE_Y_INTEGRAL / Util.innerProduct(pls, Spectra.Y));
} else {
return pls;
}
}
private Spectra() {
throw new UnsupportedOperationException();
}
/**
* @see <a href="https://doi.org/10.25039/CIE.DS.xvudnb9b">CIE 2018, CIE 1931 colour-matching functions , 2 degree observer (data table),
* International Commission on Illumination (CIE), Vienna, Austria,
* DOI:10.25039/CIE.DS.xvudnb9b</a>
*/
private static final class CIE_XYZ {
private static final double[] CIE_lambda = {
360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376,
377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393,
394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410,
411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427,
428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444,
445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461,
462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478,
479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495,
496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512,
513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529,
530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546,
547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563,
564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580,
581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597,
598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614,
615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631,
632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648,
649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665,
666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682,
683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699,
700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716,
717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733,
734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750,
751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767,
768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784,
785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801,
802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818,
819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830
};
private static final double[] CIE_X = {
// CIE X function values
0.0001299000, 0.0001458470, 0.0001638021, 0.0001840037, 0.0002066902,
0.0002321000, 0.0002607280, 0.0002930750, 0.0003293880, 0.0003699140,
0.0004149000, 0.0004641587, 0.0005189860, 0.0005818540, 0.0006552347,
0.0007416000, 0.0008450296, 0.0009645268, 0.001094949, 0.001231154,
0.001368000, 0.001502050, 0.001642328, 0.001802382, 0.001995757,
0.002236000, 0.002535385, 0.002892603, 0.003300829, 0.003753236,
0.004243000, 0.004762389, 0.005330048, 0.005978712, 0.006741117,
0.007650000, 0.008751373, 0.01002888, 0.01142170, 0.01286901,
0.01431000, 0.01570443, 0.01714744, 0.01878122, 0.02074801,
0.02319000, 0.02620736, 0.02978248, 0.03388092, 0.03846824,
0.04351000, 0.04899560, 0.05502260, 0.06171880, 0.06921200,
0.07763000, 0.08695811, 0.09717672, 0.1084063, 0.1207672,
0.1343800, 0.1493582, 0.1653957, 0.1819831, 0.1986110,
0.2147700, 0.2301868, 0.2448797, 0.2587773, 0.2718079,
0.2839000, 0.2949438, 0.3048965, 0.3137873, 0.3216454,
0.3285000, 0.3343513, 0.3392101, 0.3431213, 0.3461296,
0.3482800, 0.3495999, 0.3501474, 0.3500130, 0.3492870,
0.3480600, 0.3463733, 0.3442624, 0.3418088, 0.3390941,
0.3362000, 0.3331977, 0.3300411, 0.3266357, 0.3228868,
0.3187000, 0.3140251, 0.3088840, 0.3032904, 0.2972579,
0.2908000, 0.2839701, 0.2767214, 0.2689178, 0.2604227,
0.2511000, 0.2408475, 0.2298512, 0.2184072, 0.2068115,
0.1953600, 0.1842136, 0.1733273, 0.1626881, 0.1522833,
0.1421000, 0.1321786, 0.1225696, 0.1132752, 0.1042979,
0.09564000, 0.08729955, 0.07930804, 0.07171776, 0.06458099,
0.05795001, 0.05186211, 0.04628152, 0.04115088, 0.03641283,
0.03201000, 0.02791720, 0.02414440, 0.02068700, 0.01754040,
0.01470000, 0.01216179, 0.009919960, 0.007967240, 0.006296346,
0.004900000, 0.003777173, 0.002945320, 0.002424880, 0.002236293,
0.002400000, 0.002925520, 0.003836560, 0.005174840, 0.006982080,
0.009300000, 0.01214949, 0.01553588, 0.01947752, 0.02399277,
0.02910000, 0.03481485, 0.04112016, 0.04798504, 0.05537861,
0.06327000, 0.07163501, 0.08046224, 0.08973996, 0.09945645,
0.1096000, 0.1201674, 0.1311145, 0.1423679, 0.1538542,
0.1655000, 0.1772571, 0.1891400, 0.2011694, 0.2133658,
0.2257499, 0.2383209, 0.2510668, 0.2639922, 0.2771017,
0.2904000, 0.3038912, 0.3175726, 0.3314384, 0.3454828,
0.3597000, 0.3740839, 0.3886396, 0.4033784, 0.4183115,
0.4334499, 0.4487953, 0.4643360, 0.4800640, 0.4959713,
0.5120501, 0.5282959, 0.5446916, 0.5612094, 0.5778215,
0.5945000, 0.6112209, 0.6279758, 0.6447602, 0.6615697,
0.6784000, 0.6952392, 0.7120586, 0.7288284, 0.7455188,
0.7621000, 0.7785432, 0.7948256, 0.8109264, 0.8268248,
0.8425000, 0.8579325, 0.8730816, 0.8878944, 0.9023181,
0.9163000, 0.9297995, 0.9427984, 0.9552776, 0.9672179,
0.9786000, 0.9893856, 0.9995488, 1.0090892, 1.0180064,
1.0263000, 1.0339827, 1.0409860, 1.0471880, 1.0524667,
1.0567000, 1.0597944, 1.0617992, 1.0628068, 1.0629096,
1.0622000, 1.0607352, 1.0584436, 1.0552244, 1.0509768,
1.0456000, 1.0390369, 1.0313608, 1.0226662, 1.0130477,
1.0026000, 0.9913675, 0.9793314, 0.9664916, 0.9528479,
0.9384000, 0.9231940, 0.9072440, 0.8905020, 0.8729200,
0.8544499, 0.8350840, 0.8149460, 0.7941860, 0.7729540,
0.7514000, 0.7295836, 0.7075888, 0.6856022, 0.6638104,
0.6424000, 0.6215149, 0.6011138, 0.5811052, 0.5613977,
0.5419000, 0.5225995, 0.5035464, 0.4847436, 0.4661939,
0.4479000, 0.4298613, 0.4120980, 0.3946440, 0.3775333,
0.3608000, 0.3444563, 0.3285168, 0.3130192, 0.2980011,
0.2835000, 0.2695448, 0.2561184, 0.2431896, 0.2307272,
0.2187000, 0.2070971, 0.1959232, 0.1851708, 0.1748323,
0.1649000, 0.1553667, 0.1462300, 0.1374900, 0.1291467,
0.1212000, 0.1136397, 0.1064650, 0.09969044, 0.09333061,
0.08740000, 0.08190096, 0.07680428, 0.07207712, 0.06768664,
0.06360000, 0.05980685, 0.05628216, 0.05297104, 0.04981861,
0.04677000, 0.04378405, 0.04087536, 0.03807264, 0.03540461,
0.03290000, 0.03056419, 0.02838056, 0.02634484, 0.02445275,
0.02270000, 0.02108429, 0.01959988, 0.01823732, 0.01698717,
0.01584000, 0.01479064, 0.01383132, 0.01294868, 0.01212920,
0.01135916, 0.01062935, 0.009938846, 0.009288422, 0.008678854,
0.008110916, 0.007582388, 0.007088746, 0.006627313, 0.006195408,
0.005790346, 0.005409826, 0.005052583, 0.004717512, 0.004403507,
0.004109457, 0.003833913, 0.003575748, 0.003334342, 0.003109075,
0.002899327, 0.002704348, 0.002523020, 0.002354168, 0.002196616,
0.002049190, 0.001910960, 0.001781438, 0.001660110, 0.001546459,
0.001439971, 0.001340042, 0.001246275, 0.001158471, 0.001076430,
0.0009999493, 0.0009287358, 0.0008624332, 0.0008007503, 0.0007433960,
0.0006900786, 0.0006405156, 0.0005945021, 0.0005518646, 0.0005124290,
0.0004760213, 0.0004424536, 0.0004115117, 0.0003829814, 0.0003566491,
0.0003323011, 0.0003097586, 0.0002888871, 0.0002695394, 0.0002515682,
0.0002348261, 0.0002191710, 0.0002045258, 0.0001908405, 0.0001780654,
0.0001661505, 0.0001550236, 0.0001446219, 0.0001349098, 0.0001258520,
0.0001174130, 0.0001095515, 0.0001022245, 0.00009539445, 0.00008902390,
0.00008307527, 0.00007751269, 0.00007231304, 0.00006745778, 0.00006292844,
0.00005870652, 0.00005477028, 0.00005109918, 0.00004767654, 0.00004448567,
0.00004150994, 0.00003873324, 0.00003614203, 0.00003372352, 0.00003146487,
0.00002935326, 0.00002737573, 0.00002552433, 0.00002379376, 0.00002217870,
0.00002067383, 0.00001927226, 0.00001796640, 0.00001674991, 0.00001561648,
0.00001455977, 0.00001357387, 0.00001265436, 0.00001179723, 0.00001099844,
0.00001025398, 0.000009559646, 0.000008912044, 0.000008308358, 0.000007745769,
0.000007221456, 0.000006732475, 0.000006276423, 0.000005851304, 0.000005455118,
0.000005085868, 0.000004741466, 0.000004420236, 0.000004120783, 0.000003841716,
0.000003581652, 0.000003339127, 0.000003112949, 0.000002902121, 0.000002705645,
0.000002522525, 0.000002351726, 0.000002192415, 0.000002043902, 0.000001905497,
0.000001776509, 0.000001656215, 0.000001544022, 0.000001439440, 0.000001341977,
0.000001251141
};
private static final double[] CIE_Y = {
// CIE Y function values
0.000003917000, 0.000004393581, 0.000004929604, 0.000005532136, 0.000006208245,
0.000006965000, 0.000007813219, 0.000008767336, 0.000009839844, 0.00001104323,
0.00001239000, 0.00001388641, 0.00001555728, 0.00001744296, 0.00001958375,
0.00002202000, 0.00002483965, 0.00002804126, 0.00003153104, 0.00003521521,
0.00003900000, 0.00004282640, 0.00004691460, 0.00005158960, 0.00005717640,
0.00006400000, 0.00007234421, 0.00008221224, 0.00009350816, 0.0001061361,
0.0001200000, 0.0001349840, 0.0001514920, 0.0001702080, 0.0001918160,
0.0002170000, 0.0002469067, 0.0002812400, 0.0003185200, 0.0003572667,
0.0003960000, 0.0004337147, 0.0004730240, 0.0005178760, 0.0005722187,
0.0006400000, 0.0007245600, 0.0008255000, 0.0009411600, 0.001069880,
0.001210000, 0.001362091, 0.001530752, 0.001720368, 0.001935323,
0.002180000, 0.002454800, 0.002764000, 0.003117800, 0.003526400,
0.004000000, 0.004546240, 0.005159320, 0.005829280, 0.006546160,
0.007300000, 0.008086507, 0.008908720, 0.009767680, 0.01066443,
0.01160000, 0.01257317, 0.01358272, 0.01462968, 0.01571509,
0.01684000, 0.01800736, 0.01921448, 0.02045392, 0.02171824,
0.02300000, 0.02429461, 0.02561024, 0.02695857, 0.02835125,
0.02980000, 0.03131083, 0.03288368, 0.03452112, 0.03622571,
0.03800000, 0.03984667, 0.04176800, 0.04376600, 0.04584267,
0.04800000, 0.05024368, 0.05257304, 0.05498056, 0.05745872,
0.06000000, 0.06260197, 0.06527752, 0.06804208, 0.07091109,
0.07390000, 0.07701600, 0.08026640, 0.08366680, 0.08723280,
0.09098000, 0.09491755, 0.09904584, 0.1033674, 0.1078846,
0.1126000, 0.1175320, 0.1226744, 0.1279928, 0.1334528,
0.1390200, 0.1446764, 0.1504693, 0.1564619, 0.1627177,
0.1693000, 0.1762431, 0.1835581, 0.1912735, 0.1994180,
0.2080200, 0.2171199, 0.2267345, 0.2368571, 0.2474812,
0.2586000, 0.2701849, 0.2822939, 0.2950505, 0.3085780,
0.3230000, 0.3384021, 0.3546858, 0.3716986, 0.3892875,
0.4073000, 0.4256299, 0.4443096, 0.4633944, 0.4829395,
0.5030000, 0.5235693, 0.5445120, 0.5656900, 0.5869653,
0.6082000, 0.6293456, 0.6503068, 0.6708752, 0.6908424,
0.7100000, 0.7281852, 0.7454636, 0.7619694, 0.7778368,
0.7932000, 0.8081104, 0.8224962, 0.8363068, 0.8494916,
0.8620000, 0.8738108, 0.8849624, 0.8954936, 0.9054432,
0.9148501, 0.9237348, 0.9320924, 0.9399226, 0.9472252,
0.9540000, 0.9602561, 0.9660074, 0.9712606, 0.9760225,
0.9803000, 0.9840924, 0.9874812, 0.9903128, 0.9928116,
0.9949501, 0.9967108, 0.9980983, 0.9991120, 0.9997482,
1.0000000, 0.9998567, 0.9993046, 0.9983255, 0.9968987,
0.9950000, 0.9926005, 0.9897426, 0.9864444, 0.9827241,
0.9786000, 0.9740837, 0.9691712, 0.9638568, 0.9581349,
0.9520000, 0.9454504, 0.9384992, 0.9311628, 0.9234576,
0.9154000, 0.9070064, 0.8982772, 0.8892048, 0.8797816,
0.8700000, 0.8598613, 0.8493920, 0.8386220, 0.8275813,
0.8163000, 0.8047947, 0.7930820, 0.7811920, 0.7691547,
0.7570000, 0.7447541, 0.7324224, 0.7200036, 0.7074965,
0.6949000, 0.6822192, 0.6694716, 0.6566744, 0.6438448,
0.6310000, 0.6181555, 0.6053144, 0.5924756, 0.5796379,
0.5668000, 0.5539611, 0.5411372, 0.5283528, 0.5156323,
0.5030000, 0.4904688, 0.4780304, 0.4656776, 0.4534032,
0.4412000, 0.4290800, 0.4170360, 0.4050320, 0.3930320,
0.3810000, 0.3689184, 0.3568272, 0.3447768, 0.3328176,
0.3210000, 0.3093381, 0.2978504, 0.2865936, 0.2756245,
0.2650000, 0.2547632, 0.2448896, 0.2353344, 0.2260528,
0.2170000, 0.2081616, 0.1995488, 0.1911552, 0.1829744,
0.1750000, 0.1672235, 0.1596464, 0.1522776, 0.1451259,
0.1382000, 0.1315003, 0.1250248, 0.1187792, 0.1127691,
0.1070000, 0.1014762, 0.09618864, 0.09112296, 0.08626485,
0.08160000, 0.07712064, 0.07282552, 0.06871008, 0.06476976,
0.06100000, 0.05739621, 0.05395504, 0.05067376, 0.04754965,
0.04458000, 0.04175872, 0.03908496, 0.03656384, 0.03420048,
0.03200000, 0.02996261, 0.02807664, 0.02632936, 0.02470805,
0.02320000, 0.02180077, 0.02050112, 0.01928108, 0.01812069,
0.01700000, 0.01590379, 0.01483718, 0.01381068, 0.01283478,
0.01192000, 0.01106831, 0.01027339, 0.009533311, 0.008846157,
0.008210000, 0.007623781, 0.007085424, 0.006591476, 0.006138485,
0.005723000, 0.005343059, 0.004995796, 0.004676404, 0.004380075,
0.004102000, 0.003838453, 0.003589099, 0.003354219, 0.003134093,
0.002929000, 0.002738139, 0.002559876, 0.002393244, 0.002237275,
0.002091000, 0.001953587, 0.001824580, 0.001703580, 0.001590187,
0.001484000, 0.001384496, 0.001291268, 0.001204092, 0.001122744,
0.001047000, 0.0009765896, 0.0009111088, 0.0008501332, 0.0007932384,
0.0007400000, 0.0006900827, 0.0006433100, 0.0005994960, 0.0005584547,
0.0005200000, 0.0004839136, 0.0004500528, 0.0004183452, 0.0003887184,
0.0003611000, 0.0003353835, 0.0003114404, 0.0002891656, 0.0002684539,
0.0002492000, 0.0002313019, 0.0002146856, 0.0001992884, 0.0001850475,
0.0001719000, 0.0001597781, 0.0001486044, 0.0001383016, 0.0001287925,
0.0001200000, 0.0001118595, 0.0001043224, 0.00009733560, 0.00009084587,
0.00008480000, 0.00007914667, 0.00007385800, 0.00006891600, 0.00006430267,
0.00006000000, 0.00005598187, 0.00005222560, 0.00004871840, 0.00004544747,
0.00004240000, 0.00003956104, 0.00003691512, 0.00003444868, 0.00003214816,
0.00003000000, 0.00002799125, 0.00002611356, 0.00002436024, 0.00002272461,
0.00002120000, 0.00001977855, 0.00001845285, 0.00001721687, 0.00001606459,
0.00001499000, 0.00001398728, 0.00001305155, 0.00001217818, 0.00001136254,
0.00001060000, 0.000009885877, 0.000009217304, 0.000008592362, 0.000008009133,
0.000007465700, 0.000006959567, 0.000006487995, 0.000006048699, 0.000005639396,
0.000005257800, 0.000004901771, 0.000004569720, 0.000004260194, 0.000003971739,
0.000003702900, 0.000003452163, 0.000003218302, 0.000003000300, 0.000002797139,
0.000002607800, 0.000002431220, 0.000002266531, 0.000002113013, 0.000001969943,
0.000001836600, 0.000001712230, 0.000001596228, 0.000001488090, 0.000001387314,
0.000001293400, 0.000001205820, 0.000001124143, 0.000001048009, 0.0000009770578,
0.0000009109300, 0.0000008492513, 0.0000007917212, 0.0000007380904, 0.0000006881098,
0.0000006415300, 0.0000005980895, 0.0000005575746, 0.0000005198080, 0.0000004846123,
0.0000004518100
};
private static final double[] CIE_Z = {
// CIE Z function values
0.0006061000, 0.0006808792, 0.0007651456, 0.0008600124, 0.0009665928,
0.001086000, 0.001220586, 0.001372729, 0.001543579, 0.001734286,
0.001946000, 0.002177777, 0.002435809, 0.002731953, 0.003078064,
0.003486000, 0.003975227, 0.004540880, 0.005158320, 0.005802907,
0.006450001, 0.007083216, 0.007745488, 0.008501152, 0.009414544,
0.01054999, 0.01196580, 0.01365587, 0.01558805, 0.01773015,
0.02005001, 0.02251136, 0.02520288, 0.02827972, 0.03189704,
0.03621000, 0.04143771, 0.04750372, 0.05411988, 0.06099803,
0.06785001, 0.07448632, 0.08136156, 0.08915364, 0.09854048,
0.1102000, 0.1246133, 0.1417017, 0.1613035, 0.1832568,
0.2074000, 0.2336921, 0.2626114, 0.2947746, 0.3307985,
0.3713000, 0.4162091, 0.4654642, 0.5196948, 0.5795303,
0.6456000, 0.7184838, 0.7967133, 0.8778459, 0.9594390,
1.0390501, 1.1153673, 1.1884971, 1.2581233, 1.3239296,
1.3856000, 1.4426352, 1.4948035, 1.5421903, 1.5848807,
1.6229600, 1.6564048, 1.6852959, 1.7098745, 1.7303821,
1.7470600, 1.7600446, 1.7696233, 1.7762637, 1.7804334,
1.7826000, 1.7829682, 1.7816998, 1.7791982, 1.7758671,
1.7721100, 1.7682589, 1.7640390, 1.7589438, 1.7524663,
1.7441000, 1.7335595, 1.7208581, 1.7059369, 1.6887372,
1.6692000, 1.6475287, 1.6234127, 1.5960223, 1.5645280,
1.5281000, 1.4861114, 1.4395215, 1.3898799, 1.3387362,
1.2876400, 1.2374223, 1.1878243, 1.1387611, 1.0901480,
1.0419000, 0.9941976, 0.9473473, 0.9014531, 0.8566193,
0.8129501, 0.7705173, 0.7294448, 0.6899136, 0.6521049,
0.6162000, 0.5823286, 0.5504162, 0.5203376, 0.4919673,
0.4651800, 0.4399246, 0.4161836, 0.3938822, 0.3729459,
0.3533000, 0.3348578, 0.3175521, 0.3013375, 0.2861686,
0.2720000, 0.2588171, 0.2464838, 0.2347718, 0.2234533,
0.2123000, 0.2011692, 0.1901196, 0.1792254, 0.1685608,
0.1582000, 0.1481383, 0.1383758, 0.1289942, 0.1200751,
0.1117000, 0.1039048, 0.09666748, 0.08998272, 0.08384531,
0.07824999, 0.07320899, 0.06867816, 0.06456784, 0.06078835,
0.05725001, 0.05390435, 0.05074664, 0.04775276, 0.04489859,
0.04216000, 0.03950728, 0.03693564, 0.03445836, 0.03208872,
0.02984000, 0.02771181, 0.02569444, 0.02378716, 0.02198925,
0.02030000, 0.01871805, 0.01724036, 0.01586364, 0.01458461,
0.01340000, 0.01230723, 0.01130188, 0.01037792, 0.009529306,
0.008749999, 0.008035200, 0.007381600, 0.006785400, 0.006242800,
0.005749999, 0.005303600, 0.004899800, 0.004534200, 0.004202400,
0.003900000, 0.003623200, 0.003370600, 0.003141400, 0.002934800,
0.002749999, 0.002585200, 0.002438600, 0.002309400, 0.002196800,
0.002100000, 0.002017733, 0.001948200, 0.001889800, 0.001840933,
0.001800000, 0.001766267, 0.001737800, 0.001711200, 0.001683067,
0.001650001, 0.001610133, 0.001564400, 0.001513600, 0.001458533,
0.001400000, 0.001336667, 0.001270000, 0.001205000, 0.001146667,
0.001100000, 0.001068800, 0.001049400, 0.001035600, 0.001021200,
0.001000000, 0.0009686400, 0.0009299200, 0.0008868800, 0.0008425600,
0.0008000000, 0.0007609600, 0.0007236800, 0.0006859200, 0.0006454400,
0.0006000000, 0.0005478667, 0.0004916000, 0.0004354000, 0.0003834667,
0.0003400000, 0.0003072533, 0.0002831600, 0.0002654400, 0.0002518133,
0.0002400000, 0.0002295467, 0.0002206400, 0.0002119600, 0.0002021867,
0.0001900000, 0.0001742133, 0.0001556400, 0.0001359600, 0.0001168533,
0.0001000000, 0.00008613333, 0.00007460000, 0.00006500000, 0.00005693333,
0.00004999999, 0.00004416000, 0.00003948000, 0.00003572000, 0.00003264000,
0.00003000000, 0.00002765333, 0.00002556000, 0.00002364000, 0.00002181333,
0.00002000000, 0.00001813333, 0.00001620000, 0.00001420000, 0.00001213333,
0.00001000000, 0.000007733333, 0.000005400000, 0.000003200000, 0.000001333333,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000,
0.000000000000
};
}
}

@ -0,0 +1,79 @@
package eu.jonahbauer.raytracing.render.spectrum;
import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.color.ColorXYZ;
import eu.jonahbauer.raytracing.render.color.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 extends Texture, SkyBox {
int LAMBDA_MIN = 360;
int LAMBDA_MAX = 830;
/**
* {@return the maximum value of <code>this</code> spectrum over the range of wavelengths}
*/
double max();
/**
* {@return the value of <code>this</code> spectrum at a given wavelength}
* @param lambda the wavelength in nanometers
*/
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);
}
default @NotNull ColorXYZ toXYZ() {
return new ColorXYZ(
Util.innerProduct(Spectra.X, this) / ColorXYZ.CIE_Y_INTEGRAL,
Util.innerProduct(Spectra.Y, this) / ColorXYZ.CIE_Y_INTEGRAL,
Util.innerProduct(Spectra.Z, this) / ColorXYZ.CIE_Y_INTEGRAL
);
}
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());
}
enum Type {
ALBEDO, ILLUMINANT, UNBOUNDED
}
}

@ -0,0 +1,17 @@
package eu.jonahbauer.raytracing.render.spectrum;
import org.jetbrains.annotations.NotNull;
final class Util {
private Util() {
throw new UnsupportedOperationException();
}
public static double innerProduct(@NotNull Spectrum f, @NotNull Spectrum g) {
var integral = 0.0;
for (var lambda = Spectrum.LAMBDA_MIN; lambda <= Spectrum.LAMBDA_MAX; lambda++) {
integral += f.get(lambda) * g.get(lambda);
}
return integral;
}
}

@ -1,12 +1,13 @@
package eu.jonahbauer.raytracing.render.texture; package eu.jonahbauer.raytracing.render.texture;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public record CheckerTexture(double scale, @NotNull Texture even, @NotNull Texture odd) implements Texture { public record CheckerTexture(double scale, @NotNull Texture even, @NotNull Texture odd) implements Texture {
@Override @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 x = (int) Math.floor(p.x() / scale);
var y = (int) Math.floor(p.y() / scale); var y = (int) Math.floor(p.y() / scale);
var z = (int) Math.floor(p.z() / scale); var z = (int) Math.floor(p.z() / scale);

@ -1,130 +0,0 @@
package eu.jonahbauer.raytracing.render.texture;
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 {
public static final @NotNull Color BLACK = new Color(0.0, 0.0, 0.0);
public static final @NotNull Color WHITE = new Color(1.0, 1.0, 1.0);
public static final @NotNull Color RED = new Color(1.0, 0.0, 0.0);
public static final @NotNull Color GREEN = new Color(0.0, 1.0, 0.0);
public static final @NotNull Color BLUE = new Color(0.0, 0.0, 1.0);
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(
(1 - t) * a.r + t * b.r,
(1 - t) * a.g + t * b.g,
(1 - t) * a.b + t * b.b
);
}
public static @NotNull Color multiply(@NotNull Color a, @NotNull Color b) {
return new Color(a.r() * b.r(), a.g() * b.g(), a.b() * b.b());
}
public static @NotNull Color multiply(@NotNull Color a, double b) {
return new Color(a.r() * b, a.g() * b, a.b() * b);
}
public static @NotNull Color add(@NotNull Color a, @NotNull Color b) {
return new Color(a.r() + b.r(), a.g() + b.g(), a.b() + b.b());
}
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 average(@NotNull Color current, @NotNull Color next, int index) {
var factor = 1d / index;
return new Color(
Math.fma(factor, next.r() - current.r(), current.r()),
Math.fma(factor, next.g() - current.g(), current.g()),
Math.fma(factor, next.b() - current.b(), current.b())
);
}
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) {
if (!Double.isFinite(r) || !Double.isFinite(g) || !Double.isFinite(b)) {
throw new IllegalArgumentException("r, g and b must be finite");
}
if (r < 0 || g < 0 || b < 0) {
throw new IllegalArgumentException("r, g and b must be non-negative");
}
}
}
public int red() {
return toInt(r);
}
public int green() {
return toInt(g);
}
public int blue() {
return toInt(b);
}
@Override
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
return this;
}
@Override
public @NotNull Color getColor(@NotNull Ray ray) {
return this;
}
@Override
public boolean isUVRequired() {
return false;
}
private static int toInt(double value) {
return Math.clamp((int) (255.99 * value), 0, 255);
}
}

@ -1,7 +1,9 @@
package eu.jonahbauer.raytracing.render.texture; package eu.jonahbauer.raytracing.render.texture;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.canvas.Image; import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.color.ColorSpace;
import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@ -10,22 +12,32 @@ import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.util.Objects; 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 { private final int width;
Objects.requireNonNull(image, "image"); private final int height;
} private final @NotNull Spectrum[][] spectra;
public ImageTexture(@NotNull BufferedImage image, @NotNull ColorSpace cs, @NotNull Spectrum.Type type) {
this.width = image.getWidth();
this.height = image.getHeight();
this.spectra = new Spectrum[height][width];
public ImageTexture(@NotNull BufferedImage image) { for (int y = 0; y < height; y++) {
this(new Image(image)); for (int x = 0; x < width; x++) {
var rgb = cs.decode(new ColorRGB(image.getRGB(x, y)));
spectra[y][x] = cs.toSpectrum(rgb, type);
}
}
} }
public ImageTexture(@NotNull String path) { public ImageTexture(@NotNull String path, @NotNull ColorSpace cs) {
this(read(path)); this(read(path), cs, Spectrum.Type.ALBEDO);
} }
private static @NotNull BufferedImage read(@NotNull String path) { 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); return ImageIO.read(in);
} catch (IOException ex) { } catch (IOException ex) {
throw new UncheckedIOException(ex); throw new UncheckedIOException(ex);
@ -33,11 +45,11 @@ public record ImageTexture(@NotNull Image image) implements Texture {
} }
@Override @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); u = Math.clamp(u, 0, 1);
v = 1 - Math.clamp(v, 0, 1); v = 1 - Math.clamp(v, 0, 1);
int x = (int) (u * (image.getWidth() - 1)); int x = (int) (u * (width - 1));
int y = (int) (v * (image.getHeight() - 1)); int y = (int) (v * (height - 1));
return image.get(x, y); return spectra[y][x];
} }
} }

@ -1,6 +1,8 @@
package eu.jonahbauer.raytracing.render.texture; package eu.jonahbauer.raytracing.render.texture;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.spectrum.Spectra;
import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
@ -11,11 +13,11 @@ import java.util.random.RandomGenerator;
public final class PerlinTexture implements Texture { public final class PerlinTexture implements Texture {
private static final int POINT_COUNT = 256; private static final int POINT_COUNT = 256;
private static final @NotNull Random RANDOM = new Random(); 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 double scale;
private final int turbulence; private final int turbulence;
private final @NotNull DoubleFunction<Color> color; private final @NotNull DoubleFunction<Spectrum> color;
private final int mask; private final int mask;
private final Vec3[] randvec; private final Vec3[] randvec;
@ -35,12 +37,12 @@ public final class PerlinTexture implements Texture {
this(scale, turbulence, GREYSCALE); 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); this(scale, turbulence, color, POINT_COUNT, RANDOM);
} }
public PerlinTexture( public PerlinTexture(
double scale, int turbulence, @NotNull DoubleFunction<Color> color, double scale, int turbulence, @NotNull DoubleFunction<Spectrum> color,
int count, @NotNull RandomGenerator random int count, @NotNull RandomGenerator random
) { ) {
if ((count & (count - 1)) != 0) throw new IllegalArgumentException("count must be a power of two"); 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 @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 noise = getNoise(p, turbulence);
var t = Math.fma(0.5, Math.sin(Math.PI * noise), 0.5); var t = Math.fma(0.5, Math.sin(Math.PI * noise), 0.5);
return color.apply(t); return color.apply(t);

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

@ -7,10 +7,10 @@ import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.render.texture.Texture;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.random.RandomGenerator;
/** /**
* The result of a {@linkplain Hittable#hit(Ray, Range) hit}. * The result of a {@linkplain Hittable#hit(Ray, Range, RandomGenerator) hit}.
* @param t the {@code t} value at which the hit occurs * @param t the {@code t} value at which the hit occurs
* @param position the position of the hit * @param position the position of the hit
* @param normal the surface normal at the hit position * @param normal the surface normal at the hit position
@ -24,10 +24,6 @@ public record HitResult(
double t, @NotNull Vec3 position, @NotNull Vec3 normal, @NotNull Hittable target, double t, @NotNull Vec3 position, @NotNull Vec3 normal, @NotNull Hittable target,
@NotNull Material material, double u, double v, boolean isFrontFace @NotNull Material material, double u, double v, boolean isFrontFace
) implements Comparable<HitResult> { ) implements Comparable<HitResult> {
public HitResult {
Objects.requireNonNull(position, "position");
normal = normal.unit();
}
public @NotNull HitResult withPositionAndNormal(@NotNull Vec3 position, @NotNull Vec3 normal) { public @NotNull HitResult withPositionAndNormal(@NotNull Vec3 position, @NotNull Vec3 normal) {
return new HitResult(t, position, normal, target, material, u, v, isFrontFace); return new HitResult(t, position, normal, target, material, u, v, isFrontFace);

@ -9,15 +9,16 @@ import eu.jonahbauer.raytracing.scene.transform.Translate;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public interface Hittable { public interface Hittable {
@NotNull Range FORWARD = new Range(0.001, Double.POSITIVE_INFINITY); @NotNull Range FORWARD = new Range(0.001, Double.POSITIVE_INFINITY);
/** /**
* @see #hit(Ray, Range) * @see #hit(Ray, Range, RandomGenerator)
*/ */
default @NotNull Optional<HitResult> hit(@NotNull Ray ray) { default @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull RandomGenerator random) {
return hit(ray, FORWARD); return hit(ray, FORWARD, random);
} }
/** /**
@ -31,7 +32,7 @@ public interface Hittable {
* @return the result of the hit test, containing (among others) the value {@code t} such that {@code ray.at(t)} is * @return the result of the hit test, containing (among others) the value {@code t} such that {@code ray.at(t)} is
* a point on {@code this} hittable * a point on {@code this} hittable
*/ */
@NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range); @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range, @NotNull RandomGenerator random);
/** /**
* {@return the axis-aligned bounding box of this hittable} * {@return the axis-aligned bounding box of this hittable}

@ -2,7 +2,8 @@ package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectrum.Spectra;
import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree; import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree;
import eu.jonahbauer.raytracing.scene.util.HittableCollection; import eu.jonahbauer.raytracing.scene.util.HittableCollection;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -11,6 +12,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.random.RandomGenerator;
public final class Scene extends HittableCollection { public final class Scene extends HittableCollection {
private final @NotNull HittableCollection objects; private final @NotNull HittableCollection objects;
@ -23,7 +25,7 @@ public final class Scene extends HittableCollection {
} }
public Scene(@NotNull List<? extends @NotNull Hittable> objects, @Nullable List<? extends @NotNull Target> targets) { 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) { public Scene(@NotNull SkyBox background, @NotNull List<? extends @NotNull Hittable> objects) {
@ -41,8 +43,8 @@ public final class Scene extends HittableCollection {
} }
@Override @Override
public void hit(@NotNull Ray ray, @NotNull State state) { public void hit(@NotNull Ray ray, @NotNull State state, @NotNull RandomGenerator random) {
objects.hit(ray, state); objects.hit(ray, state, random);
} }
@Override @Override
@ -54,7 +56,7 @@ public final class Scene extends HittableCollection {
return targets; return targets;
} }
public @NotNull Color getBackgroundColor(@NotNull Ray ray) { public @NotNull SampledSpectrum getBackgroundColor(@NotNull Ray ray) {
return background.getColor(ray); return background.getColor(ray);
} }
} }

@ -1,21 +1,27 @@
package eu.jonahbauer.raytracing.scene; package eu.jonahbauer.raytracing.scene;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.spectrum.SampledSpectrum;
import eu.jonahbauer.raytracing.render.spectrum.Spectrum;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@FunctionalInterface @FunctionalInterface
public interface SkyBox { 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 -> { return ray -> {
// altitude from -pi/2 to pi/2 // altitude from -pi/2 to pi/2
var alt = Math.copySign( var alt = Math.copySign(
Math.acos(ray.direction().withY(0).unit().times(ray.direction().unit())), Math.acos(ray.direction().withY(0).unit().dot(ray.direction().unit())),
ray.direction().y() 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
);
}; };
} }
} }

@ -7,9 +7,11 @@ import eu.jonahbauer.raytracing.render.material.Material;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public abstract class Hittable2D implements Hittable { public abstract class Hittable2D implements Hittable {
protected final @NotNull Vec3 origin; protected final @NotNull Vec3 origin;
@ -31,23 +33,23 @@ public abstract class Hittable2D implements Hittable {
var n = u.cross(v); var n = u.cross(v);
if (n.squared() < 1e-8) throw new IllegalArgumentException(); if (n.squared() < 1e-8) throw new IllegalArgumentException();
this.normal = n.unit(); this.normal = n.unit();
this.d = origin.times(normal); this.d = origin.dot(normal);
this.w = n.div(n.squared()); this.w = n.div(n.squared());
} }
@Override @Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) { public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range, @Nullable RandomGenerator random) {
var denominator = ray.direction().times(normal); var denominator = ray.direction().dot(normal);
if (Math.abs(denominator) < 1e-8) return Optional.empty(); // parallel if (Math.abs(denominator) < 1e-8) return Optional.empty(); // parallel
var t = (d - ray.origin().times(normal)) / denominator; var t = (d - ray.origin().dot(normal)) / denominator;
if (!range.surrounds(t)) return Optional.empty(); if (!range.surrounds(t)) return Optional.empty();
var position = ray.at(t); var position = ray.at(t);
var p = position.minus(origin); var p = position.minus(origin);
var alpha = w.times(p.cross(v)); var alpha = w.dot(p.cross(v));
var beta = w.times(u.cross(p)); var beta = w.dot(u.cross(p));
if (!isInterior(alpha, beta)) return Optional.empty(); if (!isInterior(alpha, beta)) return Optional.empty();
var frontFace = denominator < 0; var frontFace = denominator < 0;
@ -58,10 +60,10 @@ public abstract class Hittable2D implements Hittable {
} }
protected double hit0(@NotNull Ray ray, @NotNull Range range) { protected double hit0(@NotNull Ray ray, @NotNull Range range) {
var denominator = ray.direction().times(normal); var denominator = ray.direction().dot(normal);
if (Math.abs(denominator) < 1e-8) return Double.NaN; // parallel if (Math.abs(denominator) < 1e-8) return Double.NaN; // parallel
var t = (d - ray.origin().times(normal)) / denominator; var t = (d - ray.origin().dot(normal)) / denominator;
if (!range.surrounds(t)) return Double.NaN; if (!range.surrounds(t)) return Double.NaN;
var position = ray.at(t); var position = ray.at(t);

@ -48,7 +48,7 @@ public final class Box implements Hittable, Target {
} }
@Override @Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) { public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range, @Nullable RandomGenerator random) {
// based on AABB#hit with additional detection of the side hit // based on AABB#hit with additional detection of the side hit
var origin = ray.origin(); var origin = ray.origin();
var direction = ray.direction(); var direction = ray.direction();
@ -103,13 +103,13 @@ public final class Box implements Hittable, Target {
t = tmin; t = tmin;
side = entry; side = entry;
frontFace = true; frontFace = true;
material = materials[side.ordinal()]; material = materials[entry.ordinal()];
normal = side.normal; normal = side.normal;
} else if (range.surrounds(tmax) && materials[exit.ordinal()] != null) { } else if (range.surrounds(tmax) && materials[exit.ordinal()] != null) {
t = tmax; t = tmax;
side = exit; side = exit;
frontFace = false; frontFace = false;
material = materials[side.ordinal()]; material = materials[exit.ordinal()];
normal = side.normal.neg(); normal = side.normal.neg();
} else { } else {
return Optional.empty(); return Optional.empty();
@ -130,7 +130,7 @@ public final class Box implements Hittable, Target {
@Override @Override
public double getProbabilityDensity(@NotNull Vec3 origin, @NotNull Vec3 direction) { public double getProbabilityDensity(@NotNull Vec3 origin, @NotNull Vec3 direction) {
if (contains(origin)) return 1 / (4 * Math.PI); if (contains(origin)) return 1 / (4 * Math.PI);
if (hit(new Ray(origin, direction)).isEmpty()) return 0; if (hit(new Ray(origin, direction), null).isEmpty()) return 0;
var solidAngle = 0d; var solidAngle = 0d;
for (var s : Side.values()) { for (var s : Side.values()) {

@ -10,15 +10,16 @@ import eu.jonahbauer.raytracing.scene.Hittable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNull IsotropicMaterial material) implements Hittable { public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNull IsotropicMaterial material) implements Hittable {
@Override @Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) { public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range, @NotNull RandomGenerator random) {
var hit1 = boundary.hit(ray, Range.UNIVERSE); var hit1 = boundary.hit(ray, Range.UNIVERSE, random);
if (hit1.isEmpty()) return Optional.empty(); if (hit1.isEmpty()) return Optional.empty();
var hit2 = boundary.hit(ray, new Range(hit1.get().t() + 0.0001, Double.POSITIVE_INFINITY)); var hit2 = boundary.hit(ray, new Range(hit1.get().t() + 0.0001, Double.POSITIVE_INFINITY), random);
if (hit2.isEmpty()) return Optional.empty(); if (hit2.isEmpty()) return Optional.empty();
var tmin = Math.max(range.min(), hit1.get().t()); var tmin = Math.max(range.min(), hit1.get().t());
@ -28,7 +29,7 @@ public record ConstantMedium(@NotNull Hittable boundary, double density, @NotNul
var length = ray.direction().length(); var length = ray.direction().length();
var distance = length * (tmax - tmin); var distance = length * (tmax - tmin);
var hitDistance = - Math.log(Math.random()) / density; var hitDistance = - Math.log(random.nextDouble()) / density;
if (hitDistance > distance) return Optional.empty(); if (hitDistance > distance) return Optional.empty();
var t = tmin + hitDistance / length; var t = tmin + hitDistance / length;

@ -9,6 +9,7 @@ import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.Target; import eu.jonahbauer.raytracing.scene.Target;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -40,13 +41,13 @@ public final class Sphere implements Hittable, Target {
} }
@Override @Override
public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) { public @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range, @Nullable RandomGenerator random) {
var t = hit0(ray, range); var t = hit0(ray, range);
if (Double.isNaN(t)) return Optional.empty(); if (Double.isNaN(t)) return Optional.empty();
var position = ray.at(t); var position = ray.at(t);
var normal = Vec3.fma(invRadius, position, normalizedCenter); var normal = Vec3.fma(invRadius, position, normalizedCenter);
var frontFace = normal.times(ray.direction()) < 0; var frontFace = normal.dot(ray.direction()) < 0;
double u; double u;
double v; double v;
@ -70,7 +71,7 @@ public final class Sphere implements Hittable, Target {
var oc = ray.origin().minus(center); var oc = ray.origin().minus(center);
var a = ray.direction().squared(); var a = ray.direction().squared();
var h = ray.direction().times(oc); var h = ray.direction().dot(oc);
var c = oc.squared() - radius * radius; var c = oc.squared() - radius * radius;
var discriminant = h * h - a * c; var discriminant = h * h - a * c;

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

@ -2,10 +2,8 @@ package eu.jonahbauer.raytracing.scene.transform;
import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.HitResult; import eu.jonahbauer.raytracing.scene.HitResult;
import eu.jonahbauer.raytracing.scene.Hittable; import eu.jonahbauer.raytracing.scene.Hittable;
import eu.jonahbauer.raytracing.scene.Target;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
@ -24,7 +22,7 @@ public abstract class Transform implements Hittable {
protected abstract @NotNull HitResult transform(@NotNull HitResult result); protected abstract @NotNull HitResult transform(@NotNull HitResult result);
@Override @Override
public final @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) { public final @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range, @NotNull RandomGenerator random) {
return object.hit(transform(ray), range).map(this::transform); return object.hit(transform(ray), range, random).map(this::transform);
} }
} }

@ -35,7 +35,7 @@ public sealed class Translate extends Transform {
@Override @Override
protected final @NotNull Ray transform(@NotNull Ray ray) { 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 @Override

@ -8,6 +8,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.random.RandomGenerator;
public final class HittableBinaryTree extends HittableCollection { public final class HittableBinaryTree extends HittableCollection {
private final @Nullable Hittable left; private final @Nullable Hittable left;
@ -46,17 +47,17 @@ public final class HittableBinaryTree extends HittableCollection {
} }
@Override @Override
public void hit(@NotNull Ray ray, @NotNull State state) { public void hit(@NotNull Ray ray, @NotNull State state, @NotNull RandomGenerator random) {
if (!bbox.hit(ray, state.getRange())) return; if (!bbox.hit(ray, state.getRange())) return;
if (left instanceof HittableCollection coll) { if (left instanceof HittableCollection coll) {
coll.hit(ray, state); coll.hit(ray, state, random);
} else if (left != null) { } else if (left != null) {
hit(state, ray, left); hit(state, ray, left, random);
} }
if (right instanceof HittableCollection coll) { if (right instanceof HittableCollection coll) {
coll.hit(ray, state); coll.hit(ray, state, random);
} else if (right != null) { } else if (right != null) {
hit(state, ray, right); hit(state, ray, right, random);
} }
} }

@ -8,20 +8,21 @@ import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.random.RandomGenerator;
public abstract class HittableCollection implements Hittable { public abstract class HittableCollection implements Hittable {
@Override @Override
public final @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range) { public final @NotNull Optional<HitResult> hit(@NotNull Ray ray, @NotNull Range range, @NotNull RandomGenerator random) {
var state = new State(range); var state = new State(range);
hit(ray, state); hit(ray, state, random);
return state.getResult(); return state.getResult();
} }
public abstract void hit(@NotNull Ray ray, @NotNull State state); public abstract void hit(@NotNull Ray ray, @NotNull State state, @NotNull RandomGenerator random);
protected static boolean hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object) { protected static boolean hit(@NotNull State state, @NotNull Ray ray, @NotNull Hittable object, @NotNull RandomGenerator random) {
var r = object.hit(ray, state.range); var r = object.hit(ray, state.range, random);
if (r.isPresent()) { if (r.isPresent()) {
if (state.range.surrounds(r.get().t())){ if (state.range.surrounds(r.get().t())){
state.result = r.get(); state.result = r.get();

@ -7,6 +7,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.random.RandomGenerator;
public final class HittableList extends HittableCollection { public final class HittableList extends HittableCollection {
private final @NotNull List<Hittable> objects; private final @NotNull List<Hittable> objects;
@ -22,8 +23,8 @@ public final class HittableList extends HittableCollection {
} }
@Override @Override
public void hit(@NotNull Ray ray, @NotNull State state) { public void hit(@NotNull Ray ray, @NotNull State state, @NotNull RandomGenerator random) {
objects.forEach(object -> hit(state, ray, object)); objects.forEach(object -> hit(state, ray, object, random));
} }
@Override @Override

@ -13,7 +13,7 @@ public final class PdfUtil {
* must be unit vectors. * must be unit vectors.
*/ */
public static double getSolidAngle(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Vec3 c) { public static double getSolidAngle(@NotNull Vec3 a, @NotNull Vec3 b, @NotNull Vec3 c) {
var angle = 2 * Math.atan(Math.abs(Vec3.tripleProduct(a, b, c)) / (1 + a.times(b) + b.times(c) + c.times(a))); var angle = 2 * Math.atan(Math.abs(Vec3.tripleProduct(a, b, c)) / (1 + a.dot(b) + b.dot(c) + c.dot(a)));
return angle < 0 ? 2 * Math.PI + angle : angle; return angle < 0 ? 2 * Math.PI + angle : angle;
} }
} }

@ -0,0 +1,531 @@
300,0.01922
301,0.222348
302,0.425476
303,0.628604
304,0.831732
305,1.03486
306,1.23799
307,1.44112
308,1.64424
309,1.84737
310,2.0505
311,2.62329
312,3.19608
313,3.76887
314,4.34166
315,4.91445
316,5.48724
317,6.06003
318,6.63282
319,7.20561
320,7.7784
321,8.47531
322,9.17222
323,9.86913
324,10.566
325,11.263
326,11.9599
327,12.6568
328,13.3537
329,14.0506
330,14.7475
331,15.0676
332,15.3876
333,15.7076
334,16.0277
335,16.3478
336,16.6678
337,16.9878
338,17.3079
339,17.628
340,17.948
341,18.2542
342,18.5603
343,18.8665
344,19.1727
345,19.4788
346,19.785
347,20.0912
348,20.3974
349,20.7035
350,21.0097
351,21.3029
352,21.5961
353,21.8894
354,22.1826
355,22.4758
356,22.769
357,23.0622
358,23.3555
359,23.6487
360,23.9419
361,24.2438
362,24.5457
363,24.8475
364,25.1494
365,25.4513
366,25.7532
367,26.0551
368,26.3569
369,26.6588
370,26.9607
371,26.7134
372,26.4661
373,26.2187
374,25.9714
375,25.7241
376,25.4768
377,25.2295
378,24.9821
379,24.7348
380,24.4875
381,25.0258
382,25.5641
383,26.1024
384,26.6407
385,27.179
386,27.7174
387,28.2557
388,28.794
389,29.3323
390,29.8706
391,31.8144
392,33.7581
393,35.7018
394,37.6456
395,39.5894
396,41.5331
397,43.4768
398,45.4206
399,47.3644
400,49.3081
401,50.0286
402,50.749
403,51.4695
404,52.19
405,52.9104
406,53.6309
407,54.3514
408,55.0719
409,55.7923
410,56.5128
411,56.8649
412,57.217
413,57.5691
414,57.9212
415,58.2733
416,58.6254
417,58.9775
418,59.3296
419,59.6817
420,60.0338
421,59.8122
422,59.5905
423,59.3689
424,59.1473
425,58.9256
426,58.704
427,58.4824
428,58.2608
429,58.0391
430,57.8175
431,59.5182
432,61.219
433,62.9197
434,64.6205
435,66.3212
436,68.0219
437,69.7227
438,71.4234
439,73.1242
440,74.8249
441,76.0671
442,77.3094
443,78.5516
444,79.7938
445,81.036
446,82.2783
447,83.5205
448,84.7627
449,86.005
450,87.2472
451,87.5837
452,87.9202
453,88.2567
454,88.5932
455,88.9297
456,89.2662
457,89.6027
458,89.9392
459,90.2757
460,90.6122
461,90.6878
462,90.7634
463,90.839
464,90.9146
465,90.9902
466,91.0657
467,91.1413
468,91.2169
469,91.2925
470,91.3681
471,91.7421
472,92.1162
473,92.4902
474,92.8643
475,93.2383
476,93.6123
477,93.9864
478,94.3604
479,94.7345
480,95.1085
481,94.7939
482,94.4793
483,94.1648
484,93.8502
485,93.5356
486,93.221
487,92.9064
488,92.5919
489,92.2773
490,91.9627
491,92.3388
492,92.7149
493,93.091
494,93.4671
495,93.8432
496,94.2193
497,94.5954
498,94.9715
499,95.3476
500,95.7237
501,95.8127
502,95.9016
503,95.9906
504,96.0795
505,96.1685
506,96.2575
507,96.3464
508,96.4354
509,96.5243
510,96.6133
511,96.6649
512,96.7164
513,96.768
514,96.8196
515,96.8712
516,96.9227
517,96.9743
518,97.0259
519,97.0774
520,97.129
521,97.626
522,98.123
523,98.62
524,99.117
525,99.614
526,100.111
527,100.608
528,101.105
529,101.602
530,102.099
531,101.965
532,101.83
533,101.696
534,101.561
535,101.427
536,101.292
537,101.158
538,101.024
539,100.889
540,100.755
541,100.911
542,101.067
543,101.223
544,101.38
545,101.536
546,101.692
547,101.848
548,102.005
549,102.161
550,102.317
551,102.085
552,101.854
553,101.622
554,101.39
555,101.158
556,100.927
557,100.695
558,100.463
559,100.232
560,100
561,99.7735
562,99.547
563,99.3205
564,99.094
565,98.8675
566,98.641
567,98.4145
568,98.188
569,97.9615
570,97.735
571,97.8533
572,97.9716
573,98.0899
574,98.2082
575,98.3265
576,98.4448
577,98.5631
578,98.6814
579,98.7997
580,98.918
581,98.3761
582,97.8342
583,97.2922
584,96.7503
585,96.2084
586,95.6665
587,95.1246
588,94.5826
589,94.0407
590,93.4988
591,93.9177
592,94.3366
593,94.7555
594,95.1744
595,95.5933
596,96.0122
597,96.4311
598,96.85
599,97.2689
600,97.6878
601,97.8459
602,98.0041
603,98.1622
604,98.3203
605,98.4784
606,98.6366
607,98.7947
608,98.9528
609,99.111
610,99.2691
611,99.2463
612,99.2236
613,99.2008
614,99.1781
615,99.1553
616,99.1325
617,99.1098
618,99.087
619,99.0643
620,99.0415
621,98.7095
622,98.3776
623,98.0456
624,97.7136
625,97.3816
626,97.0497
627,96.7177
628,96.3857
629,96.0538
630,95.7218
631,96.0353
632,96.3489
633,96.6624
634,96.976
635,97.2895
636,97.603
637,97.9166
638,98.2301
639,98.5437
640,98.8572
641,98.5382
642,98.2192
643,97.9002
644,97.5812
645,97.2622
646,96.9432
647,96.6242
648,96.3052
649,95.9862
650,95.6672
651,95.9195
652,96.1717
653,96.424
654,96.6762
655,96.9285
656,97.1808
657,97.433
658,97.6853
659,97.9375
660,98.1898
661,98.6712
662,99.1525
663,99.6339
664,100.115
665,100.597
666,101.078
667,101.559
668,102.041
669,102.522
670,103.003
671,102.616
672,102.229
673,101.842
674,101.455
675,101.068
676,100.681
677,100.294
678,99.9071
679,99.52
680,99.133
681,97.9578
682,96.7826
683,95.6074
684,94.4322
685,93.257
686,92.0817
687,90.9065
688,89.7313
689,88.5561
690,87.3809
691,87.8032
692,88.2254
693,88.6477
694,89.0699
695,89.4922
696,89.9145
697,90.3367
698,90.759
699,91.1812
700,91.6035
701,91.732
702,91.8605
703,91.989
704,92.1175
705,92.246
706,92.3746
707,92.5031
708,92.6316
709,92.7601
710,92.8886
711,91.2852
712,89.6818
713,88.0783
714,86.4749
715,84.8715
716,83.2681
717,81.6647
718,80.0612
719,78.4578
720,76.8544
721,77.8201
722,78.7858
723,79.7514
724,80.7171
725,81.6828
726,82.6485
727,83.6142
728,84.5798
729,85.5455
730,86.5112
731,87.1181
732,87.7249
733,88.3318
734,88.9386
735,89.5455
736,90.1524
737,90.7592
738,91.3661
739,91.9729
740,92.5798
741,91.1448
742,89.7098
743,88.2748
744,86.8398
745,85.4048
746,83.9699
747,82.5349
748,81.0999
749,79.6649
750,78.2299
751,76.1761
752,74.1223
753,72.0685
754,70.0147
755,67.9608
756,65.907
757,63.8532
758,61.7994
759,59.7456
760,57.6918
761,60.2149
762,62.738
763,65.2612
764,67.7843
765,70.3074
766,72.8305
767,75.3536
768,77.8768
769,80.3999
770,82.923
771,82.4581
772,81.9932
773,81.5283
774,81.0634
775,80.5985
776,80.1336
777,79.6687
778,79.2038
779,78.7389
780,78.274
781,78.402
782,78.5301
783,78.6581
784,78.7862
785,78.9142
786,79.0422
787,79.1703
788,79.2983
789,79.4264
790,79.5544
791,78.9391
792,78.3238
793,77.7085
794,77.0932
795,76.478
796,75.8627
797,75.2474
798,74.6321
799,74.0168
800,73.4015
801,72.4534
802,71.5052
803,70.5571
804,69.609
805,68.6608
806,67.7127
807,66.7646
808,65.8165
809,64.8683
810,63.9202
811,64.6059
812,65.2916
813,65.9772
814,66.6629
815,67.3486
816,68.0343
817,68.72
818,69.4056
819,70.0913
820,70.777
821,71.1435
822,71.5099
823,71.8764
824,72.2429
825,72.6094
826,72.9758
827,73.3423
828,73.7088
829,74.0752
830,74.4417
1 300 0.01922
2 301 0.222348
3 302 0.425476
4 303 0.628604
5 304 0.831732
6 305 1.03486
7 306 1.23799
8 307 1.44112
9 308 1.64424
10 309 1.84737
11 310 2.0505
12 311 2.62329
13 312 3.19608
14 313 3.76887
15 314 4.34166
16 315 4.91445
17 316 5.48724
18 317 6.06003
19 318 6.63282
20 319 7.20561
21 320 7.7784
22 321 8.47531
23 322 9.17222
24 323 9.86913
25 324 10.566
26 325 11.263
27 326 11.9599
28 327 12.6568
29 328 13.3537
30 329 14.0506
31 330 14.7475
32 331 15.0676
33 332 15.3876
34 333 15.7076
35 334 16.0277
36 335 16.3478
37 336 16.6678
38 337 16.9878
39 338 17.3079
40 339 17.628
41 340 17.948
42 341 18.2542
43 342 18.5603
44 343 18.8665
45 344 19.1727
46 345 19.4788
47 346 19.785
48 347 20.0912
49 348 20.3974
50 349 20.7035
51 350 21.0097
52 351 21.3029
53 352 21.5961
54 353 21.8894
55 354 22.1826
56 355 22.4758
57 356 22.769
58 357 23.0622
59 358 23.3555
60 359 23.6487
61 360 23.9419
62 361 24.2438
63 362 24.5457
64 363 24.8475
65 364 25.1494
66 365 25.4513
67 366 25.7532
68 367 26.0551
69 368 26.3569
70 369 26.6588
71 370 26.9607
72 371 26.7134
73 372 26.4661
74 373 26.2187
75 374 25.9714
76 375 25.7241
77 376 25.4768
78 377 25.2295
79 378 24.9821
80 379 24.7348
81 380 24.4875
82 381 25.0258
83 382 25.5641
84 383 26.1024
85 384 26.6407
86 385 27.179
87 386 27.7174
88 387 28.2557
89 388 28.794
90 389 29.3323
91 390 29.8706
92 391 31.8144
93 392 33.7581
94 393 35.7018
95 394 37.6456
96 395 39.5894
97 396 41.5331
98 397 43.4768
99 398 45.4206
100 399 47.3644
101 400 49.3081
102 401 50.0286
103 402 50.749
104 403 51.4695
105 404 52.19
106 405 52.9104
107 406 53.6309
108 407 54.3514
109 408 55.0719
110 409 55.7923
111 410 56.5128
112 411 56.8649
113 412 57.217
114 413 57.5691
115 414 57.9212
116 415 58.2733
117 416 58.6254
118 417 58.9775
119 418 59.3296
120 419 59.6817
121 420 60.0338
122 421 59.8122
123 422 59.5905
124 423 59.3689
125 424 59.1473
126 425 58.9256
127 426 58.704
128 427 58.4824
129 428 58.2608
130 429 58.0391
131 430 57.8175
132 431 59.5182
133 432 61.219
134 433 62.9197
135 434 64.6205
136 435 66.3212
137 436 68.0219
138 437 69.7227
139 438 71.4234
140 439 73.1242
141 440 74.8249
142 441 76.0671
143 442 77.3094
144 443 78.5516
145 444 79.7938
146 445 81.036
147 446 82.2783
148 447 83.5205
149 448 84.7627
150 449 86.005
151 450 87.2472
152 451 87.5837
153 452 87.9202
154 453 88.2567
155 454 88.5932
156 455 88.9297
157 456 89.2662
158 457 89.6027
159 458 89.9392
160 459 90.2757
161 460 90.6122
162 461 90.6878
163 462 90.7634
164 463 90.839
165 464 90.9146
166 465 90.9902
167 466 91.0657
168 467 91.1413
169 468 91.2169
170 469 91.2925
171 470 91.3681
172 471 91.7421
173 472 92.1162
174 473 92.4902
175 474 92.8643
176 475 93.2383
177 476 93.6123
178 477 93.9864
179 478 94.3604
180 479 94.7345
181 480 95.1085
182 481 94.7939
183 482 94.4793
184 483 94.1648
185 484 93.8502
186 485 93.5356
187 486 93.221
188 487 92.9064
189 488 92.5919
190 489 92.2773
191 490 91.9627
192 491 92.3388
193 492 92.7149
194 493 93.091
195 494 93.4671
196 495 93.8432
197 496 94.2193
198 497 94.5954
199 498 94.9715
200 499 95.3476
201 500 95.7237
202 501 95.8127
203 502 95.9016
204 503 95.9906
205 504 96.0795
206 505 96.1685
207 506 96.2575
208 507 96.3464
209 508 96.4354
210 509 96.5243
211 510 96.6133
212 511 96.6649
213 512 96.7164
214 513 96.768
215 514 96.8196
216 515 96.8712
217 516 96.9227
218 517 96.9743
219 518 97.0259
220 519 97.0774
221 520 97.129
222 521 97.626
223 522 98.123
224 523 98.62
225 524 99.117
226 525 99.614
227 526 100.111
228 527 100.608
229 528 101.105
230 529 101.602
231 530 102.099
232 531 101.965
233 532 101.83
234 533 101.696
235 534 101.561
236 535 101.427
237 536 101.292
238 537 101.158
239 538 101.024
240 539 100.889
241 540 100.755
242 541 100.911
243 542 101.067
244 543 101.223
245 544 101.38
246 545 101.536
247 546 101.692
248 547 101.848
249 548 102.005
250 549 102.161
251 550 102.317
252 551 102.085
253 552 101.854
254 553 101.622
255 554 101.39
256 555 101.158
257 556 100.927
258 557 100.695
259 558 100.463
260 559 100.232
261 560 100
262 561 99.7735
263 562 99.547
264 563 99.3205
265 564 99.094
266 565 98.8675
267 566 98.641
268 567 98.4145
269 568 98.188
270 569 97.9615
271 570 97.735
272 571 97.8533
273 572 97.9716
274 573 98.0899
275 574 98.2082
276 575 98.3265
277 576 98.4448
278 577 98.5631
279 578 98.6814
280 579 98.7997
281 580 98.918
282 581 98.3761
283 582 97.8342
284 583 97.2922
285 584 96.7503
286 585 96.2084
287 586 95.6665
288 587 95.1246
289 588 94.5826
290 589 94.0407
291 590 93.4988
292 591 93.9177
293 592 94.3366
294 593 94.7555
295 594 95.1744
296 595 95.5933
297 596 96.0122
298 597 96.4311
299 598 96.85
300 599 97.2689
301 600 97.6878
302 601 97.8459
303 602 98.0041
304 603 98.1622
305 604 98.3203
306 605 98.4784
307 606 98.6366
308 607 98.7947
309 608 98.9528
310 609 99.111
311 610 99.2691
312 611 99.2463
313 612 99.2236
314 613 99.2008
315 614 99.1781
316 615 99.1553
317 616 99.1325
318 617 99.1098
319 618 99.087
320 619 99.0643
321 620 99.0415
322 621 98.7095
323 622 98.3776
324 623 98.0456
325 624 97.7136
326 625 97.3816
327 626 97.0497
328 627 96.7177
329 628 96.3857
330 629 96.0538
331 630 95.7218
332 631 96.0353
333 632 96.3489
334 633 96.6624
335 634 96.976
336 635 97.2895
337 636 97.603
338 637 97.9166
339 638 98.2301
340 639 98.5437
341 640 98.8572
342 641 98.5382
343 642 98.2192
344 643 97.9002
345 644 97.5812
346 645 97.2622
347 646 96.9432
348 647 96.6242
349 648 96.3052
350 649 95.9862
351 650 95.6672
352 651 95.9195
353 652 96.1717
354 653 96.424
355 654 96.6762
356 655 96.9285
357 656 97.1808
358 657 97.433
359 658 97.6853
360 659 97.9375
361 660 98.1898
362 661 98.6712
363 662 99.1525
364 663 99.6339
365 664 100.115
366 665 100.597
367 666 101.078
368 667 101.559
369 668 102.041
370 669 102.522
371 670 103.003
372 671 102.616
373 672 102.229
374 673 101.842
375 674 101.455
376 675 101.068
377 676 100.681
378 677 100.294
379 678 99.9071
380 679 99.52
381 680 99.133
382 681 97.9578
383 682 96.7826
384 683 95.6074
385 684 94.4322
386 685 93.257
387 686 92.0817
388 687 90.9065
389 688 89.7313
390 689 88.5561
391 690 87.3809
392 691 87.8032
393 692 88.2254
394 693 88.6477
395 694 89.0699
396 695 89.4922
397 696 89.9145
398 697 90.3367
399 698 90.759
400 699 91.1812
401 700 91.6035
402 701 91.732
403 702 91.8605
404 703 91.989
405 704 92.1175
406 705 92.246
407 706 92.3746
408 707 92.5031
409 708 92.6316
410 709 92.7601
411 710 92.8886
412 711 91.2852
413 712 89.6818
414 713 88.0783
415 714 86.4749
416 715 84.8715
417 716 83.2681
418 717 81.6647
419 718 80.0612
420 719 78.4578
421 720 76.8544
422 721 77.8201
423 722 78.7858
424 723 79.7514
425 724 80.7171
426 725 81.6828
427 726 82.6485
428 727 83.6142
429 728 84.5798
430 729 85.5455
431 730 86.5112
432 731 87.1181
433 732 87.7249
434 733 88.3318
435 734 88.9386
436 735 89.5455
437 736 90.1524
438 737 90.7592
439 738 91.3661
440 739 91.9729
441 740 92.5798
442 741 91.1448
443 742 89.7098
444 743 88.2748
445 744 86.8398
446 745 85.4048
447 746 83.9699
448 747 82.5349
449 748 81.0999
450 749 79.6649
451 750 78.2299
452 751 76.1761
453 752 74.1223
454 753 72.0685
455 754 70.0147
456 755 67.9608
457 756 65.907
458 757 63.8532
459 758 61.7994
460 759 59.7456
461 760 57.6918
462 761 60.2149
463 762 62.738
464 763 65.2612
465 764 67.7843
466 765 70.3074
467 766 72.8305
468 767 75.3536
469 768 77.8768
470 769 80.3999
471 770 82.923
472 771 82.4581
473 772 81.9932
474 773 81.5283
475 774 81.0634
476 775 80.5985
477 776 80.1336
478 777 79.6687
479 778 79.2038
480 779 78.7389
481 780 78.274
482 781 78.402
483 782 78.5301
484 783 78.6581
485 784 78.7862
486 785 78.9142
487 786 79.0422
488 787 79.1703
489 788 79.2983
490 789 79.4264
491 790 79.5544
492 791 78.9391
493 792 78.3238
494 793 77.7085
495 794 77.0932
496 795 76.478
497 796 75.8627
498 797 75.2474
499 798 74.6321
500 799 74.0168
501 800 73.4015
502 801 72.4534
503 802 71.5052
504 803 70.5571
505 804 69.609
506 805 68.6608
507 806 67.7127
508 807 66.7646
509 808 65.8165
510 809 64.8683
511 810 63.9202
512 811 64.6059
513 812 65.2916
514 813 65.9772
515 814 66.6629
516 815 67.3486
517 816 68.0343
518 817 68.72
519 818 69.4056
520 819 70.0913
521 820 70.777
522 821 71.1435
523 822 71.5099
524 823 71.8764
525 824 72.2429
526 825 72.6094
527 826 72.9758
528 827 73.3423
529 828 73.7088
530 829 74.0752
531 830 74.4417

@ -0,0 +1,531 @@
300,0.0341
301,0.36014
302,0.68618
303,1.01222
304,1.33826
305,1.6643
306,1.99034
307,2.31638
308,2.64242
309,2.96846
310,3.2945
311,4.98865
312,6.6828
313,8.37695
314,10.0711
315,11.7652
316,13.4594
317,15.1535
318,16.8477
319,18.5418
320,20.236
321,21.9177
322,23.5995
323,25.2812
324,26.963
325,28.6447
326,30.3265
327,32.0082
328,33.69
329,35.3717
330,37.0535
331,37.343
332,37.6326
333,37.9221
334,38.2116
335,38.5011
336,38.7907
337,39.0802
338,39.3697
339,39.6593
340,39.9488
341,40.4451
342,40.9414
343,41.4377
344,41.934
345,42.4302
346,42.9265
347,43.4228
348,43.9191
349,44.4154
350,44.9117
351,45.0844
352,45.257
353,45.4297
354,45.6023
355,45.775
356,45.9477
357,46.1203
358,46.293
359,46.4656
360,46.6383
361,47.1834
362,47.7285
363,48.2735
364,48.8186
365,49.3637
366,49.9088
367,50.4539
368,50.9989
369,51.544
370,52.0891
371,51.8777
372,51.6664
373,51.455
374,51.2437
375,51.0323
376,50.8209
377,50.6096
378,50.3982
379,50.1869
380,49.9755
381,50.4428
382,50.91
383,51.3773
384,51.8446
385,52.3118
386,52.7791
387,53.2464
388,53.7137
389,54.1809
390,54.6482
391,57.4589
392,60.2695
393,63.0802
394,65.8909
395,68.7015
396,71.5122
397,74.3229
398,77.1336
399,79.9442
400,82.7549
401,83.628
402,84.5011
403,85.3742
404,86.2473
405,87.1204
406,87.9936
407,88.8667
408,89.7398
409,90.6129
410,91.486
411,91.6806
412,91.8752
413,92.0697
414,92.2643
415,92.4589
416,92.6535
417,92.8481
418,93.0426
419,93.2372
420,93.4318
421,92.7568
422,92.0819
423,91.4069
424,90.732
425,90.057
426,89.3821
427,88.7071
428,88.0322
429,87.3572
430,86.6823
431,88.5006
432,90.3188
433,92.1371
434,93.9554
435,95.7736
436,97.5919
437,99.4102
438,101.228
439,103.047
440,104.865
441,106.079
442,107.294
443,108.508
444,109.722
445,110.936
446,112.151
447,113.365
448,114.579
449,115.794
450,117.008
451,117.088
452,117.169
453,117.249
454,117.33
455,117.41
456,117.49
457,117.571
458,117.651
459,117.732
460,117.812
461,117.517
462,117.222
463,116.927
464,116.632
465,116.336
466,116.041
467,115.746
468,115.451
469,115.156
470,114.861
471,114.967
472,115.073
473,115.18
474,115.286
475,115.392
476,115.498
477,115.604
478,115.711
479,115.817
480,115.923
481,115.212
482,114.501
483,113.789
484,113.078
485,112.367
486,111.656
487,110.945
488,110.233
489,109.522
490,108.811
491,108.865
492,108.92
493,108.974
494,109.028
495,109.082
496,109.137
497,109.191
498,109.245
499,109.3
500,109.354
501,109.199
502,109.044
503,108.888
504,108.733
505,108.578
506,108.423
507,108.268
508,108.112
509,107.957
510,107.802
511,107.501
512,107.2
513,106.898
514,106.597
515,106.296
516,105.995
517,105.694
518,105.392
519,105.091
520,104.79
521,105.08
522,105.37
523,105.66
524,105.95
525,106.239
526,106.529
527,106.819
528,107.109
529,107.399
530,107.689
531,107.361
532,107.032
533,106.704
534,106.375
535,106.047
536,105.719
537,105.39
538,105.062
539,104.733
540,104.405
541,104.369
542,104.333
543,104.297
544,104.261
545,104.225
546,104.19
547,104.154
548,104.118
549,104.082
550,104.046
551,103.641
552,103.237
553,102.832
554,102.428
555,102.023
556,101.618
557,101.214
558,100.809
559,100.405
560,100
561,99.6334
562,99.2668
563,98.9003
564,98.5337
565,98.1671
566,97.8005
567,97.4339
568,97.0674
569,96.7008
570,96.3342
571,96.2796
572,96.225
573,96.1703
574,96.1157
575,96.0611
576,96.0065
577,95.9519
578,95.8972
579,95.8426
580,95.788
581,95.0778
582,94.3675
583,93.6573
584,92.947
585,92.2368
586,91.5266
587,90.8163
588,90.1061
589,89.3958
590,88.6856
591,88.8177
592,88.9497
593,89.0818
594,89.2138
595,89.3459
596,89.478
597,89.61
598,89.7421
599,89.8741
600,90.0062
601,89.9655
602,89.9248
603,89.8841
604,89.8434
605,89.8026
606,89.7619
607,89.7212
608,89.6805
609,89.6398
610,89.5991
611,89.4091
612,89.219
613,89.029
614,88.8389
615,88.6489
616,88.4589
617,88.2688
618,88.0788
619,87.8887
620,87.6987
621,87.2577
622,86.8167
623,86.3757
624,85.9347
625,85.4936
626,85.0526
627,84.6116
628,84.1706
629,83.7296
630,83.2886
631,83.3297
632,83.3707
633,83.4118
634,83.4528
635,83.4939
636,83.535
637,83.576
638,83.6171
639,83.6581
640,83.6992
641,83.332
642,82.9647
643,82.5975
644,82.2302
645,81.863
646,81.4958
647,81.1285
648,80.7613
649,80.394
650,80.0268
651,80.0456
652,80.0644
653,80.0831
654,80.1019
655,80.1207
656,80.1395
657,80.1583
658,80.177
659,80.1958
660,80.2146
661,80.4209
662,80.6272
663,80.8336
664,81.0399
665,81.2462
666,81.4525
667,81.6588
668,81.8652
669,82.0715
670,82.2778
671,81.8784
672,81.4791
673,81.0797
674,80.6804
675,80.281
676,79.8816
677,79.4823
678,79.0829
679,78.6836
680,78.2842
681,77.4279
682,76.5716
683,75.7153
684,74.859
685,74.0027
686,73.1465
687,72.2902
688,71.4339
689,70.5776
690,69.7213
691,69.9101
692,70.0989
693,70.2876
694,70.4764
695,70.6652
696,70.854
697,71.0428
698,71.2315
699,71.4203
700,71.6091
701,71.8831
702,72.1571
703,72.4311
704,72.7051
705,72.979
706,73.253
707,73.527
708,73.801
709,74.075
710,74.349
711,73.0745
712,71.8
713,70.5255
714,69.251
715,67.9765
716,66.702
717,65.4275
718,64.153
719,62.8785
720,61.604
721,62.4322
722,63.2603
723,64.0885
724,64.9166
725,65.7448
726,66.573
727,67.4011
728,68.2293
729,69.0574
730,69.8856
731,70.4057
732,70.9259
733,71.446
734,71.9662
735,72.4863
736,73.0064
737,73.5266
738,74.0467
739,74.5669
740,75.087
741,73.9376
742,72.7881
743,71.6387
744,70.4893
745,69.3398
746,68.1904
747,67.041
748,65.8916
749,64.7421
750,63.5927
751,61.8752
752,60.1578
753,58.4403
754,56.7229
755,55.0054
756,53.288
757,51.5705
758,49.8531
759,48.1356
760,46.4182
761,48.4569
762,50.4956
763,52.5344
764,54.5731
765,56.6118
766,58.6505
767,60.6892
768,62.728
769,64.7667
770,66.8054
771,66.4631
772,66.1209
773,65.7786
774,65.4364
775,65.0941
776,64.7518
777,64.4096
778,64.0673
779,63.7251
780,63.3828
781,63.4749
782,63.567
783,63.6592
784,63.7513
785,63.8434
786,63.9355
787,64.0276
788,64.1198
789,64.2119
790,64.304
791,63.8188
792,63.3336
793,62.8484
794,62.3632
795,61.8779
796,61.3927
797,60.9075
798,60.4223
799,59.9371
800,59.4519
801,58.7026
802,57.9533
803,57.204
804,56.4547
805,55.7054
806,54.9562
807,54.2069
808,53.4576
809,52.7083
810,51.959
811,52.5072
812,53.0553
813,53.6035
814,54.1516
815,54.6998
816,55.248
817,55.7961
818,56.3443
819,56.8924
820,57.4406
821,57.7278
822,58.015
823,58.3022
824,58.5894
825,58.8765
826,59.1637
827,59.4509
828,59.7381
829,60.0253
830,60.3125
1 300 0.0341
2 301 0.36014
3 302 0.68618
4 303 1.01222
5 304 1.33826
6 305 1.6643
7 306 1.99034
8 307 2.31638
9 308 2.64242
10 309 2.96846
11 310 3.2945
12 311 4.98865
13 312 6.6828
14 313 8.37695
15 314 10.0711
16 315 11.7652
17 316 13.4594
18 317 15.1535
19 318 16.8477
20 319 18.5418
21 320 20.236
22 321 21.9177
23 322 23.5995
24 323 25.2812
25 324 26.963
26 325 28.6447
27 326 30.3265
28 327 32.0082
29 328 33.69
30 329 35.3717
31 330 37.0535
32 331 37.343
33 332 37.6326
34 333 37.9221
35 334 38.2116
36 335 38.5011
37 336 38.7907
38 337 39.0802
39 338 39.3697
40 339 39.6593
41 340 39.9488
42 341 40.4451
43 342 40.9414
44 343 41.4377
45 344 41.934
46 345 42.4302
47 346 42.9265
48 347 43.4228
49 348 43.9191
50 349 44.4154
51 350 44.9117
52 351 45.0844
53 352 45.257
54 353 45.4297
55 354 45.6023
56 355 45.775
57 356 45.9477
58 357 46.1203
59 358 46.293
60 359 46.4656
61 360 46.6383
62 361 47.1834
63 362 47.7285
64 363 48.2735
65 364 48.8186
66 365 49.3637
67 366 49.9088
68 367 50.4539
69 368 50.9989
70 369 51.544
71 370 52.0891
72 371 51.8777
73 372 51.6664
74 373 51.455
75 374 51.2437
76 375 51.0323
77 376 50.8209
78 377 50.6096
79 378 50.3982
80 379 50.1869
81 380 49.9755
82 381 50.4428
83 382 50.91
84 383 51.3773
85 384 51.8446
86 385 52.3118
87 386 52.7791
88 387 53.2464
89 388 53.7137
90 389 54.1809
91 390 54.6482
92 391 57.4589
93 392 60.2695
94 393 63.0802
95 394 65.8909
96 395 68.7015
97 396 71.5122
98 397 74.3229
99 398 77.1336
100 399 79.9442
101 400 82.7549
102 401 83.628
103 402 84.5011
104 403 85.3742
105 404 86.2473
106 405 87.1204
107 406 87.9936
108 407 88.8667
109 408 89.7398
110 409 90.6129
111 410 91.486
112 411 91.6806
113 412 91.8752
114 413 92.0697
115 414 92.2643
116 415 92.4589
117 416 92.6535
118 417 92.8481
119 418 93.0426
120 419 93.2372
121 420 93.4318
122 421 92.7568
123 422 92.0819
124 423 91.4069
125 424 90.732
126 425 90.057
127 426 89.3821
128 427 88.7071
129 428 88.0322
130 429 87.3572
131 430 86.6823
132 431 88.5006
133 432 90.3188
134 433 92.1371
135 434 93.9554
136 435 95.7736
137 436 97.5919
138 437 99.4102
139 438 101.228
140 439 103.047
141 440 104.865
142 441 106.079
143 442 107.294
144 443 108.508
145 444 109.722
146 445 110.936
147 446 112.151
148 447 113.365
149 448 114.579
150 449 115.794
151 450 117.008
152 451 117.088
153 452 117.169
154 453 117.249
155 454 117.33
156 455 117.41
157 456 117.49
158 457 117.571
159 458 117.651
160 459 117.732
161 460 117.812
162 461 117.517
163 462 117.222
164 463 116.927
165 464 116.632
166 465 116.336
167 466 116.041
168 467 115.746
169 468 115.451
170 469 115.156
171 470 114.861
172 471 114.967
173 472 115.073
174 473 115.18
175 474 115.286
176 475 115.392
177 476 115.498
178 477 115.604
179 478 115.711
180 479 115.817
181 480 115.923
182 481 115.212
183 482 114.501
184 483 113.789
185 484 113.078
186 485 112.367
187 486 111.656
188 487 110.945
189 488 110.233
190 489 109.522
191 490 108.811
192 491 108.865
193 492 108.92
194 493 108.974
195 494 109.028
196 495 109.082
197 496 109.137
198 497 109.191
199 498 109.245
200 499 109.3
201 500 109.354
202 501 109.199
203 502 109.044
204 503 108.888
205 504 108.733
206 505 108.578
207 506 108.423
208 507 108.268
209 508 108.112
210 509 107.957
211 510 107.802
212 511 107.501
213 512 107.2
214 513 106.898
215 514 106.597
216 515 106.296
217 516 105.995
218 517 105.694
219 518 105.392
220 519 105.091
221 520 104.79
222 521 105.08
223 522 105.37
224 523 105.66
225 524 105.95
226 525 106.239
227 526 106.529
228 527 106.819
229 528 107.109
230 529 107.399
231 530 107.689
232 531 107.361
233 532 107.032
234 533 106.704
235 534 106.375
236 535 106.047
237 536 105.719
238 537 105.39
239 538 105.062
240 539 104.733
241 540 104.405
242 541 104.369
243 542 104.333
244 543 104.297
245 544 104.261
246 545 104.225
247 546 104.19
248 547 104.154
249 548 104.118
250 549 104.082
251 550 104.046
252 551 103.641
253 552 103.237
254 553 102.832
255 554 102.428
256 555 102.023
257 556 101.618
258 557 101.214
259 558 100.809
260 559 100.405
261 560 100
262 561 99.6334
263 562 99.2668
264 563 98.9003
265 564 98.5337
266 565 98.1671
267 566 97.8005
268 567 97.4339
269 568 97.0674
270 569 96.7008
271 570 96.3342
272 571 96.2796
273 572 96.225
274 573 96.1703
275 574 96.1157
276 575 96.0611
277 576 96.0065
278 577 95.9519
279 578 95.8972
280 579 95.8426
281 580 95.788
282 581 95.0778
283 582 94.3675
284 583 93.6573
285 584 92.947
286 585 92.2368
287 586 91.5266
288 587 90.8163
289 588 90.1061
290 589 89.3958
291 590 88.6856
292 591 88.8177
293 592 88.9497
294 593 89.0818
295 594 89.2138
296 595 89.3459
297 596 89.478
298 597 89.61
299 598 89.7421
300 599 89.8741
301 600 90.0062
302 601 89.9655
303 602 89.9248
304 603 89.8841
305 604 89.8434
306 605 89.8026
307 606 89.7619
308 607 89.7212
309 608 89.6805
310 609 89.6398
311 610 89.5991
312 611 89.4091
313 612 89.219
314 613 89.029
315 614 88.8389
316 615 88.6489
317 616 88.4589
318 617 88.2688
319 618 88.0788
320 619 87.8887
321 620 87.6987
322 621 87.2577
323 622 86.8167
324 623 86.3757
325 624 85.9347
326 625 85.4936
327 626 85.0526
328 627 84.6116
329 628 84.1706
330 629 83.7296
331 630 83.2886
332 631 83.3297
333 632 83.3707
334 633 83.4118
335 634 83.4528
336 635 83.4939
337 636 83.535
338 637 83.576
339 638 83.6171
340 639 83.6581
341 640 83.6992
342 641 83.332
343 642 82.9647
344 643 82.5975
345 644 82.2302
346 645 81.863
347 646 81.4958
348 647 81.1285
349 648 80.7613
350 649 80.394
351 650 80.0268
352 651 80.0456
353 652 80.0644
354 653 80.0831
355 654 80.1019
356 655 80.1207
357 656 80.1395
358 657 80.1583
359 658 80.177
360 659 80.1958
361 660 80.2146
362 661 80.4209
363 662 80.6272
364 663 80.8336
365 664 81.0399
366 665 81.2462
367 666 81.4525
368 667 81.6588
369 668 81.8652
370 669 82.0715
371 670 82.2778
372 671 81.8784
373 672 81.4791
374 673 81.0797
375 674 80.6804
376 675 80.281
377 676 79.8816
378 677 79.4823
379 678 79.0829
380 679 78.6836
381 680 78.2842
382 681 77.4279
383 682 76.5716
384 683 75.7153
385 684 74.859
386 685 74.0027
387 686 73.1465
388 687 72.2902
389 688 71.4339
390 689 70.5776
391 690 69.7213
392 691 69.9101
393 692 70.0989
394 693 70.2876
395 694 70.4764
396 695 70.6652
397 696 70.854
398 697 71.0428
399 698 71.2315
400 699 71.4203
401 700 71.6091
402 701 71.8831
403 702 72.1571
404 703 72.4311
405 704 72.7051
406 705 72.979
407 706 73.253
408 707 73.527
409 708 73.801
410 709 74.075
411 710 74.349
412 711 73.0745
413 712 71.8
414 713 70.5255
415 714 69.251
416 715 67.9765
417 716 66.702
418 717 65.4275
419 718 64.153
420 719 62.8785
421 720 61.604
422 721 62.4322
423 722 63.2603
424 723 64.0885
425 724 64.9166
426 725 65.7448
427 726 66.573
428 727 67.4011
429 728 68.2293
430 729 69.0574
431 730 69.8856
432 731 70.4057
433 732 70.9259
434 733 71.446
435 734 71.9662
436 735 72.4863
437 736 73.0064
438 737 73.5266
439 738 74.0467
440 739 74.5669
441 740 75.087
442 741 73.9376
443 742 72.7881
444 743 71.6387
445 744 70.4893
446 745 69.3398
447 746 68.1904
448 747 67.041
449 748 65.8916
450 749 64.7421
451 750 63.5927
452 751 61.8752
453 752 60.1578
454 753 58.4403
455 754 56.7229
456 755 55.0054
457 756 53.288
458 757 51.5705
459 758 49.8531
460 759 48.1356
461 760 46.4182
462 761 48.4569
463 762 50.4956
464 763 52.5344
465 764 54.5731
466 765 56.6118
467 766 58.6505
468 767 60.6892
469 768 62.728
470 769 64.7667
471 770 66.8054
472 771 66.4631
473 772 66.1209
474 773 65.7786
475 774 65.4364
476 775 65.0941
477 776 64.7518
478 777 64.4096
479 778 64.0673
480 779 63.7251
481 780 63.3828
482 781 63.4749
483 782 63.567
484 783 63.6592
485 784 63.7513
486 785 63.8434
487 786 63.9355
488 787 64.0276
489 788 64.1198
490 789 64.2119
491 790 64.304
492 791 63.8188
493 792 63.3336
494 793 62.8484
495 794 62.3632
496 795 61.8779
497 796 61.3927
498 797 60.9075
499 798 60.4223
500 799 59.9371
501 800 59.4519
502 801 58.7026
503 802 57.9533
504 803 57.204
505 804 56.4547
506 805 55.7054
507 806 54.9562
508 807 54.2069
509 808 53.4576
510 809 52.7083
511 810 51.959
512 811 52.5072
513 812 53.0553
514 813 53.6035
515 814 54.1516
516 815 54.6998
517 816 55.248
518 817 55.7961
519 818 56.3443
520 819 56.8924
521 820 57.4406
522 821 57.7278
523 822 58.015
524 823 58.3022
525 824 58.5894
526 825 58.8765
527 826 59.1637
528 827 59.4509
529 828 59.7381
530 829 60.0253
531 830 60.3125

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

@ -21,10 +21,10 @@ class Vec3Test {
} }
@Test @Test
void timesVec() { void dotVec() {
var a = new Vec3(1, 2, 3); var a = new Vec3(1, 2, 3);
var b = new Vec3(-1, 1, -2); var b = new Vec3(-1, 1, -2);
assertEquals(-5, a.times(b)); assertEquals(-5, a.dot(b));
} }
@Test @Test

@ -1,6 +1,6 @@
package eu.jonahbauer.raytracing.render.canvas; package eu.jonahbauer.raytracing.render.canvas;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.ImageFormat; import eu.jonahbauer.raytracing.render.ImageFormat;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
@ -13,11 +13,11 @@ import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
class ImageTest { class RGBCanvasTest {
@Test @Test
void test(@TempDir Path dir) throws IOException { 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 y = 0; y < image.getHeight(); y++) {
for (var x = 0; x < image.getWidth(); x++) { for (var x = 0; x < image.getWidth(); x++) {
@ -25,7 +25,7 @@ class ImageTest {
var g = (double) y / (image.getHeight() - 1); var g = (double) y / (image.getHeight() - 1);
var b = 0; 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 expected;
String actual; 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); 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.Range;
import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Ray;
import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.texture.Color; import eu.jonahbauer.raytracing.render.color.ColorRGB;
import eu.jonahbauer.raytracing.render.material.LambertianMaterial; import eu.jonahbauer.raytracing.render.material.LambertianMaterial;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -15,7 +15,7 @@ class SphereTest {
void hit() { void hit() {
var center = new Vec3(1, 2, 3); var center = new Vec3(1, 2, 3);
var radius = 5; 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 origin = new Vec3(6, 7, 8);
var direction = new Vec3(-1, -1, -1); var direction = new Vec3(-1, -1, -1);

Loading…
Cancel
Save