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;
}