diff --git a/src/main/java/basics/math/algebra/Vector.java b/src/main/java/basics/math/algebra/Vector.java index 85a2412..3f49043 100644 --- a/src/main/java/basics/math/algebra/Vector.java +++ b/src/main/java/basics/math/algebra/Vector.java @@ -105,7 +105,7 @@ public class Vector { } // based on https://bheisler.github.io/post/writing-raytracer-in-rust-part-3/ - public Vector refract(Vector normal, Vector incoming, double ior) { + public static Vector refract(Vector normal, Vector incoming, double ior) { double iDotN = incoming.dot(normal); double etaI = 1; diff --git a/src/main/java/entry/Main.java b/src/main/java/entry/Main.java index 1892daf..e125f7e 100644 --- a/src/main/java/entry/Main.java +++ b/src/main/java/entry/Main.java @@ -11,7 +11,7 @@ public class Main { public static void main(String[] args) { - Resolution resolution = new Resolution(800, 800, 2); + Resolution resolution = new Resolution(800, 800, 4); PlayerController controller = new PlayerController(); diff --git a/src/main/java/geometry/scene/Scene.java b/src/main/java/geometry/scene/Scene.java index ec049b4..360eb4d 100644 --- a/src/main/java/geometry/scene/Scene.java +++ b/src/main/java/geometry/scene/Scene.java @@ -3,17 +3,16 @@ package geometry.scene; import basics.math.algebra.Ray; import basics.math.algebra.Vector; import optics.light.Color; -import optics.light.Directional; import optics.light.LightSource; import optics.light.Point; -import pathtracing.BSDF; +import pathtracing.pbr.Dielectric; +import pathtracing.pbr.Emissive; +import pathtracing.pbr.PhysicallyBasedMaterial; import raytracing.BasicMaterial; import renderer.mesh.BasicMesh; import renderer.mesh.Mesh; -import renderer.primitive.Disk; import renderer.primitive.Plane; import renderer.primitive.Sphere; -import shading.Material; import java.util.ArrayList; import java.util.List; @@ -62,11 +61,11 @@ public class 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); + var red = new Dielectric(new Color(1.0, 0.3, 0.3), 1.0, false, 1.5); + var green = new Dielectric(new Color(0.3, 1.0, 0.3), 1.0, false, 1.5); + var blue = new Dielectric(new Color(0.3, 0.3, 1.0), 1.0, false, 1.5); + var white = new Dielectric(new Color(1.0, 1.0, 1.0), 1.0, false, 1.5); + var light = new Emissive(Color.WHITE, 8.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))); diff --git a/src/main/java/optics/light/Color.java b/src/main/java/optics/light/Color.java index dd88f11..5aa31ff 100644 --- a/src/main/java/optics/light/Color.java +++ b/src/main/java/optics/light/Color.java @@ -105,17 +105,11 @@ public class Color { } public Color lerp(Color other, double k) { - return this.scale(k).add(other.scale(1.0 - k)); + return this.scale(1.0 - k).add(other.scale(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 Color lerp(Color other, Color k) { + return this.mul(Color.WHITE.sub(k)).add(other.mul(k)); } public int getIntRGB() { @@ -126,6 +120,10 @@ public class Color { return 0xff000000 | (r << 16) | (g << 8) | b; } + public double max() { + return Math.max(Math.max(this.r, this.g), this.b); + } + public enum SwizzleMask { rgb, grb, diff --git a/src/main/java/pathtracing/BSDF.java b/src/main/java/pathtracing/BSDF.java deleted file mode 100644 index e6edfb8..0000000 --- a/src/main/java/pathtracing/BSDF.java +++ /dev/null @@ -1,30 +0,0 @@ -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 index fd1e4d1..a86a4b6 100644 --- a/src/main/java/pathtracing/Pathtracer.java +++ b/src/main/java/pathtracing/Pathtracer.java @@ -1,14 +1,14 @@ 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 pathtracing.pbr.Emissive; +import pathtracing.pbr.PhysicallyBasedMaterial; import renderer.Renderer; import java.util.Optional; @@ -30,15 +30,8 @@ public class Pathtracer implements Renderer { 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) { + public Color traceDirect(Ray directRay, int depth) { + if (depth == 4) { return Color.BLACK; } @@ -52,58 +45,9 @@ public class Pathtracer implements Renderer { Vector intersection = directRay.travel(result.get().getDistance()); Vector normal = result.get().getMesh().normalAt(intersection, directRay.getDirection()); - BSDF material = (BSDF) result.get().getMesh().getMaterial(); + var material = (PhysicallyBasedMaterial) 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)); + return material.surfaceColor(normal, directRay.getDirection(), intersection, this, depth + 1); } @Override diff --git a/src/main/java/pathtracing/bdf/BDF.java b/src/main/java/pathtracing/bdf/BDF.java new file mode 100644 index 0000000..7427ef4 --- /dev/null +++ b/src/main/java/pathtracing/bdf/BDF.java @@ -0,0 +1,44 @@ +package pathtracing.bdf; + +import basics.math.algebra.Basis; +import basics.math.algebra.Vector; + +public abstract class BDF { + + public static Vector sampleBTDF(Vector normal, Vector incoming, double roughness, double ior) { + Vector up = Vector.refract(normal, incoming, ior).lerp(normal.negate(), roughness); + + Vector hemisphereSample = BDF.cosineHemisphere(roughness); + + var orthnormalBasis = Basis.constructOrthonormalBasis(up); + + return orthnormalBasis.vectors[0].scale(hemisphereSample.z) + .add(orthnormalBasis.vectors[1].scale(hemisphereSample.x)) + .add(orthnormalBasis.vectors[2].scale(hemisphereSample.y)); + } + + public static Vector sampleBRDF(Vector normal, Vector incoming, double roughness) { + Vector up = incoming.reflect(normal).lerp(normal, roughness); + + Vector hemisphereSample = BDF.cosineHemisphere(roughness); + + var orthnormalBasis = Basis.constructOrthonormalBasis(up); + + return orthnormalBasis.vectors[0].scale(hemisphereSample.z) + .add(orthnormalBasis.vectors[1].scale(hemisphereSample.x)) + .add(orthnormalBasis.vectors[2].scale(hemisphereSample.y)); + } + + public static Vector cosineHemisphere(double maximumAngle) { + double u1 = Math.random(); + double u2 = Math.random() * maximumAngle; + + 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)); + } +} diff --git a/src/main/java/pathtracing/bdf/BRDF.java b/src/main/java/pathtracing/bdf/BRDF.java new file mode 100644 index 0000000..79b0a35 --- /dev/null +++ b/src/main/java/pathtracing/bdf/BRDF.java @@ -0,0 +1,19 @@ +package pathtracing.bdf; + +import basics.math.algebra.Basis; +import basics.math.algebra.Vector; + +public class BRDF { + + public static Vector sampleDirection(Vector incoming, Vector normal, double roughness) { + Vector hemisphereSample = BDF.cosineHemisphere(roughness); + + var direction = incoming.reflect(normal).lerp(normal, roughness); + + var orthnormalBasis = Basis.constructOrthonormalBasis(direction); + + return orthnormalBasis.vectors[0].scale(hemisphereSample.z) + .add(orthnormalBasis.vectors[1].scale(hemisphereSample.x)) + .add(orthnormalBasis.vectors[2].scale(hemisphereSample.y)); + } +} diff --git a/src/main/java/pathtracing/bdf/BTDF.java b/src/main/java/pathtracing/bdf/BTDF.java new file mode 100644 index 0000000..f4d4d56 --- /dev/null +++ b/src/main/java/pathtracing/bdf/BTDF.java @@ -0,0 +1,20 @@ +package pathtracing.bdf; + +import basics.math.algebra.Basis; +import basics.math.algebra.Vector; + +public class BTDF { + + public static Vector sampleDirection(Vector incoming, Vector normal, double roughness, double ior) { + Vector hemisphereSample = BDF.cosineHemisphere(roughness); + + var direction = Vector.refract(normal, incoming, ior) + .lerp(normal.negate(), roughness); + + var orthnormalBasis = Basis.constructOrthonormalBasis(direction); + + return orthnormalBasis.vectors[0].scale(hemisphereSample.z) + .add(orthnormalBasis.vectors[1].scale(hemisphereSample.x)) + .add(orthnormalBasis.vectors[2].scale(hemisphereSample.y)); + } +} diff --git a/src/main/java/pathtracing/pbr/Dielectric.java b/src/main/java/pathtracing/pbr/Dielectric.java new file mode 100644 index 0000000..8119b82 --- /dev/null +++ b/src/main/java/pathtracing/pbr/Dielectric.java @@ -0,0 +1,49 @@ +package pathtracing.pbr; + +import basics.math.algebra.Basis; +import basics.math.algebra.Ray; +import basics.math.algebra.Vector; +import optics.light.Color; +import pathtracing.Pathtracer; +import pathtracing.bdf.BDF; + +public class Dielectric extends PhysicallyBasedMaterial { + + private final Color reflectance; + private final double roughness; + private final boolean transmissive; + private final double ior; + + public Dielectric(Color reflectance, double roughness, boolean transmissive, double ior) { + this.reflectance = reflectance; + this.roughness = roughness; + this.transmissive = transmissive; + this.ior = ior; + } + + private Color schlickFresnel(double cosTheta) { + return reflectance.lerp(Color.WHITE, Math.pow(1.0 - cosTheta, 5.0)); + } + + @Override + public Color surfaceColor(Vector normal, Vector incident, Vector intersection, Pathtracer pathtracer, int depth) { + var cosTheta = Math.abs(incident.dot(normal)); + + var schlick = schlickFresnel(cosTheta).scale(1.0 - roughness); + + var reflectionRay = new Ray(intersection, BDF.sampleBRDF(normal, incident, roughness), 1e-3, 1e3); + var reflectedColor = pathtracer.traceDirect(reflectionRay, depth); + + //if (transmissive) { + // var refractionRay = new Ray(intersection, BDF.sampleBTDF(normal, incident, roughness, ior), 1e-3, 1e3); + // var refractionColor = reflectance.mul(pathtracer.traceDirect(refractionRay, depth)); + + // return refractionColor.lerp(reflectedColor, schlick); + //} else { + var diffuseRay = new Ray(intersection, BDF.sampleBRDF(normal, incident, 1.0), 1e-3, 1e3); + var diffuseColor = reflectance.mul(pathtracer.traceDirect(diffuseRay, depth)); + + return diffuseColor.lerp(reflectedColor, schlick); + // } + } +} diff --git a/src/main/java/pathtracing/pbr/Emissive.java b/src/main/java/pathtracing/pbr/Emissive.java new file mode 100644 index 0000000..3890d18 --- /dev/null +++ b/src/main/java/pathtracing/pbr/Emissive.java @@ -0,0 +1,21 @@ +package pathtracing.pbr; + +import basics.math.algebra.Vector; +import optics.light.Color; +import pathtracing.Pathtracer; + +public class Emissive extends PhysicallyBasedMaterial { + + private final Color emission; + private final double strength; + + public Emissive(Color emission, double strength) { + this.emission = emission; + this.strength = strength; + } + + @Override + public Color surfaceColor(Vector normal, Vector incident, Vector intersection, Pathtracer pathtracer, int depth) { + return emission.scale(strength); + } +} diff --git a/src/main/java/pathtracing/pbr/PhysicallyBasedMaterial.java b/src/main/java/pathtracing/pbr/PhysicallyBasedMaterial.java new file mode 100644 index 0000000..90ab4e7 --- /dev/null +++ b/src/main/java/pathtracing/pbr/PhysicallyBasedMaterial.java @@ -0,0 +1,11 @@ +package pathtracing.pbr; + +import basics.math.algebra.Vector; +import optics.light.Color; +import pathtracing.Pathtracer; +import shading.Material; + +public abstract class PhysicallyBasedMaterial implements Material { + + public abstract Color surfaceColor(Vector normal, Vector incident, Vector intersection, Pathtracer pathtracer, int depth); +} diff --git a/src/main/java/renderer/Scheduler.java b/src/main/java/renderer/Scheduler.java index 5d8671c..9f4b8fd 100644 --- a/src/main/java/renderer/Scheduler.java +++ b/src/main/java/renderer/Scheduler.java @@ -13,7 +13,7 @@ public class Scheduler implements AutoCloseable { private static final Scheduler INSTANCE = new Scheduler(); - private ExecutorService executorService = Executors.newCachedThreadPool(); + private final ExecutorService executorService = Executors.newCachedThreadPool(); private Scheduler() { } @@ -34,7 +34,7 @@ public class Scheduler implements AutoCloseable { int tileWidth = buffer.getWidth() / THREADS_PER_SIDE; int tileHeight = buffer.getHeight() / THREADS_PER_SIDE; - ArrayList processors = new ArrayList(totalThreads); + var processors = new ArrayList(totalThreads); for (int x = 0; x < THREADS_PER_SIDE; x++) { int x0 = x * tileWidth; diff --git a/src/main/java/renderer/canvas/ContributionBuffer.java b/src/main/java/renderer/canvas/ContributionBuffer.java index fd16921..7b87bc2 100644 --- a/src/main/java/renderer/canvas/ContributionBuffer.java +++ b/src/main/java/renderer/canvas/ContributionBuffer.java @@ -1,17 +1,18 @@ package renderer.canvas; -import basics.math.algebra.Vector; import optics.light.Color; +import java.util.Arrays; + public class ContributionBuffer { // linear rgb colors - private volatile Color[][] buffer; + private final Color[][] buffer; // amount of samples for every pixel - private volatile double[][] samples; + private final double[][] samples; - private int width; - private int height; + private final int width; + private final int height; private int maxSample; @@ -22,15 +23,17 @@ public class ContributionBuffer { this.buffer = new Color[width][height]; this.samples = new double[width][height]; + for (int i = 0; i < width; i++) { + Arrays.fill(this.samples[i], 1.0); + } + for (int x = 0; x < width; x++) { - for (int y = 0; y < height; y++) { - this.buffer[x][y] = Color.BLACK; - } + Arrays.fill(this.buffer[x], Color.BLACK); } } public void contribute(int x, int y, Color color) { - buffer[x][y] = buffer[x][y].add(color); + buffer[x][y] = buffer[x][y].lerp(color, 1.0 / samples[x][y]); samples[x][y]++; maxSample = Math.max(maxSample, (int) samples[x][y]); @@ -47,15 +50,13 @@ public class ContributionBuffer { Color color = buffer[x][y]; color.r = color.g = color.b = 0; // reset sample count - samples[x][y] = 0; + samples[x][y] = 1; } } } private int getRGB(int x, int y) { - Color linearRGB = buffer[x][y].scale(1.0 / samples[x][y]); - - linearRGB.colorCorrect(); + Color linearRGB = buffer[x][y]; int red = (int) (linearRGB.r* 255.0); int green = (int) (linearRGB.g * 255.0);