From 00fbf4e4f1be8594f430cd6772cc43f9fcbea4b6 Mon Sep 17 00:00:00 2001 From: jbb01 <32650546+jbb01@users.noreply.github.com> Date: Sun, 11 Aug 2024 16:32:40 +0200 Subject: [PATCH] adjust rendering pipeline for spectral rendering --- .../eu/jonahbauer/raytracing/Examples.java | 129 +++++++------ .../java/eu/jonahbauer/raytracing/Main.java | 9 +- .../eu/jonahbauer/raytracing/math/Ray.java | 8 +- .../raytracing/render/ImageFormat.java | 37 ++-- .../render/camera/SimpleCamera.java | 3 +- .../raytracing/render/canvas/Canvas.java | 39 ++-- .../raytracing/render/canvas/Image.java | 58 ------ .../raytracing/render/canvas/LiveCanvas.java | 18 +- .../raytracing/render/canvas/RGBCanvas.java | 76 ++++++++ .../raytracing/render/canvas/XYZCanvas.java | 74 ++++++++ .../render/material/DielectricMaterial.java | 13 +- .../render/material/DiffuseLight.java | 16 +- .../render/material/DirectionalMaterial.java | 9 +- .../render/material/IsotropicMaterial.java | 16 +- .../render/material/LambertianMaterial.java | 7 + .../raytracing/render/material/Material.java | 14 +- .../render/material/MetallicMaterial.java | 13 +- .../raytracing/render/renderer/Renderer.java | 6 +- .../render/renderer/SimpleRenderer.java | 45 ++--- .../render/spectral/SampledSpectrum.java | 5 +- .../render/spectral/colors/ColorRGB.java | 153 +++++++++++++++ .../render/spectral/colors/ColorSpace.java | 13 +- .../render/spectral/colors/SpectrumTable.java | 3 +- .../colors/SpectrumTableGenerator.java | 13 +- .../spectrum/DenselySampledSpectrum.java | 8 - .../spectrum/PiecewiseLinearSpectrum.java | 10 - .../spectral/spectrum/RGBAlbedoSpectrum.java | 4 +- .../spectrum/RGBIlluminantSpectrum.java | 6 +- .../spectrum/RGBUnboundedSpectrum.java | 6 +- .../spectral/spectrum/ScaledSpectrum.java | 15 ++ .../render/spectral/spectrum/Spectra.java | 3 + .../render/spectral/spectrum/Spectrum.java | 42 ++++- .../render/texture/CheckerTexture.java | 3 +- .../raytracing/render/texture/Color.java | 178 ------------------ .../render/texture/ImageTexture.java | 61 ++++-- .../render/texture/PerlinTexture.java | 12 +- .../raytracing/render/texture/Texture.java | 5 +- .../eu/jonahbauer/raytracing/scene/Scene.java | 7 +- .../jonahbauer/raytracing/scene/SkyBox.java | 14 +- .../raytracing/scene/transform/RotateY.java | 2 +- .../raytracing/scene/transform/Translate.java | 2 +- .../{ImageTest.java => RGBCanvasTest.java} | 10 +- .../scene/hittable3d/SphereTest.java | 4 +- 43 files changed, 704 insertions(+), 465 deletions(-) delete mode 100644 src/main/java/eu/jonahbauer/raytracing/render/canvas/Image.java create mode 100644 src/main/java/eu/jonahbauer/raytracing/render/canvas/RGBCanvas.java create mode 100644 src/main/java/eu/jonahbauer/raytracing/render/canvas/XYZCanvas.java create mode 100644 src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorRGB.java create mode 100644 src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/ScaledSpectrum.java delete mode 100644 src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java rename src/test/java/eu/jonahbauer/raytracing/render/canvas/{ImageTest.java => RGBCanvasTest.java} (79%) diff --git a/src/main/java/eu/jonahbauer/raytracing/Examples.java b/src/main/java/eu/jonahbauer/raytracing/Examples.java index f2a89ed..805ff35 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Examples.java +++ b/src/main/java/eu/jonahbauer/raytracing/Examples.java @@ -1,8 +1,11 @@ package eu.jonahbauer.raytracing; import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpaces; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBIlluminantSpectrum; import eu.jonahbauer.raytracing.render.texture.CheckerTexture; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; import eu.jonahbauer.raytracing.render.camera.SimpleCamera; import eu.jonahbauer.raytracing.render.material.*; import eu.jonahbauer.raytracing.render.texture.ImageTexture; @@ -49,13 +52,14 @@ public class Examples { public static @NotNull Example getSimpleScene(int height) { if (height <= 0) height = 675; + var cs = ColorSpaces.sRGB; return new Example( new Scene(getSkyBox(), List.of( - new Sphere(new Vec3(0, -100.5, -1.0), 100.0, new LambertianMaterial(new Color(0.8, 0.8, 0.0))), - new Sphere(new Vec3(0, 0, -1.2), 0.5, new LambertianMaterial(new Color(0.1, 0.2, 0.5))), + new Sphere(new Vec3(0, -100.5, -1.0), 100.0, new LambertianMaterial(new ColorRGB(0.8, 0.8, 0.0), cs)), + new Sphere(new Vec3(0, 0, -1.2), 0.5, new LambertianMaterial(new ColorRGB(0.1, 0.2, 0.5), cs)), new Sphere(new Vec3(-1.0, 0, -1.2), 0.5, new DielectricMaterial(1.5)), new Sphere(new Vec3(-1.0, 0, -1.2), 0.4, new DielectricMaterial(1 / 1.5)), - new Sphere(new Vec3(1.0, 0, -1.2), 0.5, new MetallicMaterial(new Color(0.8, 0.6, 0.2), 0.0)) + new Sphere(new Vec3(1.0, 0, -1.2), 0.5, new MetallicMaterial(new ColorRGB(0.8, 0.6, 0.2), cs, 0.0)) )), SimpleCamera.builder() .withImage(height * 16 / 9, height) @@ -66,11 +70,16 @@ public class Examples { public static @NotNull Example getSpheres(int height) { if (height <= 0) height = 675; + var cs = ColorSpaces.sRGB; + var rng = new Random(1); var objects = new ArrayList(); objects.add(new Sphere( new Vec3(0, -1000, 0), 1000, - new LambertianMaterial(new CheckerTexture(0.32, new Color(.2, .3, .1), new Color(.9, .9, .9))) + new LambertianMaterial(new CheckerTexture(0.32, + new RGBAlbedoSpectrum(cs, new ColorRGB(.2, .3, .1)), + new RGBAlbedoSpectrum(cs, new ColorRGB(.9, .9, .9)) + )) )); for (int a = -11; a < 11; a++) { @@ -82,13 +91,13 @@ public class Examples { var rnd = rng.nextDouble(); if (rnd < 0.8) { // diffuse - var albedo = Color.random(rng).times(Color.random(rng)); - material = new LambertianMaterial(albedo); + var albedo = ColorRGB.random(rng).times(ColorRGB.random(rng)); + material = new LambertianMaterial(albedo, cs); } else if (rnd < 0.95) { // metal - var albedo = Color.random(rng, 0.5, 1.0); + var albedo = ColorRGB.random(rng, 0.5, 1.0); var fuzz = rng.nextDouble() * 0.5; - material = new MetallicMaterial(albedo, fuzz); + material = new MetallicMaterial(albedo, cs, fuzz); } else { // glass material = new DielectricMaterial(1.5); @@ -99,8 +108,8 @@ public class Examples { } objects.add(new Sphere(new Vec3(0, 1, 0), 1.0, new DielectricMaterial(1.5))); - objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new Color(0.4, 0.2, 0.1)))); - objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new Color(0.7, 0.6, 0.5)))); + objects.add(new Sphere(new Vec3(-4, 1, 0), 1.0, new LambertianMaterial(new ColorRGB(0.4, 0.2, 0.1), cs))); + objects.add(new Sphere(new Vec3(4, 1, 0), 1.0, new MetallicMaterial(new ColorRGB(0.7, 0.6, 0.5), cs))); var camera = SimpleCamera.builder() .withImage(height * 16 / 9, height) @@ -116,13 +125,14 @@ public class Examples { public static @NotNull Example getSquares(int height) { if (height <= 0) height = 600; + var cs = ColorSpaces.sRGB; return new Example( new Scene(getSkyBox(), List.of( - new Parallelogram(new Vec3(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(1.0, 0.2, 0.2))), - new Parallelogram(new Vec3(-2, -2, 0), new Vec3(4, 0, 0), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 1.0, 0.2))), - new Parallelogram(new Vec3(3, -2, 1), new Vec3(0, 0, 4), new Vec3(0, 4, 0), new LambertianMaterial(new Color(0.2, 0.2, 1.0))), - new Parallelogram(new Vec3(-2, 3, 1), new Vec3(4, 0, 0), new Vec3(0, 0, 4), new LambertianMaterial(new Color(1.0, 0.5, 0.0))), - new Parallelogram(new Vec3(-2, -3, 5), new Vec3(4, 0, 0), new Vec3(0, 0, -4), new LambertianMaterial(new Color(0.2, 0.8, 0.8))) + new Parallelogram(new Vec3(-3, -2, 5), new Vec3(0, 0, -4), new Vec3(0, 4, 0), new LambertianMaterial(new ColorRGB(1.0, 0.2, 0.2), cs)), + new Parallelogram(new Vec3(-2, -2, 0), new Vec3(4, 0, 0), new Vec3(0, 4, 0), new LambertianMaterial(new ColorRGB(0.2, 1.0, 0.2), cs)), + new Parallelogram(new Vec3(3, -2, 1), new Vec3(0, 0, 4), new Vec3(0, 4, 0), new LambertianMaterial(new ColorRGB(0.2, 0.2, 1.0), cs)), + new Parallelogram(new Vec3(-2, 3, 1), new Vec3(4, 0, 0), new Vec3(0, 0, 4), new LambertianMaterial(new ColorRGB(1.0, 0.5, 0.0), cs)), + new Parallelogram(new Vec3(-2, -3, 5), new Vec3(4, 0, 0), new Vec3(0, 0, -4), new LambertianMaterial(new ColorRGB(0.2, 0.8, 0.8), cs)) )), SimpleCamera.builder() .withImage(height, height) @@ -135,12 +145,13 @@ public class Examples { public static @NotNull Example getLight(int height) { if (height <= 0) height = 225; + var cs = ColorSpaces.sRGB; return new Example( new Scene(List.of( - new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new Color(0.2, 0.2, 0.2))), - new Sphere(new Vec3(0, 2, 0), 2, new LambertianMaterial(new Color(0.2, 0.2, 0.2))), - new Parallelogram(new Vec3(3, 1, -2), new Vec3(2, 0, 0), new Vec3(0, 2, 0), new DiffuseLight(new Color(4.0, 4.0, 4.0))), - new Sphere(new Vec3(0, 7, 0), 2, new DiffuseLight(new Color(4.0, 4.0, 4.0))) + new Sphere(new Vec3(0, -1000, 0), 1000, new LambertianMaterial(new ColorRGB(0.2, 0.2, 0.2), cs)), + new Sphere(new Vec3(0, 2, 0), 2, new LambertianMaterial(new ColorRGB(0.2, 0.2, 0.2), cs)), + new Parallelogram(new Vec3(3, 1, -2), new Vec3(2, 0, 0), new Vec3(0, 2, 0), new DiffuseLight(new ColorRGB(4.0, 4.0, 4.0), cs)), + new Sphere(new Vec3(0, 7, 0), 2, new DiffuseLight(new ColorRGB(4.0, 4.0, 4.0), cs)) )), SimpleCamera.builder() .withImage(height * 16 / 9, height) @@ -154,10 +165,11 @@ public class Examples { public static @NotNull Example getCornellBox(int height) { if (height <= 0) height = 600; - var red = new LambertianMaterial(new Color(.65, .05, .05)); - var white = new LambertianMaterial(new Color(.73, .73, .73)); - var green = new LambertianMaterial(new Color(.12, .45, .15)); - var light = new DiffuseLight(new Color(15.0, 15.0, 15.0)); + var cs = ColorSpaces.sRGB; + var red = new LambertianMaterial(new ColorRGB(.65, .05, .05), cs); + var white = new LambertianMaterial(new ColorRGB(.73, .73, .73), cs); + var green = new LambertianMaterial(new ColorRGB(.12, .45, .15), cs); + var light = new DiffuseLight(new ColorRGB(15.0, 15.0, 15.0), cs); return new Example( new Scene(List.of( @@ -181,10 +193,11 @@ public class Examples { public static @NotNull Example getCornellBoxSmoke(int height) { if (height <= 0) height = 600; - var red = new LambertianMaterial(new Color(.65, .05, .05)); - var white = new LambertianMaterial(new Color(.73, .73, .73)); - var green = new LambertianMaterial(new Color(.12, .45, .15)); - var light = new DiffuseLight(new Color(7.0, 7.0, 7.0)); + var cs = ColorSpaces.sRGB; + var red = new LambertianMaterial(new ColorRGB(.65, .05, .05), cs); + var white = new LambertianMaterial(new ColorRGB(.73, .73, .73), cs); + var green = new LambertianMaterial(new ColorRGB(.12, .45, .15), cs); + var light = new DiffuseLight(new ColorRGB(15.0, 15.0, 15.0), cs); return new Example( new Scene(List.of( @@ -194,13 +207,13 @@ public class Examples { new Box(new Vec3(0, 0, 0), new Vec3(165, 330, 165), white) .rotateY(Math.toRadians(15)) .translate(new Vec3(265, 0, 295)), - 0.01, new IsotropicMaterial(Color.BLACK) + 0.01, new IsotropicMaterial(ColorRGB.BLACK, cs) ), new ConstantMedium( new Box(new Vec3(0, 0, 0), new Vec3(165, 165, 165), white) .rotateY(Math.toRadians(-18)) .translate(new Vec3(130, 0, 65)), - 0.01, new IsotropicMaterial(Color.WHITE) + 0.01, new IsotropicMaterial(ColorRGB.WHITE, cs) ) )), SimpleCamera.builder() @@ -215,10 +228,11 @@ public class Examples { public static @NotNull Example getCornellBoxSphere(int height) { if (height <= 0) height = 600; - var red = new LambertianMaterial(new Color(.65, .05, .05)); - var white = new LambertianMaterial(new Color(.73, .73, .73)); - var green = new LambertianMaterial(new Color(.12, .45, .15)); - var light = new DiffuseLight(new Color(7.0, 7.0, 7.0)); + var cs = ColorSpaces.sRGB; + var red = new LambertianMaterial(new ColorRGB(.65, .05, .05), cs); + var white = new LambertianMaterial(new ColorRGB(.73, .73, .73), cs); + var green = new LambertianMaterial(new ColorRGB(.12, .45, .15), cs); + var light = new DiffuseLight(new ColorRGB(7.0, 7.0, 7.0), cs); var glass = new DielectricMaterial(1.5); var room = new Box(new Vec3(0, 0, 0), new Vec3(555, 555, 555), white, white, red, green, white, null); @@ -242,17 +256,18 @@ public class Examples { public static @NotNull Example getDiagramm(int height) { if (height <= 0) height = 450; - record Partei(String name, Color color, double stimmen) { } + var cs = ColorSpaces.sRGB; + record Partei(String name, ColorRGB color, double stimmen) { } var data = List.of( - new Partei("CDU", new Color(0x00, 0x4B, 0x76), 18.9), - new Partei("SPD", new Color(0xC0, 0x00, 0x3C), 25.7), - new Partei("AfD", new Color(0x80, 0xCD, 0xEC), 10.3), - new Partei("FDP", new Color(0xF7, 0xBB, 0x3D), 11.5), - new Partei("DIE LINKE", new Color(0x5F, 0x31, 0x6E), 4.9), - new Partei("GRÜNE", new Color(0x00, 0x85, 0x4A), 14.8), - new Partei("CSU", new Color(0x00, 0x77, 0xB6), 5.2) + new Partei("CDU", new ColorRGB(0x00, 0x4B, 0x76), 18.9), + new Partei("SPD", new ColorRGB(0xC0, 0x00, 0x3C), 25.7), + new Partei("AfD", new ColorRGB(0x80, 0xCD, 0xEC), 10.3), + new Partei("FDP", new ColorRGB(0xF7, 0xBB, 0x3D), 11.5), + new Partei("DIE LINKE", new ColorRGB(0x5F, 0x31, 0x6E), 4.9), + new Partei("GRÜNE", new ColorRGB(0x00, 0x85, 0x4A), 14.8), + new Partei("CSU", new ColorRGB(0x00, 0x77, 0xB6), 5.2) ); - var white = new LambertianMaterial(new Color(.99, .99, .99)); + var white = new LambertianMaterial(new ColorRGB(.99, .99, .99), cs); var count = data.size(); var size = 75d; @@ -272,12 +287,12 @@ public class Examples { objects.add(new Box( new Vec3((i + 1) * spacing + i * size, 0, spacing), new Vec3((i + 1) * spacing + (i + 1) * size, partei.stimmen() * 15, spacing + size), - new DielectricMaterial(1.5, partei.color()) + new DielectricMaterial(1.5, partei.color(), cs) )); } return new Example( - new Scene(new Color(1.25, 1.25, 1.25), objects), + new Scene(cs.illuminant().scale(1.25), objects), SimpleCamera.builder() .withImage(height * 16 / 9, height) .withPosition(new Vec3(700, 250, 800)) @@ -292,7 +307,7 @@ public class Examples { return new Example( new Scene(getSkyBox(), List.of( - new Sphere(Vec3.ZERO, 2, new LambertianMaterial(new ImageTexture("/eu/jonahbauer/raytracing/textures/earthmap.jpg"))) + new Sphere(Vec3.ZERO, 2, new LambertianMaterial(new ImageTexture("earthmap.jpg", ColorSpaces.sRGB))) )), SimpleCamera.builder() .withImage(height * 16 / 9, height) @@ -325,12 +340,13 @@ public class Examples { public static @NotNull Example getFinal(int height) { if (height <= 0) height = 400; + var cs = ColorSpaces.sRGB; var objects = new ArrayList(); var random = new Random(1); // boxes var boxes = new ArrayList(); - var ground = new LambertianMaterial(new Color(0.48, 0.83, 0.53)); + var ground = new LambertianMaterial(new ColorRGB(0.48, 0.83, 0.53), cs); for (int i = 0; i < 20; i++) { for (int j = 0; j < 20; j++) { var w = 100.0; @@ -348,31 +364,31 @@ public class Examples { // light objects.add(new Parallelogram( new Vec3(123, 554, 147), new Vec3(300, 0, 0), new Vec3(0, 0, 265), - new DiffuseLight(new Color(7., 7., 7.)) + new DiffuseLight(new ColorRGB(7., 7., 7.), cs) )); // spheres with different materials - objects.add(new Sphere(new Vec3(400, 400, 200), 50, new LambertianMaterial(new Color(0.7, 0.3, 0.1)))); + objects.add(new Sphere(new Vec3(400, 400, 200), 50, new LambertianMaterial(new ColorRGB(0.7, 0.3, 0.1), cs))); objects.add(new Sphere(new Vec3(260, 150, 45), 50, new DielectricMaterial(1.5))); - objects.add(new Sphere(new Vec3(0, 150, 145), 50, new MetallicMaterial(new Color(0.8, 0.8, 0.9), 1.0))); + objects.add(new Sphere(new Vec3(0, 150, 145), 50, new MetallicMaterial(new ColorRGB(0.8, 0.8, 0.9), cs, 1.0))); // glass sphere filled with gas var boundary = new Sphere(new Vec3(360, 150, 145), 70, new DielectricMaterial(1.5)); objects.add(boundary); - objects.add(new ConstantMedium(boundary, 0.2, new IsotropicMaterial(new Color(0.2, 0.4, 0.9)))); + objects.add(new ConstantMedium(boundary, 0.2, new IsotropicMaterial(new ColorRGB(0.2, 0.4, 0.9), cs))); // put the world in a glass sphere objects.add(new ConstantMedium( new Sphere(new Vec3(0, 0, 0), 5000, new DielectricMaterial(1.5)), - 0.0001, new IsotropicMaterial(new Color(1., 1., 1.)) + 0.0001, new IsotropicMaterial(new ColorRGB(1., 1., 1.), cs) )); // textures spheres - objects.add(new Sphere(new Vec3(400, 200, 400), 100, new LambertianMaterial(new ImageTexture("/eu/jonahbauer/raytracing/textures/earthmap.jpg")))); + objects.add(new Sphere(new Vec3(400, 200, 400), 100, new LambertianMaterial(new ImageTexture("earthmap.jpg", cs)))); objects.add(new Sphere(new Vec3(220, 280, 300), 80, new LambertianMaterial(new PerlinTexture(0.2)))); // box from spheres - var white = new LambertianMaterial(new Color(.73, .73, .73)); + var white = new LambertianMaterial(new ColorRGB(.73, .73, .73), cs); var spheres = new ArrayList(); for (int j = 0; j < 1000; j++) { spheres.add(new Sphere(new Vec3(random.nextDouble(165), random.nextDouble(165), random.nextDouble(165)), 10, white)); @@ -391,6 +407,9 @@ public class Examples { } private static @NotNull SkyBox getSkyBox() { - return SkyBox.gradient(new Color(0.5, 0.7, 1.0), Color.WHITE); + return SkyBox.gradient( + new RGBIlluminantSpectrum(ColorSpaces.sRGB, new ColorRGB(0.5, 0.7, 1.0)), + ColorSpaces.sRGB.illuminant() + ); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java index 699743a..6f82d2d 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Main.java +++ b/src/main/java/eu/jonahbauer/raytracing/Main.java @@ -2,9 +2,12 @@ package eu.jonahbauer.raytracing; import eu.jonahbauer.raytracing.render.ImageFormat; import eu.jonahbauer.raytracing.render.canvas.Canvas; -import eu.jonahbauer.raytracing.render.canvas.Image; import eu.jonahbauer.raytracing.render.canvas.LiveCanvas; +import eu.jonahbauer.raytracing.render.canvas.XYZCanvas; import eu.jonahbauer.raytracing.render.renderer.SimpleRenderer; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpaces; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum; import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -30,11 +33,11 @@ public class Main { Canvas canvas; if (config.preview) { - var image = new LiveCanvas(new Image(camera.getWidth(), camera.getHeight())); + var image = new LiveCanvas(new XYZCanvas(camera.getWidth(), camera.getHeight()), ColorSpaces.sRGB); image.preview(); canvas = image; } else { - canvas = new Image(camera.getWidth(), camera.getHeight()); + canvas = new XYZCanvas(camera.getWidth(), camera.getHeight()); } long time = System.nanoTime(); diff --git a/src/main/java/eu/jonahbauer/raytracing/math/Ray.java b/src/main/java/eu/jonahbauer/raytracing/math/Ray.java index 0f43dc4..12bdd88 100644 --- a/src/main/java/eu/jonahbauer/raytracing/math/Ray.java +++ b/src/main/java/eu/jonahbauer/raytracing/math/Ray.java @@ -1,13 +1,19 @@ package eu.jonahbauer.raytracing.math; +import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths; import org.jetbrains.annotations.NotNull; import java.util.Objects; -public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) { +public record Ray(@NotNull Vec3 origin, @NotNull Vec3 direction, @NotNull SampledWavelengths lambda) { public Ray { Objects.requireNonNull(origin, "origin"); Objects.requireNonNull(direction, "direction"); + Objects.requireNonNull(lambda, "lambda"); + } + + public Ray(@NotNull Vec3 origin, @NotNull Vec3 direction) { + this(origin, direction, SampledWavelengths.EMPTY); } public @NotNull Vec3 at(double t) { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java b/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java index caba497..bf970c5 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java @@ -1,7 +1,7 @@ package eu.jonahbauer.raytracing.render; import eu.jonahbauer.raytracing.render.canvas.Canvas; -import eu.jonahbauer.raytracing.render.canvas.Image; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpaces; import org.jetbrains.annotations.NotNull; import java.io.*; @@ -11,7 +11,6 @@ import java.nio.file.Path; import java.util.zip.CRC32; import java.util.zip.CheckedOutputStream; import java.util.zip.DeflaterOutputStream; -import java.util.zip.InflaterInputStream; public enum ImageFormat { PPM { @@ -24,15 +23,16 @@ public enum ImageFormat { writer.write(String.valueOf(image.getHeight())); writer.write("\n255\n"); - var it = image.pixels().iterator(); - while (it.hasNext()) { - var color = it.next(); - writer.write(String.valueOf(color.red())); - writer.write(" "); - writer.write(String.valueOf(color.green())); - writer.write(" "); - writer.write(String.valueOf(color.blue())); - writer.write("\n"); + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + var color = image.getRGB(x, y, ColorSpaces.sRGB); + writer.write(String.valueOf(color.red())); + writer.write(" "); + writer.write(String.valueOf(color.green())); + writer.write(" "); + writer.write(String.valueOf(color.blue())); + writer.write("\n"); + } } } } @@ -84,13 +84,14 @@ public enum ImageFormat { idat.writeInt(IDAT_TYPE); try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) { - var pixels = image.pixels().iterator(); - for (int i = 0; pixels.hasNext(); i = (i + 1) % image.getWidth()) { - if (i == 0) deflate.writeByte(0); // filter type - var pixel = pixels.next(); - deflate.writeByte(pixel.red()); - deflate.writeByte(pixel.green()); - deflate.writeByte(pixel.blue()); + for (int y = 0; y < image.getHeight(); y++) { + deflate.writeByte(0); // filter type + for (int x = 0; x < image.getWidth(); x++) { + var pixel = image.getRGB(x, y, ColorSpaces.sRGB); + deflate.writeByte(pixel.red()); + deflate.writeByte(pixel.green()); + deflate.writeByte(pixel.blue()); + } } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java b/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java index 84f40cd..27311f9 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/camera/SimpleCamera.java @@ -2,6 +2,7 @@ package eu.jonahbauer.raytracing.render.camera; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -93,7 +94,7 @@ public final class SimpleCamera implements Camera { var origin = getRayOrigin(random); var target = getRayTarget(x, y, i, j, n, random); - return new Ray(origin, target.minus(origin)); + return new Ray(origin, target.minus(origin), SampledWavelengths.uniform(random.nextDouble())); } /** diff --git a/src/main/java/eu/jonahbauer/raytracing/render/canvas/Canvas.java b/src/main/java/eu/jonahbauer/raytracing/render/canvas/Canvas.java index 0919996..cf5a172 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/canvas/Canvas.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/canvas/Canvas.java @@ -1,22 +1,37 @@ package eu.jonahbauer.raytracing.render.canvas; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum; +import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; import org.jetbrains.annotations.NotNull; -import java.util.function.Function; -import java.util.stream.IntStream; -import java.util.stream.Stream; - public interface Canvas { + /** + * {@return the width of this canvas} + */ int getWidth(); + + /** + * {@return the height of this canvas} + */ int getHeight(); - void set(int x, int y, @NotNull Color color); - @NotNull Color get(int x, int y); + /** + * Adds a sample to this canvas + * @param x the pixel x coordinate + * @param y the pixel y coordinate + * @param n the index of the sample + * @param spectrum the sampled spectrum + * @param lambda the sampled wavelengths + */ + void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda); - default @NotNull Stream pixels() { - return IntStream.range(0, getHeight()) - .mapToObj(y -> IntStream.range(0, getWidth()).mapToObj(x -> get(x, y))) - .flatMap(Function.identity()); - } + /** + * {@return the color at a given pixel} + * @param x the pixel x coordinate + * @param y the pixel y coordinate + * @param cs the color space of the output + */ + @NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/canvas/Image.java b/src/main/java/eu/jonahbauer/raytracing/render/canvas/Image.java deleted file mode 100644 index b8157f7..0000000 --- a/src/main/java/eu/jonahbauer/raytracing/render/canvas/Image.java +++ /dev/null @@ -1,58 +0,0 @@ -package eu.jonahbauer.raytracing.render.canvas; - -import eu.jonahbauer.raytracing.render.texture.Color; -import org.jetbrains.annotations.NotNull; - -import java.awt.image.BufferedImage; -import java.util.Objects; - -public final class Image implements Canvas { - private final int width; - private final int height; - - private final Color[][] data; - - public Image(int width, int height) { - this.width = width; - this.height = height; - - if (width <= 0) throw new IllegalArgumentException("width must be positive"); - if (height <= 0) throw new IllegalArgumentException("height must be positive"); - - this.data = new Color[height][width]; - } - - public Image(@NotNull BufferedImage image) { - this(image.getWidth(), image.getHeight()); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - this.data[y][x] = new Color(image.getRGB(x, y)); - } - } - } - - @Override - public int getWidth() { - return width; - } - - @Override - public int getHeight() { - return height; - } - - @Override - public @NotNull Color get(int x, int y) { - Objects.checkIndex(x, width); - Objects.checkIndex(y, height); - return Objects.requireNonNullElse(this.data[y][x], Color.BLACK); - } - - @Override - public void set(int x, int y, @NotNull Color color) { - Objects.checkIndex(x, width); - Objects.checkIndex(y, height); - this.data[y][x] = Objects.requireNonNull(color); - } -} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/canvas/LiveCanvas.java b/src/main/java/eu/jonahbauer/raytracing/render/canvas/LiveCanvas.java index 2839f2b..e782836 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/canvas/LiveCanvas.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/canvas/LiveCanvas.java @@ -1,6 +1,9 @@ package eu.jonahbauer.raytracing.render.canvas; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum; +import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -12,10 +15,12 @@ import java.awt.image.BufferedImage; public final class LiveCanvas implements Canvas { private final @NotNull Canvas delegate; private final @NotNull BufferedImage image; + private final @NotNull ColorSpace cs; - public LiveCanvas(@NotNull Canvas delegate) { + public LiveCanvas(@NotNull Canvas delegate, @NotNull ColorSpace cs) { this.delegate = delegate; this.image = new BufferedImage(delegate.getWidth(), delegate.getHeight(), BufferedImage.TYPE_INT_RGB); + this.cs = cs; } @Override @@ -29,15 +34,16 @@ public final class LiveCanvas implements Canvas { } @Override - public void set(int x, int y, @NotNull Color color) { - delegate.set(x, y, color); + public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) { + delegate.add(x, y, n, spectrum, lambda); + var color = ColorRGB.gamma(delegate.getRGB(x, y, cs)); var rgb = color.red() << 16 | color.green() << 8 | color.blue(); image.setRGB(x, y, rgb); } @Override - public @NotNull Color get(int x, int y) { - return delegate.get(x, y); + public @NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs) { + return delegate.getRGB(x, y, cs); } public @NotNull Thread preview() { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/canvas/RGBCanvas.java b/src/main/java/eu/jonahbauer/raytracing/render/canvas/RGBCanvas.java new file mode 100644 index 0000000..34d58c1 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/canvas/RGBCanvas.java @@ -0,0 +1,76 @@ +package eu.jonahbauer.raytracing.render.canvas; + +import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum; +import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; +import org.jetbrains.annotations.NotNull; + +import java.awt.image.BufferedImage; +import java.util.Objects; + +public final class RGBCanvas implements Canvas { + private final int width; + private final int height; + + private final @NotNull ColorSpace cs; + private final @NotNull ColorRGB[][] data; + + public RGBCanvas(int width, int height, @NotNull ColorSpace cs) { + this.width = width; + this.height = height; + this.cs = Objects.requireNonNull(cs); + + if (width <= 0) throw new IllegalArgumentException("width must be positive"); + if (height <= 0) throw new IllegalArgumentException("height must be positive"); + + this.data = new ColorRGB[height][width]; + } + + public RGBCanvas(@NotNull BufferedImage image, @NotNull ColorSpace cs) { + this(image.getWidth(), image.getHeight(), cs); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + this.data[y][x] = new ColorRGB(image.getRGB(x, y)); + } + } + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) { + assert x < width; + assert y < height; + + var rgb = spectrum.toRGB(lambda, cs); + data[y][x] = ColorRGB.average(data[y][x], rgb, n); + } + + @Override + public @NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs) { + if (cs == this.cs) return get(x, y); + return cs.toRGB(this.cs.toXYZ(get(x, y))); + } + + public @NotNull ColorRGB get(int x, int y) { + assert x < width; + assert y < height; + return Objects.requireNonNullElse(data[y][x], ColorRGB.BLACK); + } + + public void set(int x, int y, @NotNull ColorRGB color) { + assert x < width; + assert y < height; + data[y][x] = Objects.requireNonNull(color); + } +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/canvas/XYZCanvas.java b/src/main/java/eu/jonahbauer/raytracing/render/canvas/XYZCanvas.java new file mode 100644 index 0000000..2375339 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/canvas/XYZCanvas.java @@ -0,0 +1,74 @@ +package eu.jonahbauer.raytracing.render.canvas; + +import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum; +import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ; +import org.jetbrains.annotations.NotNull; + +import java.awt.image.BufferedImage; +import java.util.Objects; + +public final class XYZCanvas implements Canvas { + private final int width; + private final int height; + + private final @NotNull ColorXYZ[][] data; + + public XYZCanvas(int width, int height) { + this.width = width; + this.height = height; + + if (width <= 0) throw new IllegalArgumentException("width must be positive"); + if (height <= 0) throw new IllegalArgumentException("height must be positive"); + + this.data = new ColorXYZ[height][width]; + } + + public XYZCanvas(@NotNull BufferedImage image, @NotNull ColorSpace cs) { + this(image.getWidth(), image.getHeight()); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + data[y][x] = cs.toXYZ(new ColorRGB(image.getRGB(x, y))); + } + } + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public void add(int x, int y, int n, @NotNull SampledSpectrum spectrum, @NotNull SampledWavelengths lambda) { + assert x < width; + assert y < height; + + var xyz = spectrum.toXYZ(lambda); + data[y][x] = ColorXYZ.average(get(x, y), xyz, n); + } + + public @NotNull ColorXYZ get(int x, int y) { + assert x < width; + assert y < height; + return Objects.requireNonNullElse(data[y][x], ColorXYZ.BLACK); + } + + @Override + public @NotNull ColorRGB getRGB(int x, int y, @NotNull ColorSpace cs) { + return cs.toRGB(get(x, y)); + } + + public void set(int x, int y, @NotNull ColorXYZ color) { + assert x < width; + assert y < height; + data[y][x] = Objects.requireNonNull(color); + } +} 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 4710c2b..c7a9f7c 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DielectricMaterial.java @@ -2,7 +2,10 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; @@ -13,13 +16,17 @@ import java.util.random.RandomGenerator; public record DielectricMaterial(double refractionIndex, @NotNull Texture texture) implements Material { public DielectricMaterial(double refractionIndex) { - this(refractionIndex, Color.WHITE); + this(refractionIndex, Spectra.WHITE); } public DielectricMaterial { Objects.requireNonNull(texture, "texture"); } + public DielectricMaterial(double refractionIndex, @NotNull ColorRGB color, @NotNull ColorSpace cs) { + this(refractionIndex, new RGBAlbedoSpectrum(cs, color)); + } + @Override public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { var ri = hit.isFrontFace() ? (1 / refractionIndex) : refractionIndex; @@ -32,7 +39,7 @@ public record DielectricMaterial(double refractionIndex, @NotNull Texture textur .orElseGet(() -> Vec3.reflect(ray.direction(), hit.normal())); var attenuation = texture.get(hit); - return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection))); + return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection, ray.lambda()))); } private double reflectance(double cos) { diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/DiffuseLight.java b/src/main/java/eu/jonahbauer/raytracing/render/material/DiffuseLight.java index c4996ad..4585f45 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DiffuseLight.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DiffuseLight.java @@ -1,22 +1,34 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBIlluminantSpectrum; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; +import java.util.Objects; import java.util.Optional; import java.util.random.RandomGenerator; public record DiffuseLight(@NotNull Texture texture) implements Material { + public DiffuseLight { + Objects.requireNonNull(texture, "texture"); + } + + public DiffuseLight(@NotNull ColorRGB color, @NotNull ColorSpace cs) { + this(new RGBIlluminantSpectrum(cs, color)); + } + @Override public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { return Optional.empty(); } @Override - public @NotNull Color emitted(@NotNull HitResult hit) { + public @NotNull Spectrum emitted(@NotNull HitResult hit) { return texture.get(hit); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java index 091c802..7036417 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/DirectionalMaterial.java @@ -2,7 +2,8 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; @@ -40,11 +41,11 @@ public final class DirectionalMaterial implements Material { if (back != null) return back.scatter(ray, hit, random); } // let the ray pass through without obstruction - return Optional.of(new SpecularScatterResult(Color.WHITE, new Ray(ray.at(hit.t()), ray.direction()))); + return Optional.of(new SpecularScatterResult(Spectra.WHITE, new Ray(ray.at(hit.t()), ray.direction(), ray.lambda()))); } @Override - public @NotNull Color emitted(@NotNull HitResult hit) { + public @NotNull Spectrum emitted(@NotNull HitResult hit) { if (hit.isFrontFace()) { if (front != null) return front.emitted(hit); } else { @@ -56,7 +57,7 @@ public final class DirectionalMaterial implements Material { private record DirectionalTexture(@Nullable Texture front, @Nullable Texture back) implements Texture { @Override - public @NotNull Color get(double u, double v, @NotNull Vec3 p) { + public @NotNull Spectrum get(double u, double v, @NotNull Vec3 p) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java index 08b0f29..5fbe796 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/IsotropicMaterial.java @@ -2,15 +2,27 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.render.renderer.pdf.SphereProbabilityDensityFunction; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; +import java.util.Objects; import java.util.Optional; import java.util.random.RandomGenerator; -public record IsotropicMaterial(@NotNull Color albedo) implements Material { +public record IsotropicMaterial(@NotNull Spectrum albedo) implements Material { + public IsotropicMaterial { + Objects.requireNonNull(albedo, "albedo"); + } + + public IsotropicMaterial(@NotNull ColorRGB color, @NotNull ColorSpace cs) { + this(new RGBAlbedoSpectrum(cs, color)); + } + @Override public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { return Optional.of(new PdfScatterResult(albedo(), new SphereProbabilityDensityFunction())); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java index ffa218e..f080d88 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/LambertianMaterial.java @@ -2,6 +2,9 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.render.renderer.pdf.CosineProbabilityDensityFunction; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; @@ -15,6 +18,10 @@ public record LambertianMaterial(@NotNull Texture texture) implements Material { Objects.requireNonNull(texture, "texture"); } + public LambertianMaterial(@NotNull ColorRGB color, @NotNull ColorSpace cs) { + this(new RGBAlbedoSpectrum(cs, color)); + } + @Override public @NotNull Optional scatter(@NotNull Ray ray, @NotNull HitResult hit, @NotNull RandomGenerator random) { var attenuation = texture.get(hit); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java b/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java index f5f8ba6..96af7a2 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/Material.java @@ -2,7 +2,9 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.render.renderer.pdf.ProbabilityDensityFunction; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; @@ -30,10 +32,10 @@ public interface Material { /** * {@return the color emitted for a given hit} - * @implSpec the default implementation returns {@linkplain Color#BLACK black}, i.e. no emission + * @implSpec the default implementation returns {@linkplain ColorRGB#BLACK black}, i.e. no emission */ - default @NotNull Color emitted(@NotNull HitResult hit) { - return Color.BLACK; + default @NotNull Spectrum emitted(@NotNull HitResult hit) { + return Spectra.BLACK; } /** @@ -48,7 +50,7 @@ public interface Material { * @param attenuation the attenuation of the scattered light ray * @param ray the scattered light ray */ - record SpecularScatterResult(@NotNull Color attenuation, @NotNull Ray ray) implements ScatterResult { + record SpecularScatterResult(@NotNull Spectrum attenuation, @NotNull Ray ray) implements ScatterResult { public SpecularScatterResult { Objects.requireNonNull(attenuation, "attenuation"); Objects.requireNonNull(ray, "ray"); @@ -62,7 +64,7 @@ public interface Material { * @param attenuation the attenuation of the scattered light ray * @param pdf the probability density function */ - record PdfScatterResult(@NotNull Color attenuation, @NotNull ProbabilityDensityFunction pdf) implements ScatterResult { + record PdfScatterResult(@NotNull Spectrum attenuation, @NotNull ProbabilityDensityFunction pdf) implements ScatterResult { public PdfScatterResult { Objects.requireNonNull(attenuation, "attenuation"); Objects.requireNonNull(pdf, "pdf"); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java b/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java index cdffa63..a95a79f 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/material/MetallicMaterial.java @@ -2,6 +2,9 @@ package eu.jonahbauer.raytracing.render.material; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum; import eu.jonahbauer.raytracing.render.texture.Texture; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; @@ -16,6 +19,14 @@ public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements this(texture, 0); } + public MetallicMaterial(@NotNull ColorRGB color, @NotNull ColorSpace cs) { + this(color, cs, 0); + } + + public MetallicMaterial(@NotNull ColorRGB color, @NotNull ColorSpace cs, double fuzz) { + this(new RGBAlbedoSpectrum(cs, color), fuzz); + } + public MetallicMaterial { Objects.requireNonNull(texture, "texture"); if (fuzz < 0 || !Double.isFinite(fuzz)) throw new IllegalArgumentException("fuzz must be non-negative"); @@ -28,6 +39,6 @@ public record MetallicMaterial(@NotNull Texture texture, double fuzz) implements newDirection = Vec3.fma(fuzz, Vec3.random(random), newDirection.unit()); } var attenuation = texture.get(hit); - return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection))); + return Optional.of(new SpecularScatterResult(attenuation, new Ray(hit.position(), newDirection, ray.lambda()))); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/renderer/Renderer.java b/src/main/java/eu/jonahbauer/raytracing/render/renderer/Renderer.java index 77c829a..9b55243 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/Renderer.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/Renderer.java @@ -2,13 +2,13 @@ package eu.jonahbauer.raytracing.render.renderer; import eu.jonahbauer.raytracing.render.camera.Camera; import eu.jonahbauer.raytracing.render.canvas.Canvas; -import eu.jonahbauer.raytracing.render.canvas.Image; +import eu.jonahbauer.raytracing.render.canvas.XYZCanvas; import eu.jonahbauer.raytracing.scene.Scene; import org.jetbrains.annotations.NotNull; public interface Renderer { - default @NotNull Image render(@NotNull Camera camera, @NotNull Scene scene) { - var image = new Image(camera.getWidth(), camera.getHeight()); + default @NotNull Canvas render(@NotNull Camera camera, @NotNull Scene scene) { + var image = new XYZCanvas(camera.getWidth(), camera.getHeight()); render(camera, scene, image); return image; } 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 fc9270d..1e00bd4 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/renderer/SimpleRenderer.java @@ -4,7 +4,7 @@ import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.render.material.Material; import eu.jonahbauer.raytracing.render.renderer.pdf.TargetingProbabilityDensityFunction; import eu.jonahbauer.raytracing.render.renderer.pdf.MixtureProbabilityDensityFunction; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum; import eu.jonahbauer.raytracing.render.camera.Camera; import eu.jonahbauer.raytracing.render.canvas.Canvas; import eu.jonahbauer.raytracing.scene.Scene; @@ -76,18 +76,11 @@ public final class SimpleRenderer implements Renderer { for (int x = 0; x < camera.getWidth(); x++) { var ray = camera.cast(x, y, sif, sjf, sqrtSamplesPerPixel, random); var c = getColor(scene, ray, random); - canvas.set(x, y, Color.average(canvas.get(x, y), c, sample)); + canvas.add(x, y, sample, c, ray.lambda()); } }); } } - - // apply gamma correction - getScanlineStream(camera.getHeight(), parallel).forEach(y -> { - for (int x = 0; x < camera.getWidth(); x++) { - canvas.set(x, y, Color.gamma(canvas.get(x, y), gamma)); - } - }); } /** @@ -100,7 +93,6 @@ public final class SimpleRenderer implements Renderer { getScanlineStream(camera.getHeight(), parallel).forEach(y -> { var random = splittable.split(); for (int x = 0; x < camera.getWidth(); x++) { - var color = Color.BLACK; int i = 0; for (int sj = 0; sj < sqrtSamplesPerPixel; sj++) { for (int si = 0; si < sqrtSamplesPerPixel; si++) { @@ -109,10 +101,9 @@ public final class SimpleRenderer implements Renderer { System.out.println("Casting ray " + ray + " through pixel (" + x + "," + y + ") at subpixel (" + si + "," + sj + ")..."); } var c = getColor(scene, ray, random); - color = Color.average(color, c, ++i); + canvas.add(x, y, ++i, c, ray.lambda()); } } - canvas.set(x, y, Color.gamma(color, gamma)); } }); } @@ -120,19 +111,19 @@ public final class SimpleRenderer implements Renderer { /** * {@return the color of the given ray in the given scene} */ - private @NotNull Color getColor(@NotNull Scene scene, @NotNull Ray ray, @NotNull RandomGenerator random) { + private @NotNull SampledSpectrum getColor(@NotNull Scene scene, @NotNull Ray ray, @NotNull RandomGenerator random) { return getColor0(scene, ray, maxDepth, random); } - private @NotNull Color getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) { - var color = Color.BLACK; - var attenuation = Color.WHITE; + private @NotNull SampledSpectrum getColor0(@NotNull Scene scene, @NotNull Ray ray, int depth, @NotNull RandomGenerator random) { + var color = SampledSpectrum.BLACK; + var attenuation = SampledSpectrum.WHITE; while (depth-- > 0) { var optional = scene.hit(ray); if (optional.isEmpty()) { var background = scene.getBackgroundColor(ray); - color = Color.fma(attenuation, background, color); + color = SampledSpectrum.fma(attenuation, background, color); if (DEBUG) { System.out.println(" Hit background: " + background); } @@ -144,13 +135,13 @@ public final class SimpleRenderer implements Renderer { System.out.println(" Hit " + hit.target() + " at t=" + hit.t() + " (" + hit.position() + ")"); } var material = hit.material(); - var emitted = material.emitted(hit); - if (DEBUG && !Color.BLACK.equals(emitted)) { + var emitted = material.emitted(hit).sample(ray.lambda()); + if (DEBUG && !SampledSpectrum.BLACK.equals(emitted)) { System.out.println(" Emitted: " + emitted); } var result = material.scatter(ray, hit, random); - color = Color.fma(attenuation, emitted, color); + color = SampledSpectrum.fma(attenuation, emitted, color); if (result.isEmpty()) { if (DEBUG) { @@ -161,7 +152,7 @@ public final class SimpleRenderer implements Renderer { switch (result.get()) { case Material.SpecularScatterResult(var a, var scattered) -> { - attenuation = attenuation.times(a); + attenuation = attenuation.times(a.sample(ray.lambda())); ray = scattered; if (DEBUG) { @@ -170,8 +161,8 @@ public final class SimpleRenderer implements Renderer { } case Material.PdfScatterResult(var a, var pdf) -> { if (scene.getTargets() == null) { - attenuation = attenuation.times(a); - ray = new Ray(hit.position(), pdf.generate(random)); + attenuation = attenuation.times(a.sample(ray.lambda())); + ray = new Ray(hit.position(), pdf.generate(random), ray.lambda()); if (DEBUG) { System.out.println(" Pdf scattering with albedo " + a); @@ -184,10 +175,10 @@ public final class SimpleRenderer implements Renderer { var actualPdf = mixed.value(direction); if (actualPdf == 0) break; // when actualPdf is 0, the ray should have never been generated by mixed.generate - var factor = idealPdf / actualPdf; + var factor = idealPdf / actualPdf; - attenuation = attenuation.times(a.times(factor)); - ray = new Ray(hit.position(), direction); + attenuation = attenuation.times(a.sample(ray.lambda()).times(factor)); + ray = new Ray(hit.position(), direction, ray.lambda()); if (DEBUG) { System.out.println(" Pdf scattering with albedo " + a + " and factor " + factor); @@ -215,7 +206,7 @@ public final class SimpleRenderer implements Renderer { * are encoded in the longs lower and upper 32 bits respectively. */ private static @NotNull IntStream getScanlineStream(int height, boolean parallel) { - var stream = IntStream.range(0, height); + var stream = IntStream.range(0, height).map(i -> height - i - 1); return parallel ? stream.parallel() : stream; } 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 a7f0916..f3319ab 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/SampledSpectrum.java @@ -4,11 +4,12 @@ import eu.jonahbauer.raytracing.math.IVec; import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ; import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; import org.jetbrains.annotations.NotNull; import java.util.Arrays; +// TODO use Vector API to parallelize operations public final class SampledSpectrum implements IVec { public static final SampledSpectrum BLACK; public static final SampledSpectrum WHITE; @@ -137,7 +138,7 @@ public final class SampledSpectrum implements IVec { return lambdas.toXYZ(this); } - public @NotNull Color toRGB(@NotNull SampledWavelengths lambdas, @NotNull ColorSpace cs) { + public @NotNull ColorRGB toRGB(@NotNull SampledWavelengths lambdas, @NotNull ColorSpace cs) { return cs.toRGB(toXYZ(lambdas)); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorRGB.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorRGB.java new file mode 100644 index 0000000..3013e96 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/ColorRGB.java @@ -0,0 +1,153 @@ +package eu.jonahbauer.raytracing.render.spectral.colors; + +import eu.jonahbauer.raytracing.math.IVec3; +import eu.jonahbauer.raytracing.math.Vec3; +import org.jetbrains.annotations.NotNull; + +import java.util.Random; + +import static eu.jonahbauer.raytracing.Main.DEBUG; + +public record ColorRGB(double r, double g, double b) implements IVec3 { + public static final @NotNull ColorRGB BLACK = new ColorRGB(0.0, 0.0, 0.0); + public static final @NotNull ColorRGB WHITE = new ColorRGB(1.0, 1.0, 1.0); + + public static @NotNull ColorRGB random(@NotNull Random random) { + return new ColorRGB(random.nextDouble(), random.nextDouble(), random.nextDouble()); + } + + public static @NotNull ColorRGB random(@NotNull Random random, double min, double max) { + var span = max - min; + return new ColorRGB( + Math.fma(random.nextDouble(), span, min), + Math.fma(random.nextDouble(), span, min), + Math.fma(random.nextDouble(), span, min) + ); + } + + public ColorRGB(int rgb) { + this((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); + } + + public ColorRGB(int red, int green, int blue) { + this(red / 255f, green / 255f, blue / 255f); + } + + public ColorRGB { + if (DEBUG && (!Double.isFinite(r) || !Double.isFinite(g) || !Double.isFinite(b))) { + throw new IllegalArgumentException("r, g and b must be finite"); + } + } + + /* + * Math + */ + + public static @NotNull ColorRGB average(@NotNull ColorRGB current, @NotNull ColorRGB next, int index) { + return lerp(current, next, 1d / index); + } + + public static @NotNull ColorRGB lerp(@NotNull ColorRGB a, @NotNull ColorRGB b, double t) { + if (t < 0) return a; + if (t > 1) return b; + return new ColorRGB( + Math.fma(t, b.r - a.r, a.r), + Math.fma(t, b.g - a.g, a.g), + Math.fma(t, b.b - a.b, a.b) + ); + } + + public static @NotNull ColorRGB fma(@NotNull ColorRGB a, @NotNull ColorRGB b, @NotNull ColorRGB c) { + return new ColorRGB( + Math.fma(a.r, b.r, c.r), + Math.fma(a.g, b.g, c.g), + Math.fma(a.b, b.b, c.b) + ); + } + + public static @NotNull ColorRGB gamma(@NotNull ColorRGB color) { + return new ColorRGB(gamma(color.r), gamma(color.g), gamma(color.b)); + } + + public static @NotNull ColorRGB inverseGamma(@NotNull ColorRGB color) { + return new ColorRGB(inverseGamma(color.r), inverseGamma(color.g), inverseGamma(color.b)); + } + + private static double gamma(double value) { + if (value <= 0.0031308) return 12.92 * value; + return 1.055 * Math.pow(value, 1. / 2.4) - 0.055; + } + + private static double inverseGamma(double value) { + if (value <= 0.04045) return value / 12.92; + return Math.pow((value + 0.055) / 1.055, 2.4d); + } + + @Override + public @NotNull ColorRGB plus(@NotNull ColorRGB other) { + return new ColorRGB(r + other.r, g + other.g, b + other.b); + } + + @Override + public @NotNull ColorRGB minus(@NotNull ColorRGB other) { + return new ColorRGB(r - other.r, g - other.g, b - other.b); + } + + @Override + public @NotNull ColorRGB times(double d) { + return new ColorRGB(r * d, g * d, b * d); + } + + @Override + public @NotNull ColorRGB times(@NotNull ColorRGB other) { + return new ColorRGB(r * other.r, g * other.g, b * other.b); + } + + /* + * Vec3 + */ + + @Override + public @NotNull Vec3 toVec3() { + return new Vec3(r, g, b); + } + + public static @NotNull ColorRGB fromVec3(@NotNull Vec3 vec) { + return new ColorRGB(vec.x(), vec.y(), vec.z()); + } + + /* + * Accessors + */ + + public int red() { + return toInt(r); + } + + public int green() { + return toInt(g); + } + + public int blue() { + return toInt(b); + } + + private static int toInt(double value) { + return Math.clamp((int) (255.99 * value), 0, 255); + } + + @Override + public double component1() { + return r; + } + + @Override + public double component2() { + return g; + } + + @Override + public double component3() { + return b; + } +} 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 3e14137..f281136 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 @@ -4,7 +4,6 @@ import eu.jonahbauer.raytracing.math.Matrix3; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.render.spectral.spectrum.DenselySampledSpectrum; import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; -import eu.jonahbauer.raytracing.render.texture.Color; import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -55,17 +54,17 @@ public final class ColorSpace { this.RGBfromXYZ = XYZfromRGB.invert(); } - public @NotNull Color toRGB(@NotNull ColorXYZ xyz) { + public @NotNull ColorRGB toRGB(@NotNull ColorXYZ xyz) { var out = RGBfromXYZ.times(xyz.toVec3()); - return new Color(out.x(), out.y(), out.z()); + return new ColorRGB(out.x(), out.y(), out.z()); } - public @NotNull ColorXYZ toXYZ(@NotNull Color rgb) { + public @NotNull ColorXYZ toXYZ(@NotNull ColorRGB rgb) { var out = XYZfromRGB.times(rgb.toVec3()); return ColorXYZ.fromVec3(out); } - public @NotNull Vec3 toCIELab(@NotNull Color rgb) { + public @NotNull Vec3 toCIELab(@NotNull ColorRGB rgb) { return toCIELab(toXYZ(rgb)); } @@ -86,8 +85,8 @@ public final class ColorSpace { } } - public @NotNull SigmoidPolynomial toSpectrum(@NotNull Color rgb) { - return RGBtoSpectrumTable.get(new Color( + public @NotNull SigmoidPolynomial toSpectrum(@NotNull ColorRGB rgb) { + return RGBtoSpectrumTable.get(new ColorRGB( Math.max(0, rgb.r()), Math.max(0, rgb.g()), Math.max(0, rgb.b()) 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 bbfb0e2..b227da4 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 @@ -1,6 +1,5 @@ package eu.jonahbauer.raytracing.render.spectral.colors; -import eu.jonahbauer.raytracing.render.texture.Color; import org.jetbrains.annotations.NotNull; import java.io.*; @@ -86,7 +85,7 @@ public final class SpectrumTable { } } - public @NotNull SigmoidPolynomial get(@NotNull Color color) { + public @NotNull SigmoidPolynomial get(@NotNull ColorRGB color) { // handle uniform rgb values if (color.r() == color.g() && color.g() == color.b()) { return new SigmoidPolynomial(0, 0, (color.r() - .5) / Math.sqrt(color.r() * (1 - color.r()))); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTableGenerator.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTableGenerator.java index f06f7d9..51c6bef 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTableGenerator.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/colors/SpectrumTableGenerator.java @@ -3,7 +3,6 @@ package eu.jonahbauer.raytracing.render.spectral.colors; import eu.jonahbauer.raytracing.math.Matrix3; import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; -import eu.jonahbauer.raytracing.render.texture.Color; import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -81,7 +80,7 @@ public final class SpectrumTableGenerator { return new SpectrumTable(resolution, scale, table); } - private void generate(@NotNull Color rgb, double @NotNull[] c, double @NotNull[] out, int offset) { + private void generate(@NotNull ColorRGB rgb, double @NotNull[] c, double @NotNull[] out, int offset) { gaussNewton(rgb, c, ITERATIONS); double c0 = 360.0, c1 = 1.0 / (830.0 - 360.0); double A = c[0], B = c[1], C = c[2]; @@ -97,7 +96,7 @@ public final class SpectrumTableGenerator { * @param c the coefficients, used as initial values and output * @param it the number of iterations */ - private void gaussNewton(@NotNull Color rgb, double @NotNull[] c, int it) { + private void gaussNewton(@NotNull ColorRGB rgb, double @NotNull[] c, int it) { var bestQuality = Double.POSITIVE_INFINITY; var bestCoefficients = new double[3]; @@ -134,7 +133,7 @@ public final class SpectrumTableGenerator { /** * Calculates the Jacobian matrix of the {@code polynomial}. */ - private @NotNull Matrix3 getJacobian(@NotNull Color rgb, @NotNull SigmoidPolynomial polynomial) { + private @NotNull Matrix3 getJacobian(@NotNull ColorRGB rgb, @NotNull SigmoidPolynomial polynomial) { var jac = new double[3][3]; // central finite difference coefficients for first derivative with sixth-order accuracy @@ -171,7 +170,7 @@ public final class SpectrumTableGenerator { * the given coefficients, illuminating it with the color space's standard illuminant, and converting it back to an * RBG color. The output is a vector in CIE Lab color space. */ - private @NotNull Vec3 getResidual(@NotNull Color rgb, @NotNull SigmoidPolynomial polynomial) { + private @NotNull Vec3 getResidual(@NotNull ColorRGB rgb, @NotNull SigmoidPolynomial polynomial) { var out = new SigmoidPolynomialSpectrum(polynomial, cs).toXYZ(); return cs.toCIELab(rgb).minus(cs.toCIELab(out)); } @@ -181,12 +180,12 @@ public final class SpectrumTableGenerator { return x * x * (3.0 - 2.0 * x); } - private static @NotNull Color getColor(int l, double x, double y, double z) { + private static @NotNull ColorRGB getColor(int l, double x, double y, double z) { var rgb = new double[3]; rgb[l] = z; rgb[(l + 1) % 3] = x * z; rgb[(l + 2) % 3] = y * z; - return new Color(rgb[0], rgb[1], rgb[2]); + return new ColorRGB(rgb[0], rgb[1], rgb[2]); } private record SigmoidPolynomialSpectrum(@NotNull SigmoidPolynomial polynomial, @NotNull ColorSpace cs) implements Spectrum { 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 index c3e24ae..ad759cb 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/DenselySampledSpectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/DenselySampledSpectrum.java @@ -37,14 +37,6 @@ public final class DenselySampledSpectrum implements Spectrum { this.max = Arrays.stream(this.samples).max().orElseThrow(); } - public @NotNull DenselySampledSpectrum scale(double scale) { - var s = Arrays.copyOf(samples, samples.length); - for (int i = 0; i < s.length; i++) { - s[i] *= scale; - } - return new DenselySampledSpectrum(s, min); - } - @Override public double max() { return max; 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 index 4ff4519..c6c4c3d 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/PiecewiseLinearSpectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/PiecewiseLinearSpectrum.java @@ -1,7 +1,5 @@ package eu.jonahbauer.raytracing.render.spectral.spectrum; -import org.jetbrains.annotations.NotNull; - import java.util.Arrays; public final class PiecewiseLinearSpectrum implements Spectrum { @@ -31,14 +29,6 @@ public final class PiecewiseLinearSpectrum implements Spectrum { this.max = max; } - public @NotNull PiecewiseLinearSpectrum scale(double scale) { - var v = Arrays.copyOf(values, values.length); - for (int i = 0; i < v.length; i++) { - v[i] *= scale; - } - return new PiecewiseLinearSpectrum(lambdas, v); - } - @Override public double max() { return max; 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 index 58b9d21..64c4a23 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBAlbedoSpectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/RGBAlbedoSpectrum.java @@ -2,13 +2,13 @@ package eu.jonahbauer.raytracing.render.spectral.spectrum; import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; import org.jetbrains.annotations.NotNull; public final class RGBAlbedoSpectrum implements Spectrum { private final @NotNull SigmoidPolynomial polynomial; - public RGBAlbedoSpectrum(@NotNull ColorSpace cs, @NotNull Color rgb) { + public RGBAlbedoSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) { if (rgb.r() < 0 || rgb.r() > 1 || rgb.g() < 0 || rgb.g() > 1 || rgb.b() < 0 || rgb.b() > 1) { throw new IllegalArgumentException(); } 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 4358c3f..63c5d1a 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 @@ -2,7 +2,7 @@ package eu.jonahbauer.raytracing.render.spectral.spectrum; import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; import org.jetbrains.annotations.NotNull; /** @@ -14,13 +14,13 @@ public final class RGBIlluminantSpectrum implements Spectrum { private final @NotNull SigmoidPolynomial polynomial; private final @NotNull Spectrum illuminant; - public RGBIlluminantSpectrum(@NotNull ColorSpace cs, @NotNull Color rgb) { + public RGBIlluminantSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) { if (rgb.r() < 0 || rgb.g() < 0 || rgb.b() < 0) { throw new IllegalArgumentException(); } var max = Math.max(rgb.r(), Math.max(rgb.g(), rgb.b())); this.scale = 2 * max; - this.polynomial = cs.toSpectrum(scale == 0 ? rgb.div(scale) : Color.BLACK); + this.polynomial = cs.toSpectrum(scale != 0 ? rgb.div(scale) : ColorRGB.BLACK); this.illuminant = cs.illuminant(); } 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 46a5893..ee154c1 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 @@ -2,20 +2,20 @@ package eu.jonahbauer.raytracing.render.spectral.spectrum; import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; import eu.jonahbauer.raytracing.render.spectral.colors.SigmoidPolynomial; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; import org.jetbrains.annotations.NotNull; public final class RGBUnboundedSpectrum implements Spectrum { private final double scale; private final @NotNull SigmoidPolynomial polynomial; - public RGBUnboundedSpectrum(@NotNull ColorSpace cs, @NotNull Color rgb) { + public RGBUnboundedSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) { if (rgb.r() < 0 || rgb.g() < 0 || rgb.b() < 0) { throw new IllegalArgumentException(); } var max = Math.max(rgb.r(), Math.max(rgb.g(), rgb.b())); this.scale = 2 * max; - this.polynomial = cs.toSpectrum(scale == 0 ? rgb.div(scale) : Color.BLACK); + this.polynomial = cs.toSpectrum(scale == 0 ? rgb.div(scale) : ColorRGB.BLACK); } @Override diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/ScaledSpectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/ScaledSpectrum.java new file mode 100644 index 0000000..0949ef9 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/ScaledSpectrum.java @@ -0,0 +1,15 @@ +package eu.jonahbauer.raytracing.render.spectral.spectrum; + +import org.jetbrains.annotations.NotNull; + +public record ScaledSpectrum(@NotNull Spectrum spectrum, double scale) implements Spectrum { + @Override + public double max() { + return spectrum.max() * scale; + } + + @Override + public double get(double lambda) { + return spectrum.get(lambda) * scale; + } +} 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 index e007509..e845a43 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectra.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectra.java @@ -36,6 +36,9 @@ public final class Spectra { */ public static final Spectrum D65 = read("CIE_std_illum_D65.csv", true); + public static final Spectrum BLACK = new ConstantSpectrum(0); + public static final Spectrum WHITE = new ConstantSpectrum(1); + private static @NotNull Spectrum read(@NotNull String path, boolean normalize) { var lambda = new ArrayList(); var values = new ArrayList(); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectrum.java b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectrum.java index 09fe1da..09bc817 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectrum.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/spectral/spectrum/Spectrum.java @@ -1,13 +1,18 @@ package eu.jonahbauer.raytracing.render.spectral.spectrum; +import eu.jonahbauer.raytracing.math.Ray; +import eu.jonahbauer.raytracing.math.Vec3; import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; import eu.jonahbauer.raytracing.render.spectral.colors.ColorXYZ; import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum; import eu.jonahbauer.raytracing.render.spectral.SampledWavelengths; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.texture.Texture; +import eu.jonahbauer.raytracing.scene.HitResult; +import eu.jonahbauer.raytracing.scene.SkyBox; import org.jetbrains.annotations.NotNull; -public interface Spectrum { +public interface Spectrum extends Texture, SkyBox { int LAMBDA_MIN = 360; int LAMBDA_MAX = 830; @@ -22,6 +27,10 @@ public interface Spectrum { */ double get(double lambda); + default @NotNull Spectrum scale(double scale) { + return new ScaledSpectrum(this, scale); + } + default @NotNull SampledSpectrum sample(@NotNull SampledWavelengths lambdas) { return new SampledSpectrum(lambdas, this); } @@ -34,8 +43,35 @@ public interface Spectrum { ); } - default @NotNull Color toRGB(@NotNull ColorSpace cs) { + default @NotNull ColorRGB toRGB(@NotNull ColorSpace cs) { return cs.toRGB(toXYZ()); } + /* + * Texture + */ + + @Override + default @NotNull Spectrum get(@NotNull HitResult hit) { + return this; + } + + @Override + default @NotNull Spectrum get(double u, double v, @NotNull Vec3 p) { + return this; + } + + @Override + default boolean isUVRequired() { + return false; + } + + /* + * SkyBox + */ + + @Override + default @NotNull SampledSpectrum getColor(@NotNull Ray ray) { + return this.sample(ray.lambda()); + } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/texture/CheckerTexture.java b/src/main/java/eu/jonahbauer/raytracing/render/texture/CheckerTexture.java index 53bc8c6..a88a533 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/texture/CheckerTexture.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/texture/CheckerTexture.java @@ -1,12 +1,13 @@ package eu.jonahbauer.raytracing.render.texture; import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import org.jetbrains.annotations.NotNull; public record CheckerTexture(double scale, @NotNull Texture even, @NotNull Texture odd) implements Texture { @Override - public @NotNull Color get(double u, double v, @NotNull Vec3 p) { + public @NotNull Spectrum get(double u, double v, @NotNull Vec3 p) { var x = (int) Math.floor(p.x() / scale); var y = (int) Math.floor(p.y() / scale); var z = (int) Math.floor(p.z() / scale); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java b/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java deleted file mode 100644 index fe0465d..0000000 --- a/src/main/java/eu/jonahbauer/raytracing/render/texture/Color.java +++ /dev/null @@ -1,178 +0,0 @@ -package eu.jonahbauer.raytracing.render.texture; - -import eu.jonahbauer.raytracing.math.IVec3; -import eu.jonahbauer.raytracing.math.Ray; -import eu.jonahbauer.raytracing.math.Vec3; -import eu.jonahbauer.raytracing.scene.SkyBox; -import org.jetbrains.annotations.NotNull; - -import java.util.Random; - -import static eu.jonahbauer.raytracing.Main.DEBUG; - -public record Color(double r, double g, double b) implements Texture, SkyBox, IVec3 { - public static final @NotNull Color BLACK = new Color(0.0, 0.0, 0.0); - public static final @NotNull Color WHITE = new Color(1.0, 1.0, 1.0); - - public static @NotNull Color random(@NotNull Random random) { - return new Color(random.nextDouble(), random.nextDouble(), random.nextDouble()); - } - - public static @NotNull Color random(@NotNull Random random, double min, double max) { - var span = max - min; - return new Color( - Math.fma(random.nextDouble(), span, min), - Math.fma(random.nextDouble(), span, min), - Math.fma(random.nextDouble(), span, min) - ); - } - - public static @NotNull Color gamma(@NotNull Color color, double gamma) { - if (gamma == 1.0) { - return color; - } else if (gamma == 2.0) { - return new Color( - Math.sqrt(color.r()), - Math.sqrt(color.g()), - Math.sqrt(color.b()) - ); - } else { - return new Color( - Math.pow(color.r(), 1 / gamma), - Math.pow(color.g(), 1 / gamma), - Math.pow(color.b(), 1 / gamma) - ); - } - } - - public Color(int rgb) { - this((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); - } - - public Color(int red, int green, int blue) { - this(red / 255f, green / 255f, blue / 255f); - } - - public Color { - if (DEBUG && (!Double.isFinite(r) || !Double.isFinite(g) || !Double.isFinite(b))) { - throw new IllegalArgumentException("r, g and b must be finite"); - } - } - - /* - * Math - */ - - public static @NotNull Color average(@NotNull Color current, @NotNull Color next, int index) { - return lerp(current, next, 1d / index); - } - - public static @NotNull Color lerp(@NotNull Color a, @NotNull Color b, double t) { - if (t < 0) return a; - if (t > 1) return b; - return new Color( - Math.fma(t, b.r - a.r, a.r), - Math.fma(t, b.g - a.g, a.g), - Math.fma(t, b.b - a.b, a.b) - ); - } - - public static @NotNull Color fma(@NotNull Color a, @NotNull Color b, @NotNull Color c) { - return new Color( - Math.fma(a.r, b.r, c.r), - Math.fma(a.g, b.g, c.g), - Math.fma(a.b, b.b, c.b) - ); - } - - @Override - public @NotNull Color plus(@NotNull Color other) { - return new Color(r + other.r, g + other.g, b + other.b); - } - - @Override - public @NotNull Color minus(@NotNull Color other) { - return new Color(r - other.r, g - other.g, b - other.b); - } - - @Override - public @NotNull Color times(double d) { - return new Color(r * d, g * d, b * d); - } - - @Override - public @NotNull Color times(@NotNull Color other) { - return new Color(r * other.r, g * other.g, b * other.b); - } - - /* - * Vec3 - */ - - @Override - public @NotNull Vec3 toVec3() { - return new Vec3(r, g, b); - } - - public static @NotNull Color fromVec3(@NotNull Vec3 vec) { - return new Color(vec.x(), vec.y(), vec.z()); - } - - /* - * Accessors - */ - - public int red() { - return toInt(r); - } - - public int green() { - return toInt(g); - } - - public int blue() { - return toInt(b); - } - - private static int toInt(double value) { - return Math.clamp((int) (255.99 * value), 0, 255); - } - - @Override - public double component1() { - return r; - } - - @Override - public double component2() { - return g; - } - - @Override - public double component3() { - return b; - } - - /* - * Texture - */ - - @Override - public @NotNull Color get(double u, double v, @NotNull Vec3 p) { - return this; - } - - @Override - public boolean isUVRequired() { - return false; - } - - /* - * SkyBox - */ - - @Override - public @NotNull Color getColor(@NotNull Ray ray) { - return this; - } -} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/texture/ImageTexture.java b/src/main/java/eu/jonahbauer/raytracing/render/texture/ImageTexture.java index 35e54e6..009f8e4 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/texture/ImageTexture.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/texture/ImageTexture.java @@ -1,7 +1,11 @@ package eu.jonahbauer.raytracing.render.texture; import eu.jonahbauer.raytracing.math.Vec3; -import eu.jonahbauer.raytracing.render.canvas.Image; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorSpace; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBAlbedoSpectrum; +import eu.jonahbauer.raytracing.render.spectral.spectrum.RGBIlluminantSpectrum; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import org.jetbrains.annotations.NotNull; import javax.imageio.ImageIO; @@ -10,22 +14,33 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.util.Objects; -public record ImageTexture(@NotNull Image image) implements Texture { +public final class ImageTexture implements Texture { + private static final String PATH_PREFIX = "/eu/jonahbauer/raytracing/textures/"; - public ImageTexture { - Objects.requireNonNull(image, "image"); - } + private final int width; + private final int height; + private final @NotNull Spectrum[][] spectra; + + public ImageTexture(@NotNull BufferedImage image, @NotNull ColorSpace cs, @NotNull Type type, boolean gamma) { + this.width = image.getWidth(); + this.height = image.getHeight(); + this.spectra = new Spectrum[height][width]; - public ImageTexture(@NotNull BufferedImage image) { - this(new Image(image)); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + var rgb = new ColorRGB(image.getRGB(x, y)); + if (gamma) rgb = ColorRGB.inverseGamma(rgb); + spectra[y][x] = type.newSpectrum(cs, rgb); + } + } } - public ImageTexture(@NotNull String path) { - this(read(path)); + public ImageTexture(@NotNull String path, @NotNull ColorSpace cs) { + this(read(path), cs, Type.ALBEDO, true); } private static @NotNull BufferedImage read(@NotNull String path) { - try (var in = Objects.requireNonNull(ImageTexture.class.getResourceAsStream(path))) { + try (var in = Objects.requireNonNull(ImageTexture.class.getResourceAsStream(PATH_PREFIX + path))) { return ImageIO.read(in); } catch (IOException ex) { throw new UncheckedIOException(ex); @@ -33,11 +48,29 @@ public record ImageTexture(@NotNull Image image) implements Texture { } @Override - public @NotNull Color get(double u, double v, @NotNull Vec3 p) { + public @NotNull Spectrum get(double u, double v, @NotNull Vec3 p) { u = Math.clamp(u, 0, 1); v = 1 - Math.clamp(v, 0, 1); - int x = (int) (u * (image.getWidth() - 1)); - int y = (int) (v * (image.getHeight() - 1)); - return image.get(x, y); + int x = (int) (u * (width - 1)); + int y = (int) (v * (height - 1)); + return spectra[y][x]; + } + + public enum Type { + ALBEDO { + @Override + protected @NotNull Spectrum newSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) { + return new RGBAlbedoSpectrum(cs, rgb); + } + }, + ILLUMINANT { + @Override + protected @NotNull Spectrum newSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb) { + return new RGBIlluminantSpectrum(cs, rgb); + } + }, + ; + + protected abstract @NotNull Spectrum newSpectrum(@NotNull ColorSpace cs, @NotNull ColorRGB rgb); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/render/texture/PerlinTexture.java b/src/main/java/eu/jonahbauer/raytracing/render/texture/PerlinTexture.java index d155d43..3dbb41c 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/texture/PerlinTexture.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/texture/PerlinTexture.java @@ -1,6 +1,8 @@ package eu.jonahbauer.raytracing.render.texture; import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -11,11 +13,11 @@ import java.util.random.RandomGenerator; public final class PerlinTexture implements Texture { private static final int POINT_COUNT = 256; private static final @NotNull Random RANDOM = new Random(); - private static final @NotNull DoubleFunction GREYSCALE = t -> new Color(t, t, t); + private static final @NotNull DoubleFunction GREYSCALE = Spectra.WHITE::scale; private final double scale; private final int turbulence; - private final @NotNull DoubleFunction color; + private final @NotNull DoubleFunction color; private final int mask; private final Vec3[] randvec; @@ -35,12 +37,12 @@ public final class PerlinTexture implements Texture { this(scale, turbulence, GREYSCALE); } - public PerlinTexture(double scale, int turbulence, @NotNull DoubleFunction color) { + public PerlinTexture(double scale, int turbulence, @NotNull DoubleFunction color) { this(scale, turbulence, color, POINT_COUNT, RANDOM); } public PerlinTexture( - double scale, int turbulence, @NotNull DoubleFunction color, + double scale, int turbulence, @NotNull DoubleFunction color, int count, @NotNull RandomGenerator random ) { if ((count & (count - 1)) != 0) throw new IllegalArgumentException("count must be a power of two"); @@ -118,7 +120,7 @@ public final class PerlinTexture implements Texture { } @Override - public @NotNull Color get(double u, double v, @NotNull Vec3 p) { + public @NotNull Spectrum get(double u, double v, @NotNull Vec3 p) { var noise = getNoise(p, turbulence); var t = Math.fma(0.5, Math.sin(Math.PI * noise), 0.5); return color.apply(t); diff --git a/src/main/java/eu/jonahbauer/raytracing/render/texture/Texture.java b/src/main/java/eu/jonahbauer/raytracing/render/texture/Texture.java index fcc9cb0..f63383c 100644 --- a/src/main/java/eu/jonahbauer/raytracing/render/texture/Texture.java +++ b/src/main/java/eu/jonahbauer/raytracing/render/texture/Texture.java @@ -1,6 +1,7 @@ package eu.jonahbauer.raytracing.render.texture; import eu.jonahbauer.raytracing.math.Vec3; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import eu.jonahbauer.raytracing.scene.HitResult; import org.jetbrains.annotations.NotNull; @@ -8,7 +9,7 @@ public interface Texture { /** * {@return the color of this texture for a hit} */ - default @NotNull Color get(@NotNull HitResult hit) { + default @NotNull Spectrum get(@NotNull HitResult hit) { return get(hit.u(), hit.v(), hit.position()); } @@ -18,7 +19,7 @@ public interface Texture { * @param v the texture v coordinate * @param p the position */ - @NotNull Color get(double u, double v, @NotNull Vec3 p); + @NotNull Spectrum get(double u, double v, @NotNull Vec3 p); /** * Returns whether {@link #get(double, double, Vec3)} uses the {@code u} and/or {@code v} parameters. diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/Scene.java b/src/main/java/eu/jonahbauer/raytracing/scene/Scene.java index c3b456c..b7c4af1 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/Scene.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/Scene.java @@ -2,7 +2,8 @@ package eu.jonahbauer.raytracing.scene; import eu.jonahbauer.raytracing.math.AABB; import eu.jonahbauer.raytracing.math.Ray; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectra; import eu.jonahbauer.raytracing.scene.util.HittableBinaryTree; import eu.jonahbauer.raytracing.scene.util.HittableCollection; import org.jetbrains.annotations.NotNull; @@ -23,7 +24,7 @@ public final class Scene extends HittableCollection { } public Scene(@NotNull List objects, @Nullable List targets) { - this(Color.BLACK, objects, targets); + this(Spectra.BLACK, objects, targets); } public Scene(@NotNull SkyBox background, @NotNull List objects) { @@ -54,7 +55,7 @@ public final class Scene extends HittableCollection { return targets; } - public @NotNull Color getBackgroundColor(@NotNull Ray ray) { + public @NotNull SampledSpectrum getBackgroundColor(@NotNull Ray ray) { return background.getColor(ray); } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java b/src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java index 55e2de6..9ece0a8 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/SkyBox.java @@ -1,21 +1,27 @@ package eu.jonahbauer.raytracing.scene; import eu.jonahbauer.raytracing.math.Ray; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.SampledSpectrum; +import eu.jonahbauer.raytracing.render.spectral.spectrum.Spectrum; import org.jetbrains.annotations.NotNull; @FunctionalInterface public interface SkyBox { - @NotNull Color getColor(@NotNull Ray ray); + @NotNull SampledSpectrum getColor(@NotNull Ray ray); - static @NotNull SkyBox gradient(@NotNull Color top, @NotNull Color bottom) { + static @NotNull SkyBox gradient(@NotNull Spectrum top, @NotNull Spectrum bottom) { return ray -> { // altitude from -pi/2 to pi/2 var alt = Math.copySign( Math.acos(ray.direction().withY(0).unit().dot(ray.direction().unit())), ray.direction().y() ); - return Color.lerp(bottom, top, alt / Math.PI + 0.5); + + return SampledSpectrum.lerp( + top.sample(ray.lambda()), + bottom.sample(ray.lambda()), + alt / Math.PI + 0.5 + ); }; } } diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java b/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java index b250185..0ee4074 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/transform/RotateY.java @@ -62,7 +62,7 @@ public sealed class RotateY extends Transform { var newOrigin = transform(origin); var newDirection = transform(direction); - return new Ray(newOrigin, newDirection); + return new Ray(newOrigin, newDirection, ray.lambda()); } @Override diff --git a/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java b/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java index db8a805..0dca035 100644 --- a/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java +++ b/src/main/java/eu/jonahbauer/raytracing/scene/transform/Translate.java @@ -35,7 +35,7 @@ public sealed class Translate extends Transform { @Override protected final @NotNull Ray transform(@NotNull Ray ray) { - return new Ray(ray.origin().minus(offset), ray.direction()); + return new Ray(ray.origin().minus(offset), ray.direction(), ray.lambda()); } @Override diff --git a/src/test/java/eu/jonahbauer/raytracing/render/canvas/ImageTest.java b/src/test/java/eu/jonahbauer/raytracing/render/canvas/RGBCanvasTest.java similarity index 79% rename from src/test/java/eu/jonahbauer/raytracing/render/canvas/ImageTest.java rename to src/test/java/eu/jonahbauer/raytracing/render/canvas/RGBCanvasTest.java index ed360e6..d17277a 100644 --- a/src/test/java/eu/jonahbauer/raytracing/render/canvas/ImageTest.java +++ b/src/test/java/eu/jonahbauer/raytracing/render/canvas/RGBCanvasTest.java @@ -1,6 +1,6 @@ package eu.jonahbauer.raytracing.render.canvas; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; import eu.jonahbauer.raytracing.render.ImageFormat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -13,11 +13,11 @@ import java.util.Objects; import static org.junit.jupiter.api.Assertions.assertEquals; -class ImageTest { +class RGBCanvasTest { @Test void test(@TempDir Path dir) throws IOException { - var image = new Image(256, 256); + var image = new RGBCanvas(256, 256); for (var y = 0; y < image.getHeight(); y++) { for (var x = 0; x < image.getWidth(); x++) { @@ -25,7 +25,7 @@ class ImageTest { var g = (double) y / (image.getHeight() - 1); var b = 0; - image.set(x, y, new Color(r, g, b)); + image.set(x, y, new ColorRGB(r, g, b)); } } @@ -35,7 +35,7 @@ class ImageTest { String expected; String actual; - try (var in = Objects.requireNonNull(ImageTest.class.getResourceAsStream("simple_image.ppm"))) { + try (var in = Objects.requireNonNull(RGBCanvasTest.class.getResourceAsStream("simple_image.ppm"))) { expected = new String(in.readAllBytes(), StandardCharsets.US_ASCII); } diff --git a/src/test/java/eu/jonahbauer/raytracing/scene/hittable3d/SphereTest.java b/src/test/java/eu/jonahbauer/raytracing/scene/hittable3d/SphereTest.java index e4a4ac7..8dc202b 100644 --- a/src/test/java/eu/jonahbauer/raytracing/scene/hittable3d/SphereTest.java +++ b/src/test/java/eu/jonahbauer/raytracing/scene/hittable3d/SphereTest.java @@ -3,7 +3,7 @@ package eu.jonahbauer.raytracing.scene.hittable3d; import eu.jonahbauer.raytracing.math.Range; import eu.jonahbauer.raytracing.math.Ray; import eu.jonahbauer.raytracing.math.Vec3; -import eu.jonahbauer.raytracing.render.texture.Color; +import eu.jonahbauer.raytracing.render.spectral.colors.ColorRGB; import eu.jonahbauer.raytracing.render.material.LambertianMaterial; import org.junit.jupiter.api.Test; @@ -15,7 +15,7 @@ class SphereTest { void hit() { var center = new Vec3(1, 2, 3); var radius = 5; - var sphere = new Sphere(center, radius, new LambertianMaterial(Color.WHITE)); + var sphere = new Sphere(center, radius, new LambertianMaterial(ColorRGB.WHITE)); var origin = new Vec3(6, 7, 8); var direction = new Vec3(-1, -1, -1);