diff --git a/.idea/jpath.iml b/.idea/jpath.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/jpath.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/basics/math/algebra/Basis.java b/src/main/java/basics/math/algebra/Basis.java new file mode 100644 index 0000000..9ab97f1 --- /dev/null +++ b/src/main/java/basics/math/algebra/Basis.java @@ -0,0 +1,36 @@ +package basics.math.algebra; + +/** + * Basis of an vector space. + * This class owns the components of a vector space. + */ +public class Basis { + + public Vector[] vectors; + + public Basis(Vector[] vectors) { + this.vectors = vectors; + } + + public Basis(Vector a, Vector b, Vector c) { + this.vectors = new Vector[]{a, b, c}; + } + + /** + * Constructs an orthonormal basis using the gram-schmidt formula. + * It will utilize the supplied up vector as the first basis vector, + * building the other two vectors from swizzling the up vectors components. + * + * @param up the first vector of this basis + * @return an orthonormal basis with up and 2 additional orthonormal vectors + */ + public static Basis constructOrthonormalBasis(Vector up) { + Vector n2 = Vector.diagonal(1).sub(up); + Vector n3 = Vector.diagonal(1).sub(up.shuffle(Vector.SwizzleMask.ZXY)); + + Vector w2 = n2.sub(up.project(n2)).normalize(); + Vector w3 = n3.sub(up.project(n3)).sub(w2.project(n3)).normalize(); + + return new Basis(up, w2, w3); + } +} diff --git a/src/main/java/basics/math/algebra/Vector.java b/src/main/java/basics/math/algebra/Vector.java index e0bd4b6..85a2412 100644 --- a/src/main/java/basics/math/algebra/Vector.java +++ b/src/main/java/basics/math/algebra/Vector.java @@ -96,6 +96,10 @@ public class Vector { ); } + public Vector project(Vector other) { + return this.scale(other.dot(this) / this.dot(this)); + } + public Vector reflect(Vector normal) { return this.sub(normal.scale(this.dot(normal) * 2.0)); } diff --git a/src/main/java/entry/Main.java b/src/main/java/entry/Main.java index 808f7a6..c555a9b 100644 --- a/src/main/java/entry/Main.java +++ b/src/main/java/entry/Main.java @@ -1,21 +1,22 @@ package entry; import optics.view.PlayerController; -import raytracing.Raytracer; +import pathtracing.Pathtracer; import renderer.Display; +import renderer.Renderer; import renderer.Resolution; public class Main { public static void main(String[] args) { - Resolution resolution = new Resolution(800, 800, 3); + Resolution resolution = new Resolution(800, 800, 6); PlayerController controller = new PlayerController(); Display display = new Display(resolution, controller); - Raytracer raytracer = new Raytracer(); + Renderer raytracer = new Pathtracer(); display.start(raytracer, controller); } diff --git a/src/main/java/geometry/scene/Scene.java b/src/main/java/geometry/scene/Scene.java index 748ea1c..ec049b4 100644 --- a/src/main/java/geometry/scene/Scene.java +++ b/src/main/java/geometry/scene/Scene.java @@ -6,6 +6,7 @@ import optics.light.Color; import optics.light.Directional; import optics.light.LightSource; import optics.light.Point; +import pathtracing.BSDF; import raytracing.BasicMaterial; import renderer.mesh.BasicMesh; import renderer.mesh.Mesh; @@ -29,7 +30,7 @@ public class Scene { this.lights = new ArrayList<>(); } - public static Scene generateExampleScene() { + public static Scene generateRaytracingExampleScene() { Scene scene = new Scene(new LinearList()); BasicMaterial glass = new BasicMaterial(Color.BLACK, 1.0, Color.WHITE, true); @@ -58,6 +59,33 @@ public class Scene { return scene; } + public static Scene generatePathtracingExampleScene() { + Scene scene = new Scene(new LinearList()); + + BSDF red = new BSDF(new Color(1, 0.3, 0.3), new Color(0), 1.0); + BSDF green = new BSDF(new Color(0.3,1, 0.3), new Color(0), 1); + BSDF blue = new BSDF(new Color(0.3, 0.3, 1), new Color(0), 1); + BSDF white = new BSDF(new Color(1, 1, 1), new Color(0), 1); + BSDF light = new BSDF(Color.BLACK, new Color(300), 1.0); + + scene.addMesh(new BasicMesh(white, new Sphere(new Vector(0,0,0), 1.0))); + scene.addMesh(new BasicMesh(white, new Sphere(new Vector(3,2,1), 1.0))); + + scene.addMesh(new BasicMesh(white, new Plane(new Vector(0, 1, 0), 1))); + scene.addMesh(new BasicMesh(white, new Plane(new Vector(0, 1, 0), -6))); + + scene.addMesh(new BasicMesh(red, new Plane(new Vector(-1, 0, 0), 4))); + scene.addMesh(new BasicMesh(green, new Plane(new Vector(-1, 0, 0), -4))); + + scene.addMesh(new BasicMesh(blue, new Plane(new Vector(0, 0, 1), 5))); + scene.addMesh(new BasicMesh(white, new Plane(new Vector(0, 0, 1), -4))); + + scene.addMesh(new BasicMesh(light, new Sphere(new Vector(0,6.0,0), 1.0))); + //scene.addLight(new Point(Color.WHITE.scale(1.0), new Vector(0, 4, 0))); + + return scene; + } + public void addLight(LightSource light) { this.lights.add(light); } diff --git a/src/main/java/optics/light/Color.java b/src/main/java/optics/light/Color.java index 0e83332..dd88f11 100644 --- a/src/main/java/optics/light/Color.java +++ b/src/main/java/optics/light/Color.java @@ -108,6 +108,24 @@ public class Color { return this.scale(k).add(other.scale(1.0 - k)); } + public void colorCorrect() { + this.r = tonemap(this.r); + this.g = tonemap(this.g); + this.b = tonemap(this.b); + } + + private double tonemap(double k) { + return -1 / (k + 1) + 1; + } + + public int getIntRGB() { + int r = (int) (this.r * 255); + int g = (int) (this.g * 255); + int b = (int) (this.b * 255); + + return 0xff000000 | (r << 16) | (g << 8) | b; + } + public enum SwizzleMask { rgb, grb, diff --git a/src/main/java/pathtracing/BSDF.java b/src/main/java/pathtracing/BSDF.java new file mode 100644 index 0000000..e6edfb8 --- /dev/null +++ b/src/main/java/pathtracing/BSDF.java @@ -0,0 +1,30 @@ +package pathtracing; + +import optics.light.Color; +import shading.Material; + +public class BSDF implements Material { + + private Color reflectance; + private Color emission; + + private double roughness; + + public BSDF(Color reflectance, Color emission, double roughness) { + this.reflectance = reflectance; + this.emission = emission; + this.roughness = roughness; + } + + public Color getReflectance() { + return reflectance; + } + + public Color getEmission() { + return emission; + } + + public double getRoughness() { + return roughness; + } +} diff --git a/src/main/java/pathtracing/Pathtracer.java b/src/main/java/pathtracing/Pathtracer.java new file mode 100644 index 0000000..fd1e4d1 --- /dev/null +++ b/src/main/java/pathtracing/Pathtracer.java @@ -0,0 +1,120 @@ +package pathtracing; + +import basics.math.algebra.Basis; +import basics.math.algebra.Ray; +import basics.math.algebra.Vector; +import geometry.scene.Hit; +import geometry.scene.Scene; +import optics.light.Color; +import optics.light.LightSource; +import optics.view.Camera; +import optics.view.PinholeCamera; +import renderer.Renderer; + +import java.util.Optional; + +public class Pathtracer implements Renderer { + + private final Camera camera = new PinholeCamera(new Vector(0,0,-4), Vector.origin(), 90, 1e-3, 1e3); + private final Scene scene = Scene.generatePathtracingExampleScene(); + + private double traceShadow(Vector point, Vector direction, double distance) { + Ray shadowRay = new Ray(point, direction, 1e-3, distance); + + Optional result = scene.intersect(shadowRay); + + if (result.isEmpty()) { + return 1.0; + } + + return 0.0; + } + + private double castShadow(LightSource target, Vector point, Vector surfaceNormal) { + Vector lightDirection = target.getDirection(point); + double distance = target.getDistance(point); + double shadow = traceShadow(point, lightDirection, distance); + return Math.max(surfaceNormal.dot(lightDirection), 0.0) * shadow; + } + + private Color traceDirect(Ray directRay, int depth) { + if (depth == 8) { + return Color.BLACK; + } + + Optional result = scene.intersect(directRay); + + if (result.isEmpty()) { + // Background color + return Color.BLACK; + } + + Vector intersection = directRay.travel(result.get().getDistance()); + Vector normal = result.get().getMesh().normalAt(intersection, directRay.getDirection()); + + BSDF material = (BSDF) result.get().getMesh().getMaterial(); + + var diffuse = diffuseCosineWeightedBRDF(intersection, normal, material, depth); + + return diffuse.add(material.getEmission()); + } + + private Color diffuseCosineWeightedBRDF(Vector intersection, Vector normal, BSDF bsdf, int depth) { + var next = new Ray(intersection, sampleNormalHemisphere(normal, bsdf.getRoughness()), 1e-3, 1e3); + + var cosTheta = next.getDirection().dot(normal); + + var pdf = cosTheta / Math.PI; + + var indirectLight = traceDirect(next, depth + 1).scale(pdf); + + // direct light + + Color directLight = Color.BLACK; + + for (LightSource light : scene.getLights()) { + double directInfluence = castShadow(light, intersection, normal); + + Color incoming = bsdf.getReflectance().scale(directInfluence).mul(light.getColor()); + + directLight = directLight.add(incoming); + } + + return bsdf.getReflectance().mul(indirectLight.add(directLight)); + } + + private Vector cosineHemisphere() { + double u1 = Math.random(); + double u2 = Math.random(); + + double r = Math.sqrt(u1); + double theta = 2 * Math.PI * u2; + + double x = r * Math.cos(theta); + double y = r * Math.sin(theta); + + return new Vector(x, y, Math.sqrt(1 - u1)); + } + + private Vector sampleNormalHemisphere(Vector normal, double roughness) { + Vector hemisphereSample = cosineHemisphere(); + + var orthnormalBasis = Basis.constructOrthonormalBasis(normal); + + return orthnormalBasis.vectors[0].scale(hemisphereSample.z) + .add(orthnormalBasis.vectors[1].scale(hemisphereSample.x)) + .add(orthnormalBasis.vectors[2].scale(hemisphereSample.y)); + } + + @Override + public Color traceCameraRay(double u, double v) { + var cameraRay = camera.generateViewRay(u, v); + + return traceDirect(cameraRay, 0); + } + + @Override + public Camera getCamera() { + return camera; + } +} diff --git a/src/main/java/raytracing/Raytracer.java b/src/main/java/raytracing/Raytracer.java index 0d22992..3ca07f6 100644 --- a/src/main/java/raytracing/Raytracer.java +++ b/src/main/java/raytracing/Raytracer.java @@ -14,8 +14,8 @@ import java.util.Optional; public class Raytracer implements Renderer { - private Camera camera = new PinholeCamera(new Vector(0,0,-4), Vector.origin(), 90, 1e-3, 1e3); - private Scene scene = Scene.generateExampleScene(); + private final Camera camera = new PinholeCamera(new Vector(0,0,-4), Vector.origin(), 90, 1e-3, 1e3); + private final Scene scene = Scene.generateRaytracingExampleScene(); private double traceShadow(Vector point, Vector direction, double distance) { Ray shadowRay = new Ray(point, direction, 1e-3, distance); @@ -50,7 +50,12 @@ public class Raytracer implements Renderer { Color incoming = new Color(0,0,0); if (material.refracts()) { - return refractedLight(directRay, material, normal, intersection, depth); + var reflection = reflectedLight(directRay, material, normal, intersection, depth); + var refraction = refractedLight(directRay, material, normal, intersection, depth); + + var fac = -directRay.getDirection().dot(normal); + + return refraction.lerp(reflection, Math.pow(fac, 0.5)); } Color reflectedLight = reflectedLight(directRay, material, normal, intersection, depth); @@ -77,9 +82,7 @@ public class Raytracer implements Renderer { private Color refractedLight(Ray incomingRay, BasicMaterial material, Vector normal, Vector point, int depth) { Vector refracted = incomingRay.getDirection().refract(normal, incomingRay.getDirection(), 1.45); - Color refractedLight = traceDirect(new Ray(point, refracted, 1e-3, incomingRay.getFar()), depth + 1); - - return refractedLight; + return traceDirect(new Ray(point, refracted, 1e-3, incomingRay.getFar()), depth + 1); } private double castShadow(LightSource target, Vector point, Vector surfaceNormal) { diff --git a/src/main/java/renderer/Display.java b/src/main/java/renderer/Display.java index e55d94b..4cd774b 100644 --- a/src/main/java/renderer/Display.java +++ b/src/main/java/renderer/Display.java @@ -4,9 +4,12 @@ import optics.light.Color; import optics.view.PlayerController; import renderer.canvas.RenderTarget; +import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; import java.awt.event.*; +import java.awt.image.RenderedImage; +import java.io.File; public class Display { @@ -19,6 +22,7 @@ public class Display { private int sampleCount; private long frametime; + private long startTime; public Display(Resolution resolution, PlayerController controller) { this.resolution = resolution; @@ -86,9 +90,16 @@ public class Display { try (Scheduler scheduler = Scheduler.getInstance()) { var tasks = scheduler.generateTasks(this.target, program); + startTime = System.currentTimeMillis(); + while (!this.close) { this.frametime = System.currentTimeMillis(); + if (System.currentTimeMillis() - startTime > 1000 * 60 * 20) { + ImageIO.write(this.target.getImage(), "png", new File("images/" + System.currentTimeMillis() + ".png")); + System.exit(0); + } + if (resized) { this.resolution.setSize(window.getSize()); // Window has been resized, rebuild drawing buffer @@ -128,5 +139,6 @@ public class Display { overlay.setThreads(); overlay.setFrametime(this.frametime); overlay.setSamples(sampleCount); + overlay.setTime(System.currentTimeMillis() - startTime); } } diff --git a/src/main/java/renderer/TileProcessor.java b/src/main/java/renderer/TileProcessor.java index 173e14d..cef272a 100644 --- a/src/main/java/renderer/TileProcessor.java +++ b/src/main/java/renderer/TileProcessor.java @@ -1,22 +1,20 @@ package renderer; -import basics.math.algebra.Vector; import renderer.canvas.ContributionBuffer; - import java.util.concurrent.Callable; public class TileProcessor implements Callable { private final ContributionBuffer buffer; - private FragmentProgram program; + private final FragmentProgram program; - private int width; - private int height; - private int x0; - private int y0; - private int x1; - private int y1; + private final int width; + private final int height; + private final int x0; + private final int y0; + private final int x1; + private final int y1; public TileProcessor(ContributionBuffer buffer, int x0, int y0, int x1, int y1, int width, int height, FragmentProgram program) { this.buffer = buffer; @@ -30,13 +28,15 @@ public class TileProcessor implements Callable { } @Override - public Void call() throws Exception { + public Void call() { + double widthTransform = 1.0 / (double) this.width * 2.0; + double heightTransform = 1.0 / (double) this.height * 2.0; for (int x = this.x0; x < x1; x++) { - double u = (x + Math.random()) / (double) this.width * 2.0 - 1.0; + double u = (x + Math.random()) * widthTransform - 1.0; for (int y = this.y0; y < y1; y++) { - double v = (y + Math.random()) / (double) this.height * 2.0 - 1.0; + double v = (y + Math.random()) * heightTransform - 1.0; this.buffer.contribute(x, y, this.program.fragment(u, v)); } diff --git a/src/main/java/renderer/canvas/ContributionBuffer.java b/src/main/java/renderer/canvas/ContributionBuffer.java index 81ccfee..fd16921 100644 --- a/src/main/java/renderer/canvas/ContributionBuffer.java +++ b/src/main/java/renderer/canvas/ContributionBuffer.java @@ -13,6 +13,8 @@ public class ContributionBuffer { private int width; private int height; + private int maxSample; + public ContributionBuffer(int width, int height) { this.width = width; this.height = height; @@ -30,6 +32,8 @@ public class ContributionBuffer { public void contribute(int x, int y, Color color) { buffer[x][y] = buffer[x][y].add(color); samples[x][y]++; + + maxSample = Math.max(maxSample, (int) samples[x][y]); } /** @@ -51,6 +55,8 @@ public class ContributionBuffer { private int getRGB(int x, int y) { Color linearRGB = buffer[x][y].scale(1.0 / samples[x][y]); + linearRGB.colorCorrect(); + int red = (int) (linearRGB.r* 255.0); int green = (int) (linearRGB.g * 255.0); int blue = (int) (linearRGB.b * 255.0); @@ -62,6 +68,20 @@ public class ContributionBuffer { return 0xff000000 | red << 16 | green << 8 | blue; } + public Color getPixel(int x, int y) { + return buffer[x][y].scale(1.0 / samples[x][y]); + } + + public void visualizeSamples(int[] buffer) { + for (int y = 0; y < height; y++) { + int col = y * width; + + for (int x = 0; x < width; x++) { + buffer[col + x] = Color.diagonal(samples[x][y] / maxSample).getIntRGB(); + } + } + } + public void blit(int[] buffer) { for (int y = 0; y < height; y++) { int col = y * width; diff --git a/src/main/java/renderer/canvas/RenderTarget.java b/src/main/java/renderer/canvas/RenderTarget.java index 9f6ff16..620e7aa 100644 --- a/src/main/java/renderer/canvas/RenderTarget.java +++ b/src/main/java/renderer/canvas/RenderTarget.java @@ -1,6 +1,5 @@ package renderer.canvas; -import optics.view.PlayerController; import renderer.Resolution; import renderer.Scheduler; import renderer.debug.DebugOverlay; @@ -9,6 +8,7 @@ import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; +import java.awt.image.RenderedImage; public class RenderTarget extends JPanel { @@ -70,4 +70,8 @@ public class RenderTarget extends JPanel { public DebugOverlay getOverlay() { return overlay; } + + public RenderedImage getImage() { + return this.target; + } } diff --git a/src/main/java/renderer/debug/DebugOverlay.form b/src/main/java/renderer/debug/DebugOverlay.form index 2943617..0926262 100644 --- a/src/main/java/renderer/debug/DebugOverlay.form +++ b/src/main/java/renderer/debug/DebugOverlay.form @@ -8,7 +8,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -117,6 +117,22 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/java/renderer/debug/DebugOverlay.java b/src/main/java/renderer/debug/DebugOverlay.java index 8392426..1ad3f0f 100644 --- a/src/main/java/renderer/debug/DebugOverlay.java +++ b/src/main/java/renderer/debug/DebugOverlay.java @@ -14,6 +14,7 @@ public class DebugOverlay { private JLabel samples; private JPanel root; private JLabel downScale; + private JLabel time; public void setup(){ this.root.setOpaque(false); @@ -44,4 +45,8 @@ public class DebugOverlay { public JPanel getRoot() { return root; } + + public void setTime(long l) { + this.time.setText(String.format("%.1fs", l * 1e-3)); + } } diff --git a/src/main/java/renderer/mesh/BasicMesh.java b/src/main/java/renderer/mesh/BasicMesh.java index a444ce6..e831d56 100644 --- a/src/main/java/renderer/mesh/BasicMesh.java +++ b/src/main/java/renderer/mesh/BasicMesh.java @@ -8,11 +8,11 @@ import shading.Material; public class BasicMesh extends Mesh { - private BasicMaterial material; + private Material material; private Primitive shape; - public BasicMesh(BasicMaterial material, Primitive shape) { + public BasicMesh(Material material, Primitive shape) { this.material = material; this.shape = shape; }