diff --git a/src/main/java/eu/jonahbauer/raytracing/Examples.java b/src/main/java/eu/jonahbauer/raytracing/Examples.java
index 401be82..bb545d2 100644
--- a/src/main/java/eu/jonahbauer/raytracing/Examples.java
+++ b/src/main/java/eu/jonahbauer/raytracing/Examples.java
@@ -1,6 +1,5 @@
package eu.jonahbauer.raytracing;
-import eu.jonahbauer.raytracing.math.AABB;
import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.render.texture.CheckerTexture;
import eu.jonahbauer.raytracing.render.texture.Color;
@@ -293,7 +292,7 @@ public class Examples {
return new Example(
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("/eu/jonahbauer/raytracing/textures/earthmap.jpg")))
)),
SimpleCamera.builder()
.withImage(height * 16 / 9, height)
@@ -369,7 +368,7 @@ public class Examples {
));
// 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("/eu/jonahbauer/raytracing/textures/earthmap.jpg"))));
objects.add(new Sphere(new Vec3(220, 280, 300), 80, new LambertianMaterial(new PerlinTexture(0.2))));
// box from spheres
diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Matrix3.java b/src/main/java/eu/jonahbauer/raytracing/math/Matrix3.java
new file mode 100644
index 0000000..7a40be3
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/math/Matrix3.java
@@ -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 LU decomposition — Wikipedia, The Free Encyclopedia
+ */
+ 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]);
+ }
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java
new file mode 100644
index 0000000..8566fa8
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java
@@ -0,0 +1,71 @@
+package eu.jonahbauer.raytracing.render.spectral;
+
+import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
+import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ;
+import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
+import eu.jonahbauer.raytracing.render.texture.Color;
+import org.jetbrains.annotations.NotNull;
+
+public final class 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;
+ }
+
+ private SampledSpectrum(double @NotNull[] values) {
+ this.values = values;
+ }
+
+ public static @NotNull SampledSpectrum multiply(@NotNull SampledSpectrum a, @NotNull SampledSpectrum b) {
+ var out = new double[a.values.length];
+ for (int i = 0; i < a.values.length; i++) {
+ out[i] = a.values[i] * b.values[i];
+ }
+ return new SampledSpectrum(out);
+ }
+
+ public static @NotNull SampledSpectrum multiply(@NotNull SampledSpectrum a, double b) {
+ var out = new double[a.values.length];
+ for (int i = 0; i < a.values.length; i++) {
+ out[i] = a.values[i] * b;
+ }
+ return new SampledSpectrum(out);
+ }
+
+ public static @NotNull SampledSpectrum add(@NotNull SampledSpectrum a, @NotNull SampledSpectrum b) {
+ var out = new double[a.values.length];
+ for (int i = 0; i < a.values.length; i++) {
+ out[i] = a.values[i] + b.values[i];
+ }
+ return new SampledSpectrum(out);
+ }
+
+ public double get(int index) {
+ return values[index];
+ }
+
+ public int size() {
+ return values.length;
+ }
+
+ public double average() {
+ double avg = 0;
+ for (int i = 0; i < values.length; i++) {
+ avg = Math.fma(1d / (i + 1), values[i] - avg, avg);
+ }
+ return avg;
+ }
+
+ public @NotNull ColorXYZ toXYZ(@NotNull SampledWavelengths lambdas) {
+ return lambdas.toXYZ(this);
+ }
+
+ public @NotNull Color toRGB(@NotNull SampledWavelengths lambdas, @NotNull ColorSpace cs) {
+ return cs.toRGB(toXYZ(lambdas));
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledWavelengths.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledWavelengths.java
new file mode 100644
index 0000000..8b40b56
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledWavelengths.java
@@ -0,0 +1,93 @@
+package eu.jonahbauer.raytracing.render.spectral;
+
+import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ;
+import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra;
+import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
+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 int SAMPLES = 4;
+
+ private final double @NotNull[] lambdas;
+ private final double @NotNull[] pdf;
+
+ public static @NotNull SampledWavelengths uniform(double rng) {
+ return uniform(rng, Spectrum.LAMBDA_MIN, Spectrum.LAMBDA_MAX);
+ }
+
+ public static @NotNull SampledWavelengths uniform(double rng, double min, double max) {
+ var lambdas = new double[SAMPLES];
+
+ // 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) / SAMPLES;
+ for (int i = 1; i < SAMPLES; i++) {
+ lambdas[i] = lambdas[i - 1] + delta;
+ if (lambdas[i] > max) {
+ lambdas[i] = min + (lambdas[i] - max);
+ }
+ }
+
+ var pdf = new double[SAMPLES];
+ 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;
+ }
+
+ public double get(int index) {
+ return lambdas[index];
+ }
+
+ 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.
+ */
+ public void terminateSecondary() {
+ if (pdf.length < 2 || pdf[1] == 0) return;
+ Arrays.fill(pdf, 1, pdf.length, 0d);
+ pdf[0] /= pdf.length;
+ }
+
+ @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;
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/Chromaticity.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/Chromaticity.java
new file mode 100644
index 0000000..3b9c3de
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/Chromaticity.java
@@ -0,0 +1,9 @@
+package eu.jonahbauer.raytracing.render.spectral.colors;
+
+/**
+ * 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) {
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorSpace.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorSpace.java
new file mode 100644
index 0000000..a71f759
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorSpace.java
@@ -0,0 +1,116 @@
+package eu.jonahbauer.raytracing.render.spectral.colors;
+
+import eu.jonahbauer.raytracing.math.Matrix3;
+import eu.jonahbauer.raytracing.math.Vec3;
+import eu.jonahbauer.raytracing.render.spectral.spectrum.DenselySampledSpectrum;
+import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum;
+import eu.jonahbauer.raytracing.render.texture.Color;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+/**
+ * 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;
+
+ public ColorSpace(
+ @NotNull Chromaticity r, @NotNull Chromaticity g, @NotNull Chromaticity b,
+ @NotNull Spectrum illuminant, @NotNull SpectrumTable table
+ ) {
+ 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;
+
+ 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();
+ }
+
+ public @NotNull Color toRGB(@NotNull ColorXYZ xyz) {
+ var out = RGBfromXYZ.times(xyz.toVec3());
+ return new Color(out.x(), out.y(), out.z());
+ }
+
+ public @NotNull ColorXYZ toXYZ(@NotNull Color rgb) {
+ var out = XYZfromRGB.times(rgb.toVec3());
+ return new ColorXYZ(out);
+ }
+
+ public @NotNull Vec3 toCIELab(@NotNull Color 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 SigmoidPolynomial toSpectrum(@NotNull Color rgb) {
+ return RGBtoSpectrumTable.get(new Color(
+ Math.max(0, rgb.r()),
+ Math.max(0, rgb.g()),
+ Math.max(0, rgb.b())
+ ));
+ }
+
+ 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;
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorSpaces.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorSpaces.java
new file mode 100644
index 0000000..7f42d6f
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorSpaces.java
@@ -0,0 +1,44 @@
+package eu.jonahbauer.raytracing.render.spectral.colors;
+
+import eu.jonahbauer.raytracing.render.spectral.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")
+ );
+ // 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")
+ );
+ // 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")
+ );
+
+ 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();
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorXYZ.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorXYZ.java
new file mode 100644
index 0000000..eb0147b
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorXYZ.java
@@ -0,0 +1,52 @@
+package eu.jonahbauer.raytracing.render.spectral.colors;
+
+import eu.jonahbauer.raytracing.math.Vec3;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A CIE XYZ color
+ */
+public record ColorXYZ(double x, double y, double z) {
+ public static final double CIE_Y_INTEGRAL = 106.85689500000002;
+
+ 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 double average() {
+ return (x + y + z) / 3;
+ }
+
+ public @NotNull Chromaticity xy() {
+ var factor = 1 / (x + y + z);
+ return new Chromaticity(factor * x, factor * y);
+ }
+
+ public @NotNull Vec3 toVec3() {
+ return new Vec3(x, y, z);
+ }
+
+ public static @NotNull ColorXYZ multiply(@NotNull ColorXYZ a, @NotNull ColorXYZ b) {
+ return new ColorXYZ(a.x * b.x, a.y * b.y, a.z * b.z);
+ }
+
+ public static @NotNull ColorXYZ multiply(@NotNull ColorXYZ a, double b) {
+ return new ColorXYZ(a.x * b, a.y * b, a.z * b);
+ }
+
+ public static @NotNull ColorXYZ add(@NotNull ColorXYZ a, @NotNull ColorXYZ b) {
+ return new ColorXYZ(a.x + b.x, a.y + b.y, a.z + b.z);
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SigmoidPolynomial.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SigmoidPolynomial.java
new file mode 100644
index 0000000..64c391b
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SigmoidPolynomial.java
@@ -0,0 +1,34 @@
+package eu.jonahbauer.raytracing.render.spectral.colors;
+
+import eu.jonahbauer.raytracing.render.spectral.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 s(x) = 0.5 + x / (2 * sqrt(1 + x^2))
.
+ *
+ * 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; + } + } +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTable.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTable.java new file mode 100644 index 0000000..e573d07 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTable.java @@ -0,0 +1,143 @@ +package eu.jonahbauer.raytracing.render.spectral.colors; + +import eu.jonahbauer.raytracing.render.texture.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. + *
+ * 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. + *
+ * 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 + *
+ * 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. + *
+ *
+ */
+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 Color 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 Color 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 Color 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 Color 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 Color getColor(int l, double x, double y, double z) {
+ var rgb = new double[3];
+ rgb[l] = z;
+ rgb[(l + 1) % 3] = x * z;
+ rgb[(l + 2) % 3] = y * z;
+ return new Color(rgb[0], rgb[1], rgb[2]);
+ }
+
+ 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);
+ }
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/doc-files/rgb2spectrum.png b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/doc-files/rgb2spectrum.png
new file mode 100644
index 0000000..583edbe
Binary files /dev/null and b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/doc-files/rgb2spectrum.png differ
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/BlackbodySpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/BlackbodySpectrum.java
new file mode 100644
index 0000000..4a07f8b
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/BlackbodySpectrum.java
@@ -0,0 +1,45 @@
+package eu.jonahbauer.raytracing.render.spectral.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;
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/ConstantSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/ConstantSpectrum.java
new file mode 100644
index 0000000..e5348da
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/ConstantSpectrum.java
@@ -0,0 +1,17 @@
+package eu.jonahbauer.raytracing.render.spectral.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;
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/DenselySampledSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/DenselySampledSpectrum.java
new file mode 100644
index 0000000..c3e24ae
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/DenselySampledSpectrum.java
@@ -0,0 +1,59 @@
+package eu.jonahbauer.raytracing.render.spectral.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();
+ }
+
+ public @NotNull DenselySampledSpectrum scale(double scale) {
+ var s = Arrays.copyOf(samples, samples.length);
+ for (int i = 0; i < s.length; i++) {
+ s[i] *= scale;
+ }
+ return new DenselySampledSpectrum(s, min);
+ }
+
+ @Override
+ public double max() {
+ return max;
+ }
+
+ @Override
+ public double get(double lambda) {
+ int offset = (int) Math.round(lambda) - min;
+ if (offset < 0 || offset >= samples.length) return 0;
+ return samples[offset];
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/PiecewiseLinearSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/PiecewiseLinearSpectrum.java
new file mode 100644
index 0000000..4ff4519
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/PiecewiseLinearSpectrum.java
@@ -0,0 +1,58 @@
+package eu.jonahbauer.raytracing.render.spectral.spectrum;
+
+import org.jetbrains.annotations.NotNull;
+
+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;
+ }
+
+ public @NotNull PiecewiseLinearSpectrum scale(double scale) {
+ var v = Arrays.copyOf(values, values.length);
+ for (int i = 0; i < v.length; i++) {
+ v[i] *= scale;
+ }
+ return new PiecewiseLinearSpectrum(lambdas, v);
+ }
+
+ @Override
+ public double max() {
+ return max;
+ }
+
+ @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];
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBAlbedoSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBAlbedoSpectrum.java
new file mode 100644
index 0000000..58b9d21
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBAlbedoSpectrum.java
@@ -0,0 +1,27 @@
+package eu.jonahbauer.raytracing.render.spectral.spectrum;
+
+import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
+import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial;
+import eu.jonahbauer.raytracing.render.texture.Color;
+import org.jetbrains.annotations.NotNull;
+
+public final class RGBAlbedoSpectrum implements Spectrum {
+ private final @NotNull SigmoidPolynomial polynomial;
+
+ public RGBAlbedoSpectrum(@NotNull ColorSpace cs, @NotNull Color 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.toSpectrum(rgb);
+ }
+
+ @Override
+ public double max() {
+ return polynomial.max();
+ }
+
+ @Override
+ public double get(double lambda) {
+ return polynomial.get(lambda);
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBIlluminantSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBIlluminantSpectrum.java
new file mode 100644
index 0000000..9ad0421
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBIlluminantSpectrum.java
@@ -0,0 +1,36 @@
+package eu.jonahbauer.raytracing.render.spectral.spectrum;
+
+import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
+import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial;
+import eu.jonahbauer.raytracing.render.texture.Color;
+import 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 Color rgb) {
+ if (rgb.r() < 0 || rgb.g() < 0 || rgb.b() < 0) {
+ throw new IllegalArgumentException();
+ }
+ var max = Math.max(rgb.r(), Math.max(rgb.g(), rgb.b()));
+ this.scale = 2 * max;
+ this.polynomial = cs.toSpectrum(scale == 0 ? Color.multiply(rgb, scale) : Color.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);
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBUnboundedSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBUnboundedSpectrum.java
new file mode 100644
index 0000000..d98654e
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBUnboundedSpectrum.java
@@ -0,0 +1,30 @@
+package eu.jonahbauer.raytracing.render.spectral.spectrum;
+
+import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace;
+import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial;
+import eu.jonahbauer.raytracing.render.texture.Color;
+import org.jetbrains.annotations.NotNull;
+
+public final class RGBUnboundedSpectrum implements Spectrum {
+ private final double scale;
+ private final @NotNull SigmoidPolynomial polynomial;
+
+ public RGBUnboundedSpectrum(@NotNull ColorSpace cs, @NotNull Color rgb) {
+ if (rgb.r() < 0 || rgb.g() < 0 || rgb.b() < 0) {
+ throw new IllegalArgumentException();
+ }
+ var max = Math.max(rgb.r(), Math.max(rgb.g(), rgb.b()));
+ this.scale = 2 * max;
+ this.polynomial = cs.toSpectrum(scale == 0 ? Color.multiply(rgb, scale) : Color.BLACK);
+ }
+
+ @Override
+ public double max() {
+ return scale * polynomial.max();
+ }
+
+ @Override
+ public double get(double lambda) {
+ return scale * polynomial.get(lambda);
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectra.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectra.java
new file mode 100644
index 0000000..e007509
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectra.java
@@ -0,0 +1,407 @@
+package eu.jonahbauer.raytracing.render.spectral.spectrum;
+
+import eu.jonahbauer.raytracing.render.spectral.colors.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 CIE 2022, CIE standard illuminant D65, International Commission on Illumination (CIE), Vienna, Austria, DOI: 10.25039/CIE.DS.hjfjmt59
+ */
+ public static final Spectrum D50 = read("CIE_std_illum_D50.csv", true);
+ /**
+ * the CIE standard illuminant D65
+ * @see 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
+ */
+ public static final Spectrum D65 = read("CIE_std_illum_D65.csv", true);
+
+ private static @NotNull Spectrum read(@NotNull String path, boolean normalize) {
+ var lambda = new ArrayListthis
spectrum over the range of wavelengths}
+ */
+ double max();
+
+ /**
+ * {@return the value of this
spectrum at a given wavelength}
+ * @param lambda the wavelength in nanometers
+ */
+ double get(double lambda);
+
+ 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 Color toRGB(@NotNull ColorSpace cs) {
+ return cs.toRGB(toXYZ());
+ }
+
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Util.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Util.java
new file mode 100644
index 0000000..783e3e0
--- /dev/null
+++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Util.java
@@ -0,0 +1,17 @@
+package eu.jonahbauer.raytracing.render.spectral.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;
+ }
+}
diff --git a/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java b/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java
index b7d1672..bfebde3 100644
--- a/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java
+++ b/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java
@@ -5,6 +5,7 @@ import eu.jonahbauer.raytracing.math.Vec3;
import eu.jonahbauer.raytracing.scene.SkyBox;
import org.jetbrains.annotations.NotNull;
+import java.util.Objects;
import java.util.Random;
import static eu.jonahbauer.raytracing.Main.DEBUG;
@@ -20,9 +21,9 @@ public record Color(double r, double g, double b) implements Texture, SkyBox {
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
+ Math.fma(t, b.r, Math.fma(-t, a.r, a.r)),
+ Math.fma(t, b.g, Math.fma(-t, a.g, a.g)),
+ Math.fma(t, b.b, Math.fma(-t, a.b, a.b))
);
}
@@ -86,6 +87,10 @@ public record Color(double r, double g, double b) implements Texture, SkyBox {
this(red / 255f, green / 255f, blue / 255f);
}
+ public Color(@NotNull Vec3 vec) {
+ this(vec.x(), vec.y(), vec.z());
+ }
+
public Color {
if (DEBUG) {
if (!Double.isFinite(r) || !Double.isFinite(g) || !Double.isFinite(b)) {
@@ -109,6 +114,15 @@ public record Color(double r, double g, double b) implements Texture, SkyBox {
return toInt(b);
}
+ public double component(int i) {
+ return switch (i) {
+ case 0 -> r;
+ case 1 -> g;
+ case 2 -> b;
+ default -> throw new IndexOutOfBoundsException(i);
+ };
+ }
+
@Override
public @NotNull Color get(double u, double v, @NotNull Vec3 p) {
return this;
@@ -124,6 +138,10 @@ public record Color(double r, double g, double b) implements Texture, SkyBox {
return false;
}
+ public @NotNull Vec3 toVec3() {
+ return new Vec3(r, g, b);
+ }
+
private static int toInt(double value) {
return Math.clamp((int) (255.99 * value), 0, 255);
}
diff --git a/src/main/resources/eu/jonahbauer/raytracing/colorspace/DCI_P3_spectrum.bin b/src/main/resources/eu/jonahbauer/raytracing/colorspace/DCI_P3_spectrum.bin
new file mode 100644
index 0000000..9a709bb
Binary files /dev/null and b/src/main/resources/eu/jonahbauer/raytracing/colorspace/DCI_P3_spectrum.bin differ
diff --git a/src/main/resources/eu/jonahbauer/raytracing/colorspace/Rec2020_spectrum.bin b/src/main/resources/eu/jonahbauer/raytracing/colorspace/Rec2020_spectrum.bin
new file mode 100644
index 0000000..94d8c36
Binary files /dev/null and b/src/main/resources/eu/jonahbauer/raytracing/colorspace/Rec2020_spectrum.bin differ
diff --git a/src/main/resources/eu/jonahbauer/raytracing/colorspace/sRGB_spectrum.bin b/src/main/resources/eu/jonahbauer/raytracing/colorspace/sRGB_spectrum.bin
new file mode 100644
index 0000000..f0f9750
Binary files /dev/null and b/src/main/resources/eu/jonahbauer/raytracing/colorspace/sRGB_spectrum.bin differ
diff --git a/src/main/resources/eu/jonahbauer/raytracing/spectrum/CIE_std_illum_D50.csv b/src/main/resources/eu/jonahbauer/raytracing/spectrum/CIE_std_illum_D50.csv
new file mode 100644
index 0000000..6469668
--- /dev/null
+++ b/src/main/resources/eu/jonahbauer/raytracing/spectrum/CIE_std_illum_D50.csv
@@ -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
diff --git a/src/main/resources/eu/jonahbauer/raytracing/spectrum/CIE_std_illum_D65.csv b/src/main/resources/eu/jonahbauer/raytracing/spectrum/CIE_std_illum_D65.csv
new file mode 100644
index 0000000..b6c2c7f
--- /dev/null
+++ b/src/main/resources/eu/jonahbauer/raytracing/spectrum/CIE_std_illum_D65.csv
@@ -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
diff --git a/src/main/resources/earthmap.jpg b/src/main/resources/eu/jonahbauer/raytracing/textures/earthmap.jpg
similarity index 100%
rename from src/main/resources/earthmap.jpg
rename to src/main/resources/eu/jonahbauer/raytracing/textures/earthmap.jpg