/* Copyright (c) 2013-2015 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.renderer.scene; import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.model.WaterModel; import se.llbit.chunky.renderer.WorkerState; import se.llbit.chunky.world.Block; import se.llbit.chunky.world.Material; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.math.Vector4; import java.util.Random; /** * Static methods for path tracing. * * @author Jesper Öqvist <jesper@llbit.se> */ public class PathTracer implements RayTracer { /** Extinction factor for fog rendering. */ private static final double EXTINCTION_FACTOR = 0.04; /** * Path trace the ray. */ @Override public void trace(Scene scene, WorkerState state) { Ray ray = state.ray; if (scene.isInWater(ray)) { ray.setCurrentMaterial(Block.get(Block.WATER_ID), 0); } else { ray.setCurrentMaterial(Block.AIR, 0); } pathTrace(scene, ray, state, 1, true); } /** * Path trace the ray in this scene. * * @param firstReflection {@code true} if the ray has not yet hit the first * diffuse or specular reflection */ public static boolean pathTrace(Scene scene, Ray ray, WorkerState state, int addEmitted, boolean firstReflection) { boolean hit = false; Random random = state.random; Vector3 ox = new Vector3(ray.o); Vector3 od = new Vector3(ray.d); double airDistance = 0; while (true) { if (!PreviewRayTracer.nextIntersection(scene, ray)) { if (ray.getPrevMaterial().isWater()) { ray.color.set(0, 0, 0, 1); hit = true; } else if (ray.depth == 0) { // Direct sky hit. if (!scene.transparentSky()) { scene.sky.getSkyColorInterpolated(ray); hit = true; } } else if (ray.specular) { // Indirect sky hit - specular color. scene.sky.getSkySpecularColor(ray); hit = true; } else { // Indirect sky hit - diffuse color. scene.sky.getSkyColor(ray); hit = true; } break; } Material currentMat = ray.getCurrentMaterial(); Material prevMat = ray.getPrevMaterial(); if (!scene.stillWater && ray.n.y != 0 && ((currentMat.isWater() && prevMat == Block.AIR) || (currentMat == Block.AIR && prevMat.isWater()))) { WaterModel.doWaterDisplacement(ray); if (currentMat == Block.AIR) { ray.n.y = -ray.n.y; } } float pSpecular = currentMat.specular; double pDiffuse = ray.color.w; float n1 = prevMat.ior; float n2 = currentMat.ior; if (prevMat == Block.AIR) { airDistance = ray.distance; } if (pDiffuse + pSpecular < Ray.EPSILON && n1 == n2) { // Transmission without refraction. // This can happen when the ray passes through a transparent // material into another. It can also happen for example // when passing through a transparent part of an otherwise solid // object. // TODO: material color may change here. continue; } if (pSpecular > Ray.EPSILON && random.nextFloat() < pSpecular) { // Specular reflection. firstReflection = false; if (!scene.kill(ray.depth + 1, random)) { Ray reflected = new Ray(); reflected.specularReflection(ray); if (pathTrace(scene, reflected, state, 1, false)) { ray.color.x = reflected.color.x; ray.color.y = reflected.color.y; ray.color.z = reflected.color.z; hit = true; } } } else { if (random.nextFloat() < pDiffuse) { // Diffuse reflection. firstReflection = false; if (!scene.kill(ray.depth + 1, random)) { Ray reflected = new Ray(); float emittance = 0; if (scene.emittersEnabled && currentMat.emittance > Ray.EPSILON) { emittance = addEmitted; ray.emittance.x = ray.color.x * ray.color.x * currentMat.emittance * scene.emitterIntensity; ray.emittance.y = ray.color.y * ray.color.y * currentMat.emittance * scene.emitterIntensity; ray.emittance.z = ray.color.z * ray.color.z * currentMat.emittance * scene.emitterIntensity; hit = true; } if (scene.sunEnabled) { reflected.set(ray); scene.sun.getRandomSunDirection(reflected, random); double directLightR = 0; double directLightG = 0; double directLightB = 0; boolean frontLight = reflected.d.dot(ray.n) > 0; if (frontLight || (currentMat.subSurfaceScattering && random.nextFloat() < Scene.fSubSurface)) { if (!frontLight) { reflected.o.scaleAdd(-Ray.OFFSET, ray.n); } reflected.setCurrentMaterial(reflected.getPrevMaterial(), reflected.getPrevData()); getDirectLightAttenuation(scene, reflected, state); Vector4 attenuation = state.attenuation; if (attenuation.w > 0) { double mult = QuickMath.abs(reflected.d.dot(ray.n)); directLightR = attenuation.x * attenuation.w * mult; directLightG = attenuation.y * attenuation.w * mult; directLightB = attenuation.z * attenuation.w * mult; hit = true; } } reflected.diffuseReflection(ray, random); hit = pathTrace(scene, reflected, state, 0, false) || hit; if (hit) { ray.color.x = ray.color.x * (emittance + directLightR * scene.sun.emittance.x + ( reflected.color.x + reflected.emittance.x)); ray.color.y = ray.color.y * (emittance + directLightG * scene.sun.emittance.y + ( reflected.color.y + reflected.emittance.y)); ray.color.z = ray.color.z * (emittance + directLightB * scene.sun.emittance.z + ( reflected.color.z + reflected.emittance.z)); } } else { reflected.diffuseReflection(ray, random); hit = pathTrace(scene, reflected, state, 0, false) || hit; if (hit) { ray.color.x = ray.color.x * (emittance + (reflected.color.x + reflected.emittance.x)); ray.color.y = ray.color.y * (emittance + (reflected.color.y + reflected.emittance.y)); ray.color.z = ray.color.z * (emittance + (reflected.color.z + reflected.emittance.z)); } } } } else if (n1 != n2) { // Refraction. // TODO: make this decision dependent on the material properties: boolean doRefraction = currentMat.isWater() || prevMat.isWater() || currentMat == Block.get(Block.ICE_ID) || prevMat == Block.get(Block.ICE_ID); // Refraction. float n1n2 = n1 / n2; double cosTheta = -ray.n.dot(ray.d); double radicand = 1 - n1n2 * n1n2 * (1 - cosTheta * cosTheta); if (doRefraction && radicand < Ray.EPSILON) { // Total internal reflection. if (!scene.kill(ray.depth + 1, random)) { Ray reflected = new Ray(); reflected.specularReflection(ray); if (pathTrace(scene, reflected, state, 1, false)) { ray.color.x = reflected.color.x; ray.color.y = reflected.color.y; ray.color.z = reflected.color.z; hit = true; } } } else { if (!scene.kill(ray.depth + 1, random)) { Ray refracted = new Ray(); refracted.set(ray); // Calculate angle-dependent reflectance using // Fresnel equation approximation: // R(cosineAngle) = R0 + (1 - R0) * (1 - cos(cosineAngle))^5 float a = (n1n2 - 1); float b = (n1n2 + 1); double R0 = a * a / (b * b); double c = 1 - cosTheta; double Rtheta = R0 + (1 - R0) * c * c * c * c * c; if (random.nextFloat() < Rtheta) { Ray reflected = new Ray(); reflected.specularReflection(ray); if (pathTrace(scene, reflected, state, 1, false)) { ray.color.x = reflected.color.x; ray.color.y = reflected.color.y; ray.color.z = reflected.color.z; hit = true; } } else { if (doRefraction) { double t2 = FastMath.sqrt(radicand); if (cosTheta > 0) { refracted.d.x = n1n2 * ray.d.x + (n1n2 * cosTheta - t2) * ray.n.x; refracted.d.y = n1n2 * ray.d.y + (n1n2 * cosTheta - t2) * ray.n.y; refracted.d.z = n1n2 * ray.d.z + (n1n2 * cosTheta - t2) * ray.n.z; } else { refracted.d.x = n1n2 * ray.d.x - (-n1n2 * cosTheta - t2) * ray.n.x; refracted.d.y = n1n2 * ray.d.y - (-n1n2 * cosTheta - t2) * ray.n.y; refracted.d.z = n1n2 * ray.d.z - (-n1n2 * cosTheta - t2) * ray.n.z; } refracted.d.normalize(); refracted.o.scaleAdd(Ray.OFFSET, refracted.d); } if (pathTrace(scene, refracted, state, 1, false)) { ray.color.x = ray.color.x * pDiffuse + (1 - pDiffuse); ray.color.y = ray.color.y * pDiffuse + (1 - pDiffuse); ray.color.z = ray.color.z * pDiffuse + (1 - pDiffuse); ray.color.x *= refracted.color.x; ray.color.y *= refracted.color.y; ray.color.z *= refracted.color.z; hit = true; } } } } } else { Ray transmitted = new Ray(); transmitted.set(ray); transmitted.o.scaleAdd(Ray.OFFSET, transmitted.d); if (pathTrace(scene, transmitted, state, 1, false)) { ray.color.x = ray.color.x * pDiffuse + (1 - pDiffuse); ray.color.y = ray.color.y * pDiffuse + (1 - pDiffuse); ray.color.z = ray.color.z * pDiffuse + (1 - pDiffuse); ray.color.x *= transmitted.color.x; ray.color.y *= transmitted.color.y; ray.color.z *= transmitted.color.z; hit = true; } } } if (hit && prevMat.isWater()) { // Render water fog effect. double a = ray.distance / scene.waterVisibility; double attenuation = 1 - QuickMath.min(1, a * a); ray.color.scale(attenuation); } break; } if (!hit) { ray.color.set(0, 0, 0, 1); if (firstReflection) { airDistance = ray.distance; } } // This is a simplistic fog model which gives greater artistic freedom but // less realism. The user can select fog color and density; in a more // realistic model color would depend on viewing angle and sun color/position. if (airDistance > 0 && scene.fogEnabled()) { Sun sun = scene.sun; // Pick point between ray origin and intersected object. // The chosen point is used to test if the sun is lighting the // fog between the camera and the first diffuse ray target. // The sun contribution will be proportional to the amount of // sunlit fog areas in the ray path, thus giving an approximation // of the sun inscatter leading to effects like god rays. // The way the sun contribution point is chosen is not // entirely correct because the original ray may have // travelled through glass or other materials between air gaps. // However, the results are probably close enough to not be distracting, // so this seems like a reasonable approximation. Ray atmos = new Ray(); double offset = QuickMath.clamp(airDistance * random.nextFloat(), Ray.EPSILON, airDistance - Ray.EPSILON); atmos.o.scaleAdd(offset, od, ox); sun.getRandomSunDirection(atmos, random); atmos.setCurrentMaterial(Block.AIR, 0); double fogDensity = scene.getFogDensity() * EXTINCTION_FACTOR; double extinction = Math.exp(-airDistance * fogDensity); ray.color.scale(extinction); // Check sun visibility at random point to determine inscatter brightness. getDirectLightAttenuation(scene, atmos, state); Vector4 attenuation = state.attenuation; if (attenuation.w > Ray.EPSILON) { Vector3 fogColor = scene.getFogColor(); double inscatter; if (scene.fastFog()) { inscatter = (1 - extinction); } else { inscatter = airDistance * fogDensity * Math.exp(-offset * fogDensity); } ray.color.x += attenuation.x * attenuation.w * fogColor.x * inscatter; ray.color.y += attenuation.y * attenuation.w * fogColor.y * inscatter; ray.color.z += attenuation.z * attenuation.w * fogColor.z * inscatter; } } return hit; } /** * Calculate direct lighting attenuation. */ public static void getDirectLightAttenuation(Scene scene, Ray ray, WorkerState state) { Vector4 attenuation = state.attenuation; attenuation.x = 1; attenuation.y = 1; attenuation.z = 1; attenuation.w = 1; while (attenuation.w > 0) { ray.o.scaleAdd(Ray.OFFSET, ray.d); if (!PreviewRayTracer.nextIntersection(scene, ray)) { break; } double mult = 1 - ray.color.w; 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; attenuation.w *= mult; if (ray.getPrevMaterial().isWater()) { double a = ray.distance / scene.waterVisibility; attenuation.w *= 1 - QuickMath.min(1, a * a); } } } }