diff --git a/src/main/java/eu/jonahbauer/raytracing/Examples.java b/src/main/java/eu/jonahbauer/raytracing/Examples.java index bb545d2..f2a89ed 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Examples.java +++ b/src/main/java/eu/jonahbauer/raytracing/Examples.java @@ -82,7 +82,7 @@ public class Examples { var rnd = rng.nextDouble(); if (rnd < 0.8) { // diffuse - var albedo = Color.multiply(Color.random(rng), Color.random(rng)); + var albedo = Color.random(rng).times(Color.random(rng)); material = new LambertianMaterial(albedo); } else if (rnd < 0.95) { // metal diff --git a/src/main/java/eu/jonahbauer/raytracing/math/IVec.java b/src/main/java/eu/jonahbauer/raytracing/math/IVec.java new file mode 100644 index 0000000..0aad0da --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/math/IVec.java @@ -0,0 +1,21 @@ +package eu.jonahbauer.raytracing.math; + +import org.jetbrains.annotations.NotNull; + +/** + * A vector-like object that implements the standard mathematical operations + * @param the type + */ +public interface IVec> { + double get(int i); + + @NotNull T plus(@NotNull T other); + @NotNull T minus(@NotNull T other); + @NotNull T times(@NotNull T other); + @NotNull T times(double d); + default @NotNull T div(double d) { + return times(1 / d); + } + + double @NotNull[] toArray(); +} diff --git a/src/main/java/eu/jonahbauer/raytracing/math/IVec3.java b/src/main/java/eu/jonahbauer/raytracing/math/IVec3.java new file mode 100644 index 0000000..c1bff7f --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/math/IVec3.java @@ -0,0 +1,36 @@ +package eu.jonahbauer.raytracing.math; + +import org.jetbrains.annotations.NotNull; + +/** + * A vector-like object with three components. + * @param the type + */ +public interface IVec3> extends IVec { + default double component1() { + return toVec3().x(); + } + default double component2() { + return toVec3().y(); + } + default double component3() { + return toVec3().z(); + } + + @Override + default double get(int i) { + return switch (i) { + case 0 -> component1(); + case 1 -> component2(); + case 2 -> component3(); + default -> throw new IndexOutOfBoundsException(i); + }; + } + + @NotNull Vec3 toVec3(); + + @Override + default double @NotNull [] toArray() { + return new double[] {component1(), component2(), component3()}; + } +} diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java b/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java index 20b8c4c..0ae4512 100644 --- a/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java +++ b/src/main/java/eu/jonahbauer/raytracing/math/Vec3.java @@ -7,10 +7,8 @@ import java.util.random.RandomGenerator; 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 { 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_Y = new Vec3(0, 1, 0); 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 */ 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); } @@ -75,7 +73,7 @@ public record Vec3(double x, double y, double z) { */ public static @NotNull Optional refract(@NotNull Vec3 vec, @NotNull Vec3 normal, double ri) { 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); if (ri * sinTheta > 1) return Optional.empty(); @@ -166,6 +164,10 @@ public record Vec3(double x, double y, double z) { 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) { return new Vec3(this.x + x, this.y + y, this.z + z); } @@ -179,6 +181,7 @@ public record Vec3(double x, double y, double z) { * @param other a vector * @return the sum of this and the other vector */ + @Override public @NotNull Vec3 plus(@NotNull Vec3 other) { return new Vec3(this.x + other.x, this.y + other.y, this.z + other.z); } @@ -188,49 +191,58 @@ public record Vec3(double x, double y, double z) { * @param other a vector * @return the difference of this and the other vector */ + @Override public @NotNull Vec3 minus(@NotNull Vec3 other) { 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 * @param t a scalar * @return the product of this vector and the scalar */ + @Override public @NotNull Vec3 times(double t) { return new Vec3(this.x * t, this.y * t, this.z * t); } /** - * Negates this vector. - * {@return the negated vector} + * Multiplies this vector with another vector component-wise. + * @param other a vector + * @return the component-wise product of this vector and the other vector */ - public @NotNull Vec3 neg() { - return new Vec3(-x, -y, -z); + @Override + 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. - * @return the inverted vector. + * Divides this vector by a scalar + * @param t a scalar + * @return this vector divided by the scalar */ - public @NotNull Vec3 inv() { - return new Vec3(1 / x, 1 / y, 1 / z); + @Override + 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 - * @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) { return new Vec3( Math.fma(this.y, other.z, - other.y * this.z), @@ -239,15 +251,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} */ @@ -279,17 +282,28 @@ public record Vec3(double x, double y, double z) { return div(Math.sqrt(squared)); } - /** - * {@return the n-th component of this vector} - * @param axis the component index + /* + * Accessors */ - public double get(int axis) { - return switch (axis) { - case 0 -> x; - case 1 -> y; - case 2 -> z; - default -> throw new IndexOutOfBoundsException(axis); - }; + + @Override + public double component1() { + return x; + } + + @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) { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java index 8667966..4710c2b 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java @@ -24,7 +24,7 @@ public record DielectricMaterial(double refractionIndex, @NotNull Texture textur public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { var ri = hit.isFrontFace() ? (1 / refractionIndex) : refractionIndex; - 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 reflect = reflectance > random.nextDouble(); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java index fc29fec..fc9270d 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java @@ -1,6 +1,5 @@ package eu.jonahbauer.raytracing.render.renderer; -import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.renderer.pdf.TargetingProbabilityDensityFunction; @@ -133,7 +132,7 @@ public final class SimpleRenderer implements Renderer { var optional = scene.hit(ray); if (optional.isEmpty()) { var background = scene.getBackgroundColor(ray); - color = Color.add(color, Color.multiply(attenuation, background)); + color = Color.fma(attenuation, background, color); if (DEBUG) { System.out.println(" Hit background: " + background); } @@ -151,7 +150,7 @@ public final class SimpleRenderer implements Renderer { } var result = material.scatter(ray, hit, random); - color = Color.add(color, Color.multiply(attenuation, emitted)); + color = Color.fma(attenuation, emitted, color); if (result.isEmpty()) { if (DEBUG) { @@ -162,7 +161,7 @@ public final class SimpleRenderer implements Renderer { switch (result.get()) { case Material.SpecularScatterResult(var a, var scattered) -> { - attenuation = Color.multiply(attenuation, a); + attenuation = attenuation.times(a); ray = scattered; if (DEBUG) { @@ -171,7 +170,7 @@ public final class SimpleRenderer implements Renderer { } case Material.PdfScatterResult(var a, var pdf) -> { if (scene.getTargets() == null) { - attenuation = Color.multiply(attenuation, a); + attenuation = attenuation.times(a); ray = new Ray(hit.position(), pdf.generate(random)); if (DEBUG) { @@ -187,7 +186,7 @@ public final class SimpleRenderer implements Renderer { var factor = idealPdf / actualPdf; - attenuation = Color.multiply(attenuation, Color.multiply(a, factor)); + attenuation = attenuation.times(a.times(factor)); ray = new Ray(hit.position(), direction); if (DEBUG) { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/CosineProbabilityDensityFunction.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/CosineProbabilityDensityFunction.java index 6dfe12c..e93603b 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/CosineProbabilityDensityFunction.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/pdf/CosineProbabilityDensityFunction.java @@ -15,7 +15,7 @@ public record CosineProbabilityDensityFunction(@NotNull Vec3 normal) implements @Override public double value(@NotNull Vec3 direction) { - var cos = normal.times(direction); + var cos = normal.dot(direction); return Math.max(0, cos / Math.PI); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java index 8566fa8..a7f0916 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java @@ -1,12 +1,25 @@ package eu.jonahbauer.raytracing.render.spectral; +import eu.jonahbauer.raytracing.math.IVec; import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ; import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import eu.jonahbauer.raytracing.render.texture.Color; import org.jetbrains.annotations.NotNull; -public final class SampledSpectrum { +import java.util.Arrays; + +public final class SampledSpectrum implements IVec { + public static final SampledSpectrum BLACK; + public static final SampledSpectrum WHITE; + + static { + BLACK = new SampledSpectrum(new double[SampledWavelengths.SAMPLES]); + var one = new double[SampledWavelengths.SAMPLES]; + Arrays.fill(one, 1); + WHITE = new SampledSpectrum(one); + } + private final double @NotNull[] values; public SampledSpectrum(@NotNull SampledWavelengths lambdas, @NotNull Spectrum spectrum) { @@ -21,30 +34,74 @@ public final class SampledSpectrum { this.values = values; } - public static @NotNull SampledSpectrum multiply(@NotNull SampledSpectrum a, @NotNull SampledSpectrum b) { + /* + * 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] = a.values[i] * b.values[i]; + out[i] = Math.fma(a.values[i], b.values[i], c.values[i]); } return new SampledSpectrum(out); } - public static @NotNull SampledSpectrum multiply(@NotNull SampledSpectrum a, double b) { + 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] = a.values[i] * b; + out[i] = Math.fma(t, b.values[i] - a.values[i], a.values[i]); } 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]; + @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]; } @@ -53,14 +110,29 @@ public final class SampledSpectrum { 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; + /* + * 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); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledWavelengths.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledWavelengths.java index 8b40b56..01df5cf 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledWavelengths.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledWavelengths.java @@ -12,6 +12,7 @@ import java.util.Arrays; */ public final class SampledWavelengths { public static final int SAMPLES = 4; + public static final SampledWavelengths EMPTY = new SampledWavelengths(new double[0], new double[0]); private final double @NotNull[] lambdas; private final double @NotNull[] pdf; @@ -57,12 +58,37 @@ public final class SampledWavelengths { * 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; + public @NotNull SampledWavelengths collapse() { + if (pdf.length < 2 || pdf[1] == 0) return this; + var newPdf = Arrays.copyOf(pdf, pdf.length); + Arrays.fill(newPdf, 1, newPdf.length, 0d); + newPdf[0] /= newPdf.length; + return new SampledWavelengths(lambdas, newPdf); } + /* + * 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); 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 index a71f759..3e14137 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorSpace.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorSpace.java @@ -62,7 +62,7 @@ public final class ColorSpace { public @NotNull ColorXYZ toXYZ(@NotNull Color rgb) { var out = XYZfromRGB.times(rgb.toVec3()); - return new ColorXYZ(out); + return ColorXYZ.fromVec3(out); } public @NotNull Vec3 toCIELab(@NotNull Color rgb) { 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 index eb0147b..c015e1b 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorXYZ.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorXYZ.java @@ -1,14 +1,18 @@ package eu.jonahbauer.raytracing.render.spectral.colors; +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) { +public record ColorXYZ(double x, double y, double z) implements IVec3 { 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); } @@ -21,10 +25,69 @@ public record ColorXYZ(double x, double y, double z) { ); } - public ColorXYZ(@NotNull Vec3 vec) { - this(vec.x(), vec.y(), vec.z()); + /* + * 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); + } + + /* + * Vec3 + */ + + @Override + public @NotNull Vec3 toVec3() { + return new Vec3(x, y, z); + } + + public static @NotNull ColorXYZ fromVec3(@NotNull Vec3 vec) { + return new ColorXYZ(vec.x(), vec.y(), vec.z()); + } + + /* + * Accessors + */ + public double average() { return (x + y + z) / 3; } @@ -34,19 +97,18 @@ public record ColorXYZ(double x, double y, double 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); + @Override + public double component1() { + return x; } - public static @NotNull ColorXYZ multiply(@NotNull ColorXYZ a, double b) { - return new ColorXYZ(a.x * b, a.y * b, a.z * b); + @Override + public double component2() { + return y; } - 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); + @Override + public double component3() { + return z; } } 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 index e573d07..bbfb0e2 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTable.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTable.java @@ -97,9 +97,9 @@ public final class SpectrumTable { ? (color.r() > color.b() ? 0 : 2) : (color.g() > color.b() ? 1 : 2); - var z = color.component(max); - var x = color.component((max + 1) % 3) * (resolution - 1) / z; - var y = color.component((max + 2) % 3) * (resolution - 1) / z; + 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); 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 index 9ad0421..4358c3f 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBIlluminantSpectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBIlluminantSpectrum.java @@ -20,7 +20,7 @@ public final class RGBIlluminantSpectrum implements Spectrum { } 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.polynomial = cs.toSpectrum(scale == 0 ? rgb.div(scale) : Color.BLACK); this.illuminant = cs.illuminant(); } 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 index d98654e..46a5893 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBUnboundedSpectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBUnboundedSpectrum.java @@ -15,7 +15,7 @@ public final class RGBUnboundedSpectrum implements Spectrum { } 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.polynomial = cs.toSpectrum(scale == 0 ? rgb.div(scale) : Color.BLACK); } @Override 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 bfebde3..fe0465d 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java @@ -1,43 +1,18 @@ package eu.jonahbauer.raytracing.render.texture; +import eu.jonahbauer.raytracing.math.IVec3; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.scene.SkyBox; import org.jetbrains.annotations.NotNull; -import java.util.Objects; import java.util.Random; import static eu.jonahbauer.raytracing.Main.DEBUG; -public record Color(double r, double g, double b) implements Texture, SkyBox { +public record Color(double r, double g, double b) implements Texture, SkyBox, IVec3 { 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( - 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)) - ); - } - - 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()); @@ -52,15 +27,6 @@ public record Color(double r, double g, double b) implements Texture, SkyBox { ); } - 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; @@ -87,21 +53,75 @@ 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)) { - 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"); - } + if (DEBUG && (!Double.isFinite(r) || !Double.isFinite(g) || !Double.isFinite(b))) { + throw new IllegalArgumentException("r, g and b must be finite"); } } + /* + * Math + */ + + public static @NotNull Color average(@NotNull Color current, @NotNull Color next, int index) { + return lerp(current, next, 1d / index); + } + + public static @NotNull Color lerp(@NotNull Color a, @NotNull Color b, double t) { + if (t < 0) return a; + if (t > 1) return b; + return new Color( + Math.fma(t, b.r - a.r, a.r), + Math.fma(t, b.g - a.g, a.g), + Math.fma(t, b.b - a.b, a.b) + ); + } + + public static @NotNull Color fma(@NotNull Color a, @NotNull Color b, @NotNull Color c) { + return new Color( + Math.fma(a.r, b.r, c.r), + Math.fma(a.g, b.g, c.g), + Math.fma(a.b, b.b, c.b) + ); + } + + @Override + public @NotNull Color plus(@NotNull Color other) { + return new Color(r + other.r, g + other.g, b + other.b); + } + + @Override + public @NotNull Color minus(@NotNull Color other) { + return new Color(r - other.r, g - other.g, b - other.b); + } + + @Override + public @NotNull Color times(double d) { + return new Color(r * d, g * d, b * d); + } + + @Override + public @NotNull Color times(@NotNull Color other) { + return new Color(r * other.r, g * other.g, b * other.b); + } + + /* + * Vec3 + */ + + @Override + public @NotNull Vec3 toVec3() { + return new Vec3(r, g, b); + } + + public static @NotNull Color fromVec3(@NotNull Vec3 vec) { + return new Color(vec.x(), vec.y(), vec.z()); + } + + /* + * Accessors + */ + public int red() { return toInt(r); } @@ -114,22 +134,31 @@ 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); - }; + private static int toInt(double value) { + return Math.clamp((int) (255.99 * value), 0, 255); + } + + @Override + public double component1() { + return r; } @Override - public @NotNull Color get(double u, double v, @NotNull Vec3 p) { - return this; + public double component2() { + return g; } @Override - public @NotNull Color getColor(@NotNull Ray ray) { + public double component3() { + return b; + } + + /* + * Texture + */ + + @Override + public @NotNull Color get(double u, double v, @NotNull Vec3 p) { return this; } @@ -138,11 +167,12 @@ public record Color(double r, double g, double b) implements Texture, SkyBox { return false; } - public @NotNull Vec3 toVec3() { - return new Vec3(r, g, b); - } + /* + * SkyBox + */ - private static int toInt(double value) { - return Math.clamp((int) (255.99 * value), 0, 255); + @Override + public @NotNull Color getColor(@NotNull Ray ray) { + return this; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java b/src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java index ee22d30..55e2de6 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java @@ -12,7 +12,7 @@ public interface SkyBox { return ray -> { // altitude from -pi/2 to pi/2 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() ); return Color.lerp(bottom, top, alt / Math.PI + 0.5); diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java index d0cb42b..9fe384e 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable2d/Hittable2D.java @@ -31,23 +31,23 @@ public abstract class Hittable2D implements Hittable { var n = u.cross(v); if (n.squared() < 1e-8) throw new IllegalArgumentException(); this.normal = n.unit(); - this.d = origin.times(normal); + this.d = origin.dot(normal); this.w = n.div(n.squared()); } @Override public @NotNull Optional hit(@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 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(); var position = ray.at(t); var p = position.minus(origin); - var alpha = w.times(p.cross(v)); - var beta = w.times(u.cross(p)); + var alpha = w.dot(p.cross(v)); + var beta = w.dot(u.cross(p)); if (!isInterior(alpha, beta)) return Optional.empty(); var frontFace = denominator < 0; @@ -58,10 +58,10 @@ public abstract class Hittable2D implements Hittable { } 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 - var t = (d - ray.origin().times(normal)) / denominator; + var t = (d - ray.origin().dot(normal)) / denominator; if (!range.surrounds(t)) return Double.NaN; var position = ray.at(t); diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java index e10da56..26101b4 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/hittable3d/Sphere.java @@ -46,7 +46,7 @@ public final class Sphere implements Hittable, Target { var position = ray.at(t); var normal = Vec3.fma(invRadius, position, normalizedCenter); - var frontFace = normal.times(ray.direction()) < 0; + var frontFace = normal.dot(ray.direction()) < 0; double u; double v; @@ -70,7 +70,7 @@ public final class Sphere implements Hittable, Target { var oc = ray.origin().minus(center); var a = ray.direction().squared(); - var h = ray.direction().times(oc); + var h = ray.direction().dot(oc); var c = oc.squared() - radius * radius; var discriminant = h * h - a * c; diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/util/PdfUtil.java b/src/main/java/eu/jonahbauer/raytracing/scene/util/PdfUtil.java index 739f06a..13ef8a9 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/util/PdfUtil.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/util/PdfUtil.java @@ -13,7 +13,7 @@ public final class PdfUtil { * must be unit vectors. */ 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; } } diff --git a/src/test/java/eu/jonahbauer/raytracing/math/Vec3Test.java b/src/test/java/eu/jonahbauer/raytracing/math/Vec3Test.java index 31c5220..7788d4a 100644 --- a/src/test/java/eu/jonahbauer/raytracing/math/Vec3Test.java +++ b/src/test/java/eu/jonahbauer/raytracing/math/Vec3Test.java @@ -21,10 +21,10 @@ class Vec3Test { } @Test - void timesVec() { + void dotVec() { var a = new Vec3(1, 2, 3); var b = new Vec3(-1, 1, -2); - assertEquals(-5, a.times(b)); + assertEquals(-5, a.dot(b)); } @Test