added physically based materials

This commit is contained in:
Sven Vogel 2023-05-02 18:30:05 +02:00
parent e97416c8ea
commit efc094e8cd
14 changed files with 203 additions and 127 deletions

View File

@ -105,7 +105,7 @@ public class Vector {
} }
// based on https://bheisler.github.io/post/writing-raytracer-in-rust-part-3/ // 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 iDotN = incoming.dot(normal);
double etaI = 1; double etaI = 1;

View File

@ -11,7 +11,7 @@ public class Main {
public static void main(String[] args) { public static void main(String[] args) {
Resolution resolution = new Resolution(800, 800, 2); Resolution resolution = new Resolution(800, 800, 4);
PlayerController controller = new PlayerController(); PlayerController controller = new PlayerController();

View File

@ -3,17 +3,16 @@ package geometry.scene;
import basics.math.algebra.Ray; import basics.math.algebra.Ray;
import basics.math.algebra.Vector; import basics.math.algebra.Vector;
import optics.light.Color; import optics.light.Color;
import optics.light.Directional;
import optics.light.LightSource; import optics.light.LightSource;
import optics.light.Point; import optics.light.Point;
import pathtracing.BSDF; import pathtracing.pbr.Dielectric;
import pathtracing.pbr.Emissive;
import pathtracing.pbr.PhysicallyBasedMaterial;
import raytracing.BasicMaterial; import raytracing.BasicMaterial;
import renderer.mesh.BasicMesh; import renderer.mesh.BasicMesh;
import renderer.mesh.Mesh; import renderer.mesh.Mesh;
import renderer.primitive.Disk;
import renderer.primitive.Plane; import renderer.primitive.Plane;
import renderer.primitive.Sphere; import renderer.primitive.Sphere;
import shading.Material;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -62,11 +61,11 @@ public class Scene {
public static Scene generatePathtracingExampleScene() { public static Scene generatePathtracingExampleScene() {
Scene scene = new Scene(new LinearList()); Scene scene = new Scene(new LinearList());
BSDF red = new BSDF(new Color(1, 0.3, 0.3), new Color(0), 1.0); var red = new Dielectric(new Color(1.0, 0.3, 0.3), 1.0, false, 1.5);
BSDF green = new BSDF(new Color(0.3,1, 0.3), new Color(0), 1); var green = new Dielectric(new Color(0.3, 1.0, 0.3), 1.0, false, 1.5);
BSDF blue = new BSDF(new Color(0.3, 0.3, 1), new Color(0), 1); var blue = new Dielectric(new Color(0.3, 0.3, 1.0), 1.0, false, 1.5);
BSDF white = new BSDF(new Color(1, 1, 1), new Color(0), 1); var white = new Dielectric(new Color(1.0, 1.0, 1.0), 1.0, false, 1.5);
BSDF light = new BSDF(Color.BLACK, new Color(300), 1.0); 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(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 Sphere(new Vector(3,2,1), 1.0)));

View File

@ -105,17 +105,11 @@ public class Color {
} }
public Color lerp(Color other, double k) { 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() { public Color lerp(Color other, Color k) {
this.r = tonemap(this.r); return this.mul(Color.WHITE.sub(k)).add(other.mul(k));
this.g = tonemap(this.g);
this.b = tonemap(this.b);
}
private double tonemap(double k) {
return -1 / (k + 1) + 1;
} }
public int getIntRGB() { public int getIntRGB() {
@ -126,6 +120,10 @@ public class Color {
return 0xff000000 | (r << 16) | (g << 8) | b; return 0xff000000 | (r << 16) | (g << 8) | b;
} }
public double max() {
return Math.max(Math.max(this.r, this.g), this.b);
}
public enum SwizzleMask { public enum SwizzleMask {
rgb, rgb,
grb, grb,

View File

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

View File

@ -1,14 +1,14 @@
package pathtracing; package pathtracing;
import basics.math.algebra.Basis;
import basics.math.algebra.Ray; import basics.math.algebra.Ray;
import basics.math.algebra.Vector; import basics.math.algebra.Vector;
import geometry.scene.Hit; import geometry.scene.Hit;
import geometry.scene.Scene; import geometry.scene.Scene;
import optics.light.Color; import optics.light.Color;
import optics.light.LightSource;
import optics.view.Camera; import optics.view.Camera;
import optics.view.PinholeCamera; import optics.view.PinholeCamera;
import pathtracing.pbr.Emissive;
import pathtracing.pbr.PhysicallyBasedMaterial;
import renderer.Renderer; import renderer.Renderer;
import java.util.Optional; import java.util.Optional;
@ -30,15 +30,8 @@ public class Pathtracer implements Renderer {
return 0.0; return 0.0;
} }
private double castShadow(LightSource target, Vector point, Vector surfaceNormal) { public Color traceDirect(Ray directRay, int depth) {
Vector lightDirection = target.getDirection(point); if (depth == 4) {
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; return Color.BLACK;
} }
@ -52,58 +45,9 @@ public class Pathtracer implements Renderer {
Vector intersection = directRay.travel(result.get().getDistance()); Vector intersection = directRay.travel(result.get().getDistance());
Vector normal = result.get().getMesh().normalAt(intersection, directRay.getDirection()); 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 material.surfaceColor(normal, directRay.getDirection(), intersection, this, depth + 1);
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 @Override

View File

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

View File

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

View File

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

View File

@ -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);
// }
}
}

View File

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

View File

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

View File

@ -13,7 +13,7 @@ public class Scheduler implements AutoCloseable {
private static final Scheduler INSTANCE = new Scheduler(); private static final Scheduler INSTANCE = new Scheduler();
private ExecutorService executorService = Executors.newCachedThreadPool(); private final ExecutorService executorService = Executors.newCachedThreadPool();
private Scheduler() { private Scheduler() {
} }
@ -34,7 +34,7 @@ public class Scheduler implements AutoCloseable {
int tileWidth = buffer.getWidth() / THREADS_PER_SIDE; int tileWidth = buffer.getWidth() / THREADS_PER_SIDE;
int tileHeight = buffer.getHeight() / 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++) { for (int x = 0; x < THREADS_PER_SIDE; x++) {
int x0 = x * tileWidth; int x0 = x * tileWidth;

View File

@ -1,17 +1,18 @@
package renderer.canvas; package renderer.canvas;
import basics.math.algebra.Vector;
import optics.light.Color; import optics.light.Color;
import java.util.Arrays;
public class ContributionBuffer { public class ContributionBuffer {
// linear rgb colors // linear rgb colors
private volatile Color[][] buffer; private final Color[][] buffer;
// amount of samples for every pixel // amount of samples for every pixel
private volatile double[][] samples; private final double[][] samples;
private int width; private final int width;
private int height; private final int height;
private int maxSample; private int maxSample;
@ -22,15 +23,17 @@ public class ContributionBuffer {
this.buffer = new Color[width][height]; this.buffer = new Color[width][height];
this.samples = new double[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 x = 0; x < width; x++) {
for (int y = 0; y < height; y++) { Arrays.fill(this.buffer[x], Color.BLACK);
this.buffer[x][y] = Color.BLACK;
}
} }
} }
public void contribute(int x, int y, Color color) { 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]++; samples[x][y]++;
maxSample = Math.max(maxSample, (int) samples[x][y]); maxSample = Math.max(maxSample, (int) samples[x][y]);
@ -47,15 +50,13 @@ public class ContributionBuffer {
Color color = buffer[x][y]; Color color = buffer[x][y];
color.r = color.g = color.b = 0; color.r = color.g = color.b = 0;
// reset sample count // reset sample count
samples[x][y] = 0; samples[x][y] = 1;
} }
} }
} }
private int getRGB(int x, int y) { private int getRGB(int x, int y) {
Color linearRGB = buffer[x][y].scale(1.0 / samples[x][y]); Color linearRGB = buffer[x][y];
linearRGB.colorCorrect();
int red = (int) (linearRGB.r* 255.0); int red = (int) (linearRGB.r* 255.0);
int green = (int) (linearRGB.g * 255.0); int green = (int) (linearRGB.g * 255.0);