diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java index 277f8ec3f0..f445a3e9ec 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/PathTracer.java @@ -17,7 +17,6 @@ */ package se.llbit.chunky.renderer.scene; -import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.block.minecraft.Air; import se.llbit.chunky.block.minecraft.Water; import se.llbit.chunky.renderer.EmitterSamplingStrategy; @@ -226,6 +225,16 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa Vector3 emittance = new Vector3(); Vector4 indirectEmitterColor = new Vector4(0, 0, 0, 0); + float pTransmit = currentMat.additionalTransmission; + + boolean transmitBack = pTransmit > Ray.EPSILON && random.nextFloat() < pTransmit; + + double eventProb = (transmitBack ? pTransmit: 1- pTransmit ) + Ray.EPSILON; + + if (transmitBack) { + ray.invertNormal(); + } + if (scene.emittersEnabled && (!scene.isPreventNormalEmitterWithSampling() || scene.getEmitterSamplingStrategy() == EmitterSamplingStrategy.NONE || ray.depth == 0) && currentMat.emittance > Ray.EPSILON) { // Quadratic emittance mapping, so a pixel that's 50% darker will emit only 25% as much light @@ -265,8 +274,8 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa double directLightB = 0; boolean frontLight = next.d.dot(ray.getNormal()) > 0; - - if (frontLight || (currentMat.subSurfaceScattering + //check if normal faces the sun direction, if so do sampling + if (frontLight || (false && random.nextFloat() < Scene.fSubSurface)) { if (!frontLight) { @@ -287,7 +296,7 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa } } - next.diffuseReflection(ray, random); + next.diffuseLobes(ray, random, transmitBack); hit = pathTrace(scene, next, state, false) || hit; if (hit) { cumulativeColor.x += emittance.x + ray.color.x * (directLightR * scene.sun.emittance.x + next.color.x + indirectEmitterColor.x); @@ -301,7 +310,7 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa } } else { - next.diffuseReflection(ray, random); + next.diffuseLobes(ray, random, transmitBack); hit = pathTrace(scene, next, state, false) || hit; if (hit) { @@ -315,6 +324,10 @@ private static boolean doDiffuseReflection(Ray ray, Ray next, Material currentMa cumulativeColor.z += ray.color.z * indirectEmitterColor.z; } } + //fix the normal if inverted for use in other things + if (transmitBack) { + ray.invertNormal(); + } return hit; } @@ -359,31 +372,7 @@ private static boolean doRefraction(Ray ray, Ray next, Material currentMat, Mate } } else { if (doRefraction) { - - double t2 = FastMath.sqrt(radicand); - Vector3 n = ray.getNormal(); - if (cosTheta > 0) { - next.d.x = n1n2 * ray.d.x + (n1n2 * cosTheta - t2) * n.x; - next.d.y = n1n2 * ray.d.y + (n1n2 * cosTheta - t2) * n.y; - next.d.z = n1n2 * ray.d.z + (n1n2 * cosTheta - t2) * n.z; - } else { - next.d.x = n1n2 * ray.d.x - (-n1n2 * cosTheta - t2) * n.x; - next.d.y = n1n2 * ray.d.y - (-n1n2 * cosTheta - t2) * n.y; - next.d.z = n1n2 * ray.d.z - (-n1n2 * cosTheta - t2) * n.z; - } - - next.d.normalize(); - - // See Ray.specularReflection for information on why this is needed - // This is the same thing but for refraction instead of reflection - // so this time we want the signs of the dot product to be the same - if (QuickMath.signum(next.getGeometryNormal().dot(next.d)) != QuickMath.signum(next.getGeometryNormal().dot(ray.d))) { - double factor = QuickMath.signum(next.getGeometryNormal().dot(ray.d)) * -Ray.EPSILON - next.d.dot(next.getGeometryNormal()); - next.d.scaleAdd(factor, next.getGeometryNormal()); - next.d.normalize(); - } - - next.o.scaleAdd(Ray.OFFSET, next.d); + next.specularRefraction(ray, random, radicand, n1n2, cosTheta); } if (pathTrace(scene, next, state, false)) { @@ -554,12 +543,14 @@ public static void getDirectLightAttenuation(Scene scene, Ray ray, WorkerState s attenuation.y = 1; attenuation.z = 1; attenuation.w = 1; - while (attenuation.w > 0) { + while (attenuation.w > Ray.EPSILON) { ray.o.scaleAdd(Ray.OFFSET, ray.d); if (!PreviewRayTracer.nextIntersection(scene, ray)) { break; } - double mult = 1 - ray.color.w; + Material mat = ray.getCurrentMaterial(); + double pDiffuse = scene.fancierTranslucency ? 1 - Math.sqrt(1 - ray.color.w) : ray.color.w; + double mult = 1 - pDiffuse*(1 -Math.pow(mat.additionalTransmission, 1)); attenuation.x *= ray.color.x * ray.color.w + mult; attenuation.y *= ray.color.y * ray.color.w + mult; attenuation.z *= ray.color.z * ray.color.w + mult; diff --git a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java index fb4f8adcf4..048c92e852 100644 --- a/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java +++ b/chunky/src/java/se/llbit/chunky/renderer/scene/Scene.java @@ -3201,6 +3201,16 @@ public void setPerceptualSmoothness(String materialName, float value) { refresh(ResetReason.MATERIALS_CHANGED); } + /** + * Modifies the transmission roughness property for the given material. + */ + public void setPerceptualTransmissionSmoothness(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("transmissionRoughness", Json.of(Math.pow(1 - value, 2))); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + /** * Modifies the metalness property for the given material. */ @@ -3211,6 +3221,16 @@ public void setMetalness(String materialName, float value) { refresh(ResetReason.MATERIALS_CHANGED); } + /** + * Modifies the additional transmission through diffuse property for the given material. + */ + public void setAdditionalTransmission(String materialName, float value) { + JsonObject material = materials.getOrDefault(materialName, new JsonObject()).object(); + material.set("additionalTransmission", Json.of(value)); + materials.put(materialName, material); + refresh(ResetReason.MATERIALS_CHANGED); + } + public int getYClipMin() { return yClipMin; } diff --git a/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java b/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java index a1644556ee..8c4276e302 100644 --- a/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java +++ b/chunky/src/java/se/llbit/chunky/ui/render/tabs/MaterialsTab.java @@ -53,7 +53,11 @@ public class MaterialsTab extends HBox implements RenderControlsTab, Initializab private final DoubleAdjuster specular = new DoubleAdjuster(); private final DoubleAdjuster ior = new DoubleAdjuster(); private final DoubleAdjuster perceptualSmoothness = new DoubleAdjuster(); + + private final DoubleAdjuster perceptualTransmissionSmoothness = new DoubleAdjuster(); private final DoubleAdjuster metalness = new DoubleAdjuster(); + + private final DoubleAdjuster additionalTransmission = new DoubleAdjuster(); private final ListView listView; public MaterialsTab() { @@ -69,9 +73,15 @@ public MaterialsTab() { perceptualSmoothness.setName("Smoothness"); perceptualSmoothness.setRange(0, 1); perceptualSmoothness.setTooltip("Smoothness of the selected material."); + perceptualTransmissionSmoothness.setName("Transmission Smoothness"); + perceptualTransmissionSmoothness.setRange(0, 1); + perceptualTransmissionSmoothness.setTooltip("Smoothness of refraction though material"); metalness.setName("Metalness"); metalness.setRange(0, 1); metalness.setTooltip("Metalness (texture-tinted reflectivity) of the selected material."); + additionalTransmission.setName("Additional Transmission"); + additionalTransmission.setRange(0, 1); + additionalTransmission.setTooltip("Amount of transmitted light to back in diffuse component"); ObservableList blockIds = FXCollections.observableArrayList(); blockIds.addAll(MaterialStore.collections.keySet()); blockIds.addAll(ExtraMaterials.idMap.keySet()); @@ -87,7 +97,8 @@ public MaterialsTab() { settings.setSpacing(10); settings.getChildren().addAll( new Label("Material Properties"), - emittance, specular, perceptualSmoothness, ior, metalness, + emittance, specular, perceptualSmoothness, ior,perceptualTransmissionSmoothness, metalness, + additionalTransmission, new Label("(set to zero to disable)")); setPadding(new Insets(10)); setSpacing(15); @@ -116,20 +127,26 @@ private void updateSelectedMaterial(String materialName) { double specAcc = 0; double iorAcc = 0; double perceptualSmoothnessAcc = 0; + double perceptualTransmissionSmoothnessAcc = 0; double metalnessAcc = 0; + double additionalTransmissionAcc = 0; Collection blocks = MaterialStore.collections.get(materialName); for (Block block : blocks) { emAcc += block.emittance; specAcc += block.specular; iorAcc += block.ior; perceptualSmoothnessAcc += block.getPerceptualSmoothness(); + perceptualTransmissionSmoothnessAcc += block.getPerceptualTransmissionSmoothness(); metalnessAcc += block.metalness; + additionalTransmissionAcc += block.additionalTransmission; } emittance.set(emAcc / blocks.size()); specular.set(specAcc / blocks.size()); ior.set(iorAcc / blocks.size()); perceptualSmoothness.set(perceptualSmoothnessAcc / blocks.size()); + perceptualTransmissionSmoothness.set(perceptualTransmissionSmoothnessAcc / blocks.size()); metalness.set(metalnessAcc / blocks.size()); + additionalTransmission.set(additionalTransmissionAcc/blocks.size()); materialExists = true; } else if (ExtraMaterials.idMap.containsKey(materialName)) { Material material = ExtraMaterials.idMap.get(materialName); @@ -138,7 +155,9 @@ private void updateSelectedMaterial(String materialName) { specular.set(material.specular); ior.set(material.ior); perceptualSmoothness.set(material.getPerceptualSmoothness()); + perceptualTransmissionSmoothness.set(material.getPerceptualTransmissionSmoothness()); metalness.set(material.metalness); + additionalTransmission.set(material.additionalTransmission); materialExists = true; } } else if (MaterialStore.blockIds.contains(materialName)) { @@ -148,7 +167,9 @@ private void updateSelectedMaterial(String materialName) { specular.set(block.specular); ior.set(block.ior); perceptualSmoothness.set(block.getPerceptualSmoothness()); + perceptualTransmissionSmoothness.set(block.getPerceptualTransmissionSmoothness()); metalness.set(block.metalness); + additionalTransmission.set(block.additionalTransmission); materialExists = true; } if (materialExists) { @@ -156,13 +177,18 @@ private void updateSelectedMaterial(String materialName) { specular.onValueChange(value -> scene.setSpecular(materialName, value.floatValue())); ior.onValueChange(value -> scene.setIor(materialName, value.floatValue())); perceptualSmoothness.onValueChange(value -> scene.setPerceptualSmoothness(materialName, value.floatValue())); + perceptualTransmissionSmoothness.onValueChange(value -> scene.setPerceptualTransmissionSmoothness(materialName, + value.floatValue())); metalness.onValueChange(value -> scene.setMetalness(materialName, value.floatValue())); + additionalTransmission.onValueChange(value -> scene.setAdditionalTransmission(materialName, value.floatValue())); } else { emittance.onValueChange(value -> {}); specular.onValueChange(value -> {}); ior.onValueChange(value -> {}); perceptualSmoothness.onValueChange(value -> {}); + perceptualTransmissionSmoothness.onValueChange(value -> {}); metalness.onValueChange(value -> {}); + additionalTransmission.onValueChange(value -> {}); } } diff --git a/chunky/src/java/se/llbit/chunky/world/Material.java b/chunky/src/java/se/llbit/chunky/world/Material.java index 572af56774..c716b85e6a 100644 --- a/chunky/src/java/se/llbit/chunky/world/Material.java +++ b/chunky/src/java/se/llbit/chunky/world/Material.java @@ -66,6 +66,12 @@ public abstract class Material { */ public float roughness = 0f; + /** + * The (linear) roughness controlling how blurry a refraction/transmission though a block is. A value of 0 makes the + * effect perfectly specular, a value of 1 makes it diffuse. + */ + public float transmissionRoughness = 0f; + /** * The metalness value controls how metal-y a block appears. In reality this is a boolean value * but in practice usually a float is used in PBR to allow adding dirt or scratches on metals @@ -76,9 +82,11 @@ public abstract class Material { public float metalness = 0; /** - * Subsurface scattering property. + * Additional amount of transmission to do for a given mat, for opaque materials this provides a first order + * approximation to subsurface scattering + * #TODO: figure out if to take a chunk of outgoing energy or use up left over energy */ - public boolean subSurfaceScattering = false; + public float additionalTransmission = 0f; /** * Base texture. @@ -104,7 +112,8 @@ public void restoreDefaults() { specular = 0; emittance = 0; roughness = 0; - subSurfaceScattering = false; + transmissionRoughness = 0; + additionalTransmission = 0f; } public void getColor(Ray ray) { @@ -125,6 +134,8 @@ public void loadMaterialProperties(JsonObject json) { emittance = json.get("emittance").floatValue(emittance); roughness = json.get("roughness").floatValue(roughness); metalness = json.get("metalness").floatValue(metalness); + transmissionRoughness = json.get("transmissionRoughness").floatValue(transmissionRoughness); + additionalTransmission = json.get("additionalTransmission").floatValue(additionalTransmission); } public boolean isWater() { @@ -146,4 +157,12 @@ public double getPerceptualSmoothness() { public void setPerceptualSmoothness(double perceptualSmoothness) { roughness = (float) Math.pow(1 - perceptualSmoothness, 2); } + + public double getPerceptualTransmissionSmoothness() { + return 1 - Math.sqrt(transmissionRoughness); + } + + public void setPerceptualTransmissionSmoothness(double perceptualSmoothness) { + transmissionRoughness = (float) Math.pow(1 - perceptualSmoothness, 2); + } } diff --git a/chunky/src/java/se/llbit/math/Ray.java b/chunky/src/java/se/llbit/math/Ray.java index 11deb1e4f4..ba6ba7af98 100644 --- a/chunky/src/java/se/llbit/math/Ray.java +++ b/chunky/src/java/se/llbit/math/Ray.java @@ -258,61 +258,21 @@ public float[] getBiomeWaterColor(Scene scene) { } /** - * Set this ray to a random diffuse reflection of the input ray. + * Set this ray to a random diffuse reflection or transmission of the input ray. */ - public final void diffuseReflection(Ray ray, Random random) { + public final void diffuseLobes(Ray ray, Random random, boolean transmitBack) { set(ray); - - // get random point on unit disk - double x1 = random.nextDouble(); - double x2 = random.nextDouble(); - double r = FastMath.sqrt(x1); - double theta = 2 * Math.PI * x2; - - // project to point on hemisphere in tangent space - double tx = r * FastMath.cos(theta); - double ty = r * FastMath.sin(theta); - double tz = FastMath.sqrt(1 - x1); - - // transform from tangent space to world space - double xx, xy, xz; - double ux, uy, uz; - double vx, vy, vz; - - if (QuickMath.abs(n.x) > .1) { - xx = 0; - xy = 1; - xz = 0; - } else { - xx = 1; - xy = 0; - xz = 0; - } - - ux = xy * n.z - xz * n.y; - uy = xz * n.x - xx * n.z; - uz = xx * n.y - xy * n.x; - - r = 1 / FastMath.sqrt(ux * ux + uy * uy + uz * uz); - - ux *= r; - uy *= r; - uz *= r; - - vx = uy * n.z - uz * n.y; - vy = uz * n.x - ux * n.z; - vz = ux * n.y - uy * n.x; - - d.x = ux * tx + vx * ty + n.x * tz; - d.y = uy * tx + vy * ty + n.y * tz; - d.z = uz * tx + vz * ty + n.z * tz; + // get random point on hemisphere + this.randomHemisphereDir(random); o.scaleAdd(Ray.OFFSET, d); - currentMaterial = prevMaterial; + //if a block is solid while transmiting then we want to keep the old material as it penetrates + // might be able to remove the solid check but test correctness and performance + if (!transmitBack | !currentMaterial.solid) currentMaterial = prevMaterial; specular = false; // See specularReflection for explanation of why this is needed - if(QuickMath.signum(geomN.dot(d)) == QuickMath.signum(geomN.dot(ray.d))) { + if(QuickMath.signum(geomN.dot(d)) == QuickMath.signum(geomN.dot(ray.d))^transmitBack) { double factor = QuickMath.signum(geomN.dot(ray.d)) * -Ray.EPSILON - d.dot(geomN); d.scaleAdd(factor, geomN); d.normalize(); @@ -337,49 +297,8 @@ public final void specularReflection(Ray ray, Random random) { specularDirection.scaleAdd(-2 * ray.d.dot(ray.n), ray.n, ray.d); // 2. get diffuse reflection direction (stored in this.d) - // get random point on unit disk - double x1 = random.nextDouble(); - double x2 = random.nextDouble(); - double r = FastMath.sqrt(x1); - double theta = 2 * Math.PI * x2; - - // project to point on hemisphere in tangent space - double tx = r * FastMath.cos(theta); - double ty = r * FastMath.sin(theta); - double tz = FastMath.sqrt(1 - x1); - - // transform from tangent space to world space - double xx, xy, xz; - double ux, uy, uz; - double vx, vy, vz; - - if (QuickMath.abs(n.x) > .1) { - xx = 0; - xy = 1; - xz = 0; - } else { - xx = 1; - xy = 0; - xz = 0; - } - - ux = xy * n.z - xz * n.y; - uy = xz * n.x - xx * n.z; - uz = xx * n.y - xy * n.x; - - r = 1 / FastMath.sqrt(ux * ux + uy * uy + uz * uz); - - ux *= r; - uy *= r; - uz *= r; - - vx = uy * n.z - uz * n.y; - vy = uz * n.x - ux * n.z; - vz = ux * n.y - uy * n.x; - - d.x = ux * tx + vx * ty + n.x * tz; - d.y = uy * tx + vy * ty + n.y * tz; - d.z = uz * tx + vz * ty + n.z * tz; + // get random point on hemisphere + this.randomHemisphereDir(random); // 3. scale d to be roughness * dDiffuse + (1 - roughness) * dSpecular d.scale(roughness); @@ -414,12 +333,50 @@ public final void specularReflection(Ray ray, Random random) { } } + public final void specularRefraction (Ray ray, Random random, double radicand, float n1n2, double cosTheta) { + set(ray); + double roughness = ray.getCurrentMaterial().transmissionRoughness; + this.randomHemisphereDir(random); + d.scale(-1.0); //invert for lower hemisphere + double t2 = FastMath.sqrt(radicand); + Vector3 n = ray.getNormal(); + Vector3 refractionDirection = new Vector3(); + if (cosTheta > 0) { + refractionDirection.x = n1n2 * ray.d.x + (n1n2 * cosTheta - t2) * n.x; + refractionDirection.y = n1n2 * ray.d.y + (n1n2 * cosTheta - t2) * n.y; + refractionDirection.z = n1n2 * ray.d.z + (n1n2 * cosTheta - t2) * n.z; + } else { + refractionDirection.x = n1n2 * ray.d.x - (-n1n2 * cosTheta - t2) * n.x; + refractionDirection.y = n1n2 * ray.d.y - (-n1n2 * cosTheta - t2) * n.y; + refractionDirection.z = n1n2 * ray.d.z - (-n1n2 * cosTheta - t2) * n.z; + } + + refractionDirection.normalize(); + + // 3. scale d to be roughness * dDiffuse + (1 - roughness) * dSpecular + d.scale(roughness); + d.scaleAdd(1 - roughness, refractionDirection); + d.normalize(); + o.scaleAdd(Ray.OFFSET, d); + + // See Ray.specularReflection for information on why this is needed + // This is the same thing but for refraction instead of reflection + // so this time we want the signs of the dot product to be the same + if (QuickMath.signum(geomN.dot(d)) != QuickMath.signum(geomN.dot(ray.d))) { + double factor = QuickMath.signum(geomN.dot(ray.d)) * -Ray.EPSILON - d.dot(geomN); + d.scaleAdd(factor,geomN); + d.normalize(); + } + + + } + /** - * Scatter ray normal - * + * Random direction sampled from the upper hemisphere + * stored in this.d * @param random random number source */ - public final void scatterNormal(Random random) { + public final void randomHemisphereDir(Random random) { // get random point on unit disk double x1 = random.nextDouble(); double x2 = random.nextDouble(); @@ -460,7 +417,9 @@ public final void scatterNormal(Random random) { vy = uz * n.x - ux * n.z; vz = ux * n.y - uy * n.x; - n.set(ux * tx + vx * ty + n.x * tz, uy * tx + vy * ty + n.y * tz, uz * tx + vz * ty + n.z * tz); + d.x = ux * tx + vx * ty + n.x * tz; + d.y = uy * tx + vy * ty + n.y * tz; + d.z = uz * tx + vz * ty + n.z * tz; } public void setPrevMaterial(Material mat, int data) {