diff --git a/build/RayTracingRenderer.es5.js b/build/RayTracingRenderer.es5.js index 80197bd..bafddde 100644 --- a/build/RayTracingRenderer.es5.js +++ b/build/RayTracingRenderer.es5.js @@ -2371,7 +2371,7 @@ return unrolled; } - var rayTraceCore = "\n #define STANDARD 0\n #define THIN_GLASS 1\n #define THICK_GLASS 2\n #define SHADOW_CATCHER 3\n\n #define SAMPLES_PER_MATERIAL 8\n\n const float IOR = 1.5;\n const float INV_IOR = 1.0 / IOR;\n\n const float IOR_THIN = 1.015;\n const float INV_IOR_THIN = 1.0 / IOR_THIN;\n\n const float R0 = (1.0 - IOR) * (1.0 - IOR) / ((1.0 + IOR) * (1.0 + IOR));\n\n // https://www.w3.org/WAI/GL/wiki/Relative_luminance\n const vec3 luminance = vec3(0.2126, 0.7152, 0.0722);\n\n #define RAY_MAX_DISTANCE 9999.0\n\n struct Ray {\n vec3 o;\n vec3 d;\n vec3 invD;\n float tMax;\n };\n\n struct SurfaceInteraction {\n bool hit;\n vec3 position;\n vec3 normal; // smoothed normal from the three triangle vertices\n vec3 faceNormal; // normal of the triangle\n vec3 color;\n float roughness;\n float metalness;\n int materialType;\n };\n\n struct Camera {\n mat4 transform;\n float aspect;\n float fov;\n float focus;\n float aperture;\n };\n\n void initRay(inout Ray ray, vec3 origin, vec3 direction) {\n ray.o = origin;\n ray.d = direction;\n ray.invD = 1.0 / ray.d;\n ray.tMax = RAY_MAX_DISTANCE;\n }\n\n // given the index from a 1D array, retrieve corresponding position from packed 2D texture\n ivec2 unpackTexel(int i, int columnsLog2) {\n ivec2 u;\n u.y = i >> columnsLog2; // equivalent to (i / 2^columnsLog2)\n u.x = i - (u.y << columnsLog2); // equivalent to (i % 2^columnsLog2)\n return u;\n }\n\n vec4 fetchData(sampler2D s, int i, int columnsLog2) {\n return texelFetch(s, unpackTexel(i, columnsLog2), 0);\n }\n\n ivec4 fetchData(isampler2D s, int i, int columnsLog2) {\n return texelFetch(s, unpackTexel(i, columnsLog2), 0);\n }\n\n struct Path {\n Ray ray;\n vec3 li;\n vec3 albedo;\n float alpha;\n vec3 beta;\n bool specularBounce;\n bool abort;\n };\n\n uniform Camera camera;\n uniform vec2 pixelSize; // 1 / screenResolution\n uniform vec2 jitter;\n\n in vec2 vCoord;\n"; + var rayTraceCore = "\n #define STANDARD 0\n #define THIN_GLASS 1\n #define THICK_GLASS 2\n #define SHADOW_CATCHER 3\n\n const float IOR = 1.5;\n const float INV_IOR = 1.0 / IOR;\n\n const float IOR_THIN = 1.015;\n const float INV_IOR_THIN = 1.0 / IOR_THIN;\n\n const float R0 = (1.0 - IOR) * (1.0 - IOR) / ((1.0 + IOR) * (1.0 + IOR));\n\n // https://www.w3.org/WAI/GL/wiki/Relative_luminance\n const vec3 luminance = vec3(0.2126, 0.7152, 0.0722);\n\n #define RAY_MAX_DISTANCE 9999.0\n\n struct Ray {\n vec3 o;\n vec3 d;\n vec3 invD;\n float tMax;\n };\n\n struct SurfaceInteraction {\n bool hit;\n vec3 position;\n vec3 normal; // smoothed normal from the three triangle vertices\n vec3 faceNormal; // normal of the triangle\n vec3 color;\n float roughness;\n float metalness;\n int materialType;\n };\n\n struct Camera {\n mat4 transform;\n float aspect;\n float fov;\n float focus;\n float aperture;\n };\n\n void initRay(inout Ray ray, vec3 origin, vec3 direction) {\n ray.o = origin;\n ray.d = direction;\n ray.invD = 1.0 / ray.d;\n ray.tMax = RAY_MAX_DISTANCE;\n }\n\n // given the index from a 1D array, retrieve corresponding position from packed 2D texture\n ivec2 unpackTexel(int i, int columnsLog2) {\n ivec2 u;\n u.y = i >> columnsLog2; // equivalent to (i / 2^columnsLog2)\n u.x = i - (u.y << columnsLog2); // equivalent to (i % 2^columnsLog2)\n return u;\n }\n\n vec4 fetchData(sampler2D s, int i, int columnsLog2) {\n return texelFetch(s, unpackTexel(i, columnsLog2), 0);\n }\n\n ivec4 fetchData(isampler2D s, int i, int columnsLog2) {\n return texelFetch(s, unpackTexel(i, columnsLog2), 0);\n }\n\n struct Path {\n Ray ray;\n vec3 li;\n float alpha;\n vec3 beta;\n bool specularBounce;\n bool abort;\n float misWeight;\n };\n\n uniform Camera camera;\n uniform vec2 pixelSize; // 1 / screenResolution\n uniform vec2 jitter;\n\n in vec2 vCoord;\n"; // Manually performs linear filtering if the extension OES_texture_float_linear is not supported var textureLinear = "\nvec4 textureLinear(sampler2D map, vec2 uv) {\n #ifdef OES_texture_float_linear\n return texture(map, uv);\n #else\n vec2 size = vec2(textureSize(map, 0));\n vec2 texelSize = 1.0 / size;\n\n uv = uv * size - 0.5;\n vec2 f = fract(uv);\n uv = floor(uv) + 0.5;\n\n vec4 s1 = texture(map, (uv + vec2(0, 0)) * texelSize);\n vec4 s2 = texture(map, (uv + vec2(1, 0)) * texelSize);\n vec4 s3 = texture(map, (uv + vec2(0, 1)) * texelSize);\n vec4 s4 = texture(map, (uv + vec2(1, 1)) * texelSize);\n\n return mix(mix(s1, s2, f.x), mix(s3, s4, f.x), f.y);\n #endif\n}\n"; @@ -2380,7 +2380,7 @@ var surfaceInteractionDirect = "\n\n uniform sampler2D gPosition;\n uniform sampler2D gNormal;\n uniform sampler2D gFaceNormal;\n uniform sampler2D gColor;\n uniform sampler2D gMatProps;\n\n void surfaceInteractionDirect(vec2 coord, inout SurfaceInteraction si) {\n vec4 positionAndMeshIndex = texture(gPosition, coord);\n\n si.position = positionAndMeshIndex.xyz;\n\n float meshIndex = positionAndMeshIndex.w;\n\n vec4 normalMaterialType = texture(gNormal, coord);\n\n si.normal = normalize(normalMaterialType.xyz);\n si.materialType = int(normalMaterialType.w);\n\n si.faceNormal = normalize(texture(gFaceNormal, coord).xyz);\n\n si.color = texture(gColor, coord).rgb;\n\n vec4 matProps = texture(gMatProps, coord);\n si.roughness = matProps.x;\n si.metalness = matProps.y;\n\n si.hit = meshIndex > 0.0 ? true : false;\n }\n"; - var random = "\n\n// Noise texture used to generate a different random number for each pixel.\n// We use blue noise in particular, but any type of noise will work.\nuniform sampler2D noise;\n\nuniform float stratifiedSamples[SAMPLING_DIMENSIONS];\nuniform float strataSize;\n\n// Every time we call randomSample() in the shader, and for every call to render,\n// we want that specific bit of the shader to fetch a sample from the same position in stratifiedSamples\n// This allows us to use stratified sampling for each random variable in our path tracing\nint sampleIndex = 0;\n\nfloat pixelSeed;\n\nvoid initRandom() {\n vec2 noiseSize = vec2(textureSize(noise, 0));\n\n // tile the small noise texture across the entire screen\n pixelSeed = texture(noise, vCoord / (pixelSize * noiseSize)).r;\n}\n\nfloat randomSample() {\n float stratifiedSample = stratifiedSamples[sampleIndex++];\n\n float random = fract((stratifiedSample + pixelSeed) * strataSize); // blue noise + stratified samples\n\n // transform random number between [0, 1] to (0, 1)\n return EPS + (1.0 - 2.0 * EPS) * random;\n}\n\nvec2 randomSampleVec2() {\n return vec2(randomSample(), randomSample());\n}\n"; + var random = "\n\n// Noise texture used to generate a different random number for each pixel.\n// We use blue noise in particular, but any type of noise will work.\nuniform sampler2D noise;\n\nuniform float stratifiedSamples[SAMPLING_DIMENSIONS];\nuniform float strataSize;\n\n// Every time we call randomSample() in the shader, and for every call to render,\n// we want that specific bit of the shader to fetch a sample from the same position in stratifiedSamples\n// This allows us to use stratified sampling for each random variable in our path tracing\nint sampleIndex = 0;\n\nfloat pixelSeed;\n\nvoid initRandom() {\n vec2 noiseSize = vec2(textureSize(noise, 0));\n\n // tile the small noise texture across the entire screen\n pixelSeed = texture(noise, vCoord / (pixelSize * noiseSize)).r;\n}\n\nfloat randomSample() {\n float stratifiedSample = stratifiedSamples[sampleIndex++];\n\n float random = fract((stratifiedSample + pixelSeed) * strataSize); // blue noise + stratified samples\n\n // transform random number between [0, 1] to (0, 1)\n return EPS + (1.0 - 2.0 * EPS) * random;\n}\n\nvec2 randomSampleVec2() {\n return vec2(randomSample(), randomSample());\n}\n\nstruct MaterialSamples {\n vec2 s1;\n vec2 s2;\n vec2 s3;\n};\n\nMaterialSamples getRandomMaterialSamples() {\n MaterialSamples samples;\n\n samples.s1 = randomSampleVec2();\n samples.s2 = randomSampleVec2();\n samples.s3 = randomSampleVec2();\n\n return samples;\n}\n"; // Sample the environment map using a cumulative distribution function as described in // http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Sampling_Light_Sources.html#InfiniteAreaLights @@ -2392,17 +2392,17 @@ // Estimate the direct lighting integral using multiple importance sampling // http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Direct_Lighting.html#EstimatingtheDirectLightingIntegral - var sampleMaterial = "\n\nvec3 importanceSampleLight(SurfaceInteraction si, vec3 viewDir, bool lastBounce, vec2 random) {\n vec3 li;\n\n float lightPdf;\n vec2 uv;\n vec3 lightDir = sampleEnvmap(random, uv, lightPdf);\n\n float cosThetaL = dot(si.normal, lightDir);\n\n float orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n if (orientation < 0.0) {\n return li;\n }\n\n float diffuseWeight = 1.0;\n Ray ray;\n initRay(ray, si.position + EPS * lightDir, lightDir);\n if (intersectSceneShadow(ray)) {\n if (lastBounce) {\n diffuseWeight = 0.0;\n } else {\n return li;\n }\n }\n\n vec3 irr = textureLinear(envmap, uv).xyz;\n\n float scatteringPdf;\n vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, diffuseWeight, scatteringPdf);\n\n float weight = powerHeuristic(lightPdf, scatteringPdf);\n\n li = brdf * irr * abs(cosThetaL) * weight / lightPdf;\n\n return li;\n}\n\nvec3 importanceSampleMaterial(SurfaceInteraction si, vec3 viewDir, bool lastBounce, vec3 lightDir) {\n vec3 li;\n\n float cosThetaL = dot(si.normal, lightDir);\n\n float orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n if (orientation < 0.0) {\n return li;\n }\n\n float diffuseWeight = 1.0;\n Ray ray;\n initRay(ray, si.position + EPS * lightDir, lightDir);\n if (intersectSceneShadow(ray)) {\n if (lastBounce) {\n diffuseWeight = 0.0;\n } else {\n return li;\n }\n }\n\n vec2 uv = cartesianToEquirect(lightDir);\n\n float lightPdf = envmapPdf(uv);\n\n vec3 irr = textureLinear(envmap, uv).rgb;\n\n float scatteringPdf;\n vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, diffuseWeight, scatteringPdf);\n\n float weight = powerHeuristic(scatteringPdf, lightPdf);\n\n li += brdf * irr * abs(cosThetaL) * weight / scatteringPdf;\n\n return li;\n}\n\nvoid sampleMaterial(SurfaceInteraction si, int bounce, inout Path path) {\n mat3 basis = orthonormalBasis(si.normal);\n vec3 viewDir = -path.ray.d;\n\n vec2 diffuseOrSpecular = randomSampleVec2();\n\n vec3 lightDir = diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ?\n lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()) :\n lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomSampleVec2());\n\n bool lastBounce = bounce == BOUNCES;\n\n // Add path contribution\n path.li += path.beta * (\n importanceSampleLight(si, viewDir, lastBounce, randomSampleVec2()) +\n importanceSampleMaterial(si, viewDir, lastBounce, lightDir)\n );\n\n // Get new path direction\n\n if (lastBounce) {\n return;\n }\n\n lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ?\n lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()) :\n lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomSampleVec2());\n\n float cosThetaL = dot(si.normal, lightDir);\n\n float scatteringPdf;\n vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf);\n\n path.beta *= abs(cosThetaL) * brdf / scatteringPdf;\n\n initRay(path.ray, si.position + EPS * lightDir, lightDir);\n\n // If new ray direction is pointing into the surface,\n // the light path is physically impossible and we terminate the path.\n float orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n path.abort = orientation < 0.0;\n\n path.specularBounce = false;\n}\n\n"; + var sampleMaterial = "\n\nvoid sampleMaterial(SurfaceInteraction si, int bounce, inout Path path) {\n bool lastBounce = bounce == BOUNCES;\n mat3 basis = orthonormalBasis(si.normal);\n vec3 viewDir = -path.ray.d;\n\n MaterialSamples samples = getRandomMaterialSamples();\n\n vec2 diffuseOrSpecular = samples.s1;\n vec2 lightDirSample = samples.s2;\n vec2 bounceDirSample = samples.s3;\n\n // Step 1: Add direct illumination of the light source (the hdr map)\n // On every bounce but the last, importance sample the light source\n // On the last bounce, multiple importance sample the brdf AND the light source, determined by random var\n\n vec3 lightDir;\n vec2 uv;\n float lightPdf;\n bool brdfSample = false;\n\n if (lastBounce && diffuseOrSpecular.x < 0.5) {\n // reuse this sample by multiplying by 2 to bring sample from [0, 0.5), to [0, 1)\n lightDir = 2.0 * diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ?\n lightDirDiffuse(si.faceNormal, viewDir, basis, lightDirSample) :\n lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, lightDirSample);\n\n uv = cartesianToEquirect(lightDir);\n lightPdf = envmapPdf(uv);\n brdfSample = true;\n } else {\n lightDir = sampleEnvmap(lightDirSample, uv, lightPdf);\n }\n\n float cosThetaL = dot(si.normal, lightDir);\n\n float occluded = 1.0;\n\n float orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n if (orientation < 0.0) {\n // light dir points towards surface. invalid dir.\n occluded = 0.0;\n }\n\n float diffuseWeight = 1.0;\n\n initRay(path.ray, si.position + EPS * lightDir, lightDir);\n if (intersectSceneShadow(path.ray)) {\n if (lastBounce) {\n diffuseWeight = 0.0;\n } else {\n occluded = 0.0;\n }\n }\n\n vec3 irr = textureLinear(envmap, uv).rgb;\n\n float scatteringPdf;\n vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, diffuseWeight, scatteringPdf);\n\n float weight;\n if (lastBounce) {\n weight = brdfSample ?\n 2.0 * powerHeuristic(scatteringPdf, lightPdf) / scatteringPdf :\n 2.0 * powerHeuristic(lightPdf, scatteringPdf) / lightPdf;\n } else {\n weight = powerHeuristic(lightPdf, scatteringPdf) / lightPdf;\n }\n\n path.li += path.beta * occluded * brdf * irr * abs(cosThetaL) * weight;;\n\n // Step 2: Setup ray direction for next bounce by importance sampling the BRDF\n\n if (lastBounce) {\n return;\n }\n\n lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ?\n lightDirDiffuse(si.faceNormal, viewDir, basis, bounceDirSample) :\n lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, bounceDirSample);\n\n cosThetaL = dot(si.normal, lightDir);\n\n orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n path.abort = orientation < 0.0;\n\n if (path.abort) {\n return;\n }\n\n brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf);\n\n uv = cartesianToEquirect(lightDir);\n lightPdf = envmapPdf(uv);\n\n path.misWeight = powerHeuristic(scatteringPdf, lightPdf);\n\n path.beta *= abs(cosThetaL) * brdf / scatteringPdf;\n\n path.specularBounce = false;\n\n initRay(path.ray, si.position + EPS * lightDir, lightDir);\n}\n"; - var sampleShadowCatcher = "\n\n#ifdef USE_SHADOW_CATCHER\n\nfloat importanceSampleLightShadowCatcher(SurfaceInteraction si, vec3 viewDir, vec2 random, inout float alpha) {\n float li;\n\n float lightPdf;\n vec2 uv;\n vec3 lightDir = sampleEnvmap(random, uv, lightPdf);\n\n float cosThetaL = dot(si.normal, lightDir);\n\n float orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n if (orientation < 0.0) {\n return li;\n }\n\n float occluded = 1.0;\n\n Ray ray;\n initRay(ray, si.position + EPS * lightDir, lightDir);\n if (intersectSceneShadow(ray)) {\n occluded = 0.0;\n }\n\n float irr = dot(luminance, textureLinear(envmap, uv).rgb);\n\n // lambertian BRDF\n float brdf = INVPI;\n float scatteringPdf = abs(cosThetaL) * INVPI;\n\n float weight = powerHeuristic(lightPdf, scatteringPdf);\n\n float lightEq = irr * brdf * abs(cosThetaL) * weight / lightPdf;\n\n alpha += lightEq;\n li += occluded * lightEq;\n\n return li;\n}\n\nfloat importanceSampleMaterialShadowCatcher(SurfaceInteraction si, vec3 viewDir, vec3 lightDir, inout float alpha) {\n float li;\n\n float cosThetaL = dot(si.normal, lightDir);\n\n float orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n if (orientation < 0.0) {\n return li;\n }\n\n float occluded = 1.0;\n\n Ray ray;\n initRay(ray, si.position + EPS * lightDir, lightDir);\n if (intersectSceneShadow(ray)) {\n occluded = 0.0;\n }\n\n vec2 uv = cartesianToEquirect(lightDir);\n\n float lightPdf = envmapPdf(uv);\n\n float irr = dot(luminance, textureLinear(envmap, uv).rgb);\n\n // lambertian BRDF\n float brdf = INVPI;\n float scatteringPdf = abs(cosThetaL) * INVPI;\n\n float weight = powerHeuristic(scatteringPdf, lightPdf);\n\n float lightEq = irr * brdf * abs(cosThetaL) * weight / scatteringPdf;\n\n alpha += lightEq;\n li += occluded * lightEq;\n\n return li;\n}\n\nvoid sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Path path) {\n mat3 basis = orthonormalBasis(si.normal);\n vec3 viewDir = -path.ray.d;\n vec3 color = bounce > 1 && !path.specularBounce ? sampleEnvmapFromDirection(-viewDir) : sampleBackgroundFromDirection(-viewDir);\n\n vec3 lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2());\n\n float alphaBounce = 0.0;\n\n vec3 li = path.beta * color * (\n importanceSampleLightShadowCatcher(si, viewDir, randomSampleVec2(), alphaBounce) +\n importanceSampleMaterialShadowCatcher(si, viewDir, lightDir, alphaBounce)\n );\n\n // alphaBounce contains the lighting of the shadow catcher *without* shadows\n alphaBounce = alphaBounce == 0.0 ? 1.0 : alphaBounce;\n\n // in post processing step, we divide by alpha to obtain the percentage of light relative to shadow for the shadow catcher\n path.alpha *= alphaBounce;\n\n // we only want the alpha division to affect the shadow catcher\n // factor in alpha to the previous light, so that dividing by alpha with the previous light cancels out this contribution\n path.li *= alphaBounce;\n\n // add path contribution\n path.li += li;\n\n // Get new path direction\n\n lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2());\n\n float cosThetaL = dot(si.normal, lightDir);\n\n // lambertian brdf with terms cancelled\n path.beta *= color;\n\n initRay(path.ray, si.position + EPS * lightDir, lightDir);\n\n // If new ray direction is pointing into the surface,\n // the light path is physically impossible and we terminate the path.\n float orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n path.abort = orientation < 0.0;\n\n path.specularBounce = false;\n\n // advance dimension index by unused stratified samples\n const int usedSamples = 6;\n sampleIndex += SAMPLES_PER_MATERIAL - usedSamples;\n}\n\n#endif\n\n"; + var sampleShadowCatcher = "\n\n#ifdef USE_SHADOW_CATCHER\n\nvoid sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Path path) {\n bool lastBounce = bounce == BOUNCES;\n mat3 basis = orthonormalBasis(si.normal);\n vec3 viewDir = -path.ray.d;\n vec3 color = bounce == 1 || path.specularBounce ? sampleBackgroundFromDirection(-viewDir) : sampleEnvmapFromDirection(-viewDir);\n\n si.color = vec3(1, 1, 1);\n\n MaterialSamples samples = getRandomMaterialSamples();\n\n vec2 diffuseOrSpecular = samples.s1;\n vec2 lightDirSample = samples.s2;\n vec2 bounceDirSample = samples.s3;\n\n vec3 lightDir;\n vec2 uv;\n float lightPdf;\n bool brdfSample = false;\n\n if (diffuseOrSpecular.x < 0.5) {\n lightDir = 2.0 * diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ?\n lightDirDiffuse(si.faceNormal, viewDir, basis, lightDirSample) :\n lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, lightDirSample);\n uv = cartesianToEquirect(lightDir);\n lightPdf = envmapPdf(uv);\n brdfSample = true;\n } else {\n lightDir = sampleEnvmap(lightDirSample, uv, lightPdf);\n }\n\n float cosThetaL = dot(si.normal, lightDir);\n\n float liContrib = 1.0;\n\n float orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n if (orientation < 0.0) {\n liContrib = 0.0;\n }\n\n float occluded = 1.0;\n initRay(path.ray, si.position + EPS * lightDir, lightDir);\n if (intersectSceneShadow(path.ray)) {\n occluded = 0.0;\n }\n\n float irr = dot(luminance, textureLinear(envmap, uv).rgb);\n\n float scatteringPdf;\n vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf);\n\n float weight = brdfSample ?\n 2.0 * powerHeuristic(scatteringPdf, lightPdf) / scatteringPdf :\n 2.0 * powerHeuristic(lightPdf, scatteringPdf) / lightPdf;\n\n float liEq = liContrib * brdf.r * irr * abs(cosThetaL) * weight;\n\n float alpha = liEq;\n path.alpha *= alpha;\n path.li *= alpha;\n\n path.li += occluded * path.beta * color * liEq;\n\n if (lastBounce) {\n return;\n }\n\n lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ?\n lightDirDiffuse(si.faceNormal, viewDir, basis, bounceDirSample) :\n lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, bounceDirSample);\n\n cosThetaL = dot(si.normal, lightDir);\n\n orientation = dot(si.faceNormal, viewDir) * cosThetaL;\n path.abort = orientation < 0.0;\n\n if (path.abort) {\n return;\n }\n\n brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf);\n\n uv = cartesianToEquirect(lightDir);\n lightPdf = envmapPdf(uv);\n\n path.misWeight = 0.0;\n\n path.beta = color * abs(cosThetaL) * brdf.r / scatteringPdf;\n\n path.specularBounce = false;\n\n initRay(path.ray, si.position + EPS * lightDir, lightDir);\n}\n\n#endif\n\n"; - var sampleGlass = "\n\n#ifdef USE_GLASS\n\nvoid sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) {\n vec3 viewDir = -path.ray.d;\n float cosTheta = dot(si.normal, viewDir);\n\n float F = si.materialType == THIN_GLASS ?\n fresnelSchlick(abs(cosTheta), R0) : // thin glass\n fresnelSchlickTIR(cosTheta, R0, IOR); // thick glass\n\n vec3 lightDir;\n\n float reflectionOrRefraction = randomSample();\n\n if (reflectionOrRefraction < F) {\n lightDir = reflect(-viewDir, si.normal);\n } else {\n lightDir = si.materialType == THIN_GLASS ?\n refract(-viewDir, sign(cosTheta) * si.normal, INV_IOR_THIN) : // thin glass\n refract(-viewDir, sign(cosTheta) * si.normal, cosTheta < 0.0 ? IOR : INV_IOR); // thick glass\n path.beta *= si.color;\n }\n\n initRay(path.ray, si.position + EPS * lightDir, lightDir);\n\n // advance sample index by unused stratified samples\n const int usedSamples = 1;\n sampleIndex += SAMPLES_PER_MATERIAL - usedSamples;\n\n path.li += bounce == BOUNCES ? path.beta * sampleBackgroundFromDirection(lightDir) : vec3(0.0);\n}\n\n#endif\n\n"; + var sampleGlass = "\n\n#ifdef USE_GLASS\n\nvoid sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) {\n bool lastBounce = bounce == BOUNCES;\n vec3 viewDir = -path.ray.d;\n float cosTheta = dot(si.normal, viewDir);\n\n MaterialSamples samples = getRandomMaterialSamples();\n\n float reflectionOrRefraction = samples.s1.x;\n\n float F = si.materialType == THIN_GLASS ?\n fresnelSchlick(abs(cosTheta), R0) : // thin glass\n fresnelSchlickTIR(cosTheta, R0, IOR); // thick glass\n\n vec3 lightDir;\n\n if (reflectionOrRefraction < F) {\n lightDir = reflect(-viewDir, si.normal);\n } else {\n lightDir = si.materialType == THIN_GLASS ?\n refract(-viewDir, sign(cosTheta) * si.normal, INV_IOR_THIN) : // thin glass\n refract(-viewDir, sign(cosTheta) * si.normal, cosTheta < 0.0 ? IOR : INV_IOR); // thick glass\n path.beta *= si.color;\n }\n\n path.misWeight = 1.0;\n\n initRay(path.ray, si.position + EPS * lightDir, lightDir);\n\n path.li += lastBounce ? path.beta * sampleBackgroundFromDirection(lightDir) : vec3(0.0);\n\n path.specularBounce = true;\n}\n\n#endif\n\n"; var fragment$1 = { includes: [constants$1, rayTraceCore, textureLinear, materialBuffer, intersect, surfaceInteractionDirect, random, envmap, bsdf, sample, sampleMaterial, sampleGlass, sampleShadowCatcher], outputs: ['light'], source: function source(defines) { - return "\n void bounce(inout Path path, int i, inout SurfaceInteraction si) {\n if (!si.hit) {\n if (path.specularBounce) {\n path.li += path.beta * sampleBackgroundFromDirection(path.ray.d);\n }\n\n path.abort = true;\n } else {\n #ifdef USE_GLASS\n if (si.materialType == THIN_GLASS || si.materialType == THICK_GLASS) {\n sampleGlassSpecular(si, i, path);\n }\n #endif\n #ifdef USE_SHADOW_CATCHER\n if (si.materialType == SHADOW_CATCHER) {\n sampleShadowCatcher(si, i, path);\n }\n #endif\n if (si.materialType == STANDARD) {\n sampleMaterial(si, i, path);\n }\n\n // Russian Roulette sampling\n if (i >= 2) {\n float q = 1.0 - dot(path.beta, luminance);\n if (randomSample() < q) {\n path.abort = true;\n }\n path.beta /= 1.0 - q;\n }\n }\n }\n\n // Path tracing integrator as described in\n // http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Path_Tracing.html#\n vec4 integrator(inout Ray ray) {\n Path path;\n path.ray = ray;\n path.li = vec3(0);\n path.alpha = 1.0;\n path.beta = vec3(1.0);\n path.specularBounce = true;\n path.abort = false;\n\n SurfaceInteraction si;\n\n // first surface interaction from g-buffer\n surfaceInteractionDirect(vCoord, si);\n\n // first surface interaction from ray interesction\n // intersectScene(path.ray, si);\n\n bounce(path, 1, si);\n\n // Manually unroll for loop.\n // Some hardware fails to iterate over a GLSL loop, so we provide this workaround\n // for (int i = 1; i < defines.bounces + 1, i += 1)\n // equivelant to\n ".concat(unrollLoop('i', 2, defines.BOUNCES + 1, 1, "\n if (path.abort) {\n return vec4(path.li, path.alpha);\n }\n intersectScene(path.ray, si);\n bounce(path, i, si);\n "), "\n\n return vec4(path.li, path.alpha);\n }\n\n void main() {\n initRandom();\n\n vec2 vCoordAntiAlias = vCoord + jitter;\n\n vec3 direction = normalize(vec3(vCoordAntiAlias - 0.5, -1.0) * vec3(camera.aspect, 1.0, camera.fov));\n\n // Thin lens model with depth-of-field\n // http://www.pbr-book.org/3ed-2018/Camera_Models/Projective_Camera_Models.html#TheThinLensModelandDepthofField\n // vec2 lensPoint = camera.aperture * sampleCircle(randomSampleVec2());\n // vec3 focusPoint = -direction * camera.focus / direction.z; // intersect ray direction with focus plane\n\n // vec3 origin = vec3(lensPoint, 0.0);\n // direction = normalize(focusPoint - origin);\n\n // origin = vec3(camera.transform * vec4(origin, 1.0));\n // direction = mat3(camera.transform) * direction;\n\n vec3 origin = camera.transform[3].xyz;\n direction = mat3(camera.transform) * direction;\n\n Ray cam;\n initRay(cam, origin, direction);\n\n vec4 liAndAlpha = integrator(cam);\n\n if (!(liAndAlpha.x < INF && liAndAlpha.x > -EPS)) {\n liAndAlpha = vec4(0, 0, 0, 1);\n }\n\n out_light = liAndAlpha;\n\n // Stratified Sampling Sample Count Test\n // ---------------\n // Uncomment the following code\n // Then observe the colors of the image\n // If:\n // * The resulting image is pure black\n // Extra samples are being passed to the shader that aren't being used.\n // * The resulting image contains red\n // Not enough samples are being passed to the shader\n // * The resulting image contains only white with some black\n // All samples are used by the shader. Correct result!\n\n // fragColor = vec4(0, 0, 0, 1);\n // if (sampleIndex == SAMPLING_DIMENSIONS) {\n // fragColor = vec4(1, 1, 1, 1);\n // } else if (sampleIndex > SAMPLING_DIMENSIONS) {\n // fragColor = vec4(1, 0, 0, 1);\n // }\n}\n"); + return "\n void bounce(inout Path path, int i, inout SurfaceInteraction si) {\n\n if (!si.hit) {\n vec3 irr = path.specularBounce ? sampleBackgroundFromDirection(path.ray.d) : sampleEnvmapFromDirection(path.ray.d);\n\n // hit a light source (the hdr map)\n // add contribution from light source\n // path.misWeight is the multiple importance sampled weight of this light source\n path.li += path.misWeight * path.beta * irr;\n path.abort = true;\n return;\n }\n\n #ifdef USE_GLASS\n if (si.materialType == THIN_GLASS || si.materialType == THICK_GLASS) {\n sampleGlassSpecular(si, i, path);\n }\n #endif\n #ifdef USE_SHADOW_CATCHER\n if (si.materialType == SHADOW_CATCHER) {\n sampleShadowCatcher(si, i, path);\n }\n #endif\n if (si.materialType == STANDARD) {\n sampleMaterial(si, i, path);\n }\n\n // Russian Roulette sampling\n if (i >= 2) {\n float q = 1.0 - dot(path.beta, luminance);\n if (randomSample() < q) {\n path.abort = true;\n }\n path.beta /= 1.0 - q;\n }\n\n }\n\n // Path tracing integrator as described in\n // http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Path_Tracing.html#\n vec4 integrator(inout Ray ray) {\n Path path;\n path.ray = ray;\n path.li = vec3(0);\n path.alpha = 1.0;\n path.beta = vec3(1.0);\n path.specularBounce = true;\n path.abort = false;\n path.misWeight = 1.0;\n\n SurfaceInteraction si;\n\n // first surface interaction from g-buffer\n surfaceInteractionDirect(vCoord, si);\n\n // first surface interaction from ray interesction\n // intersectScene(path.ray, si);\n\n bounce(path, 1, si);\n\n // Manually unroll for loop.\n // Some hardware fails to iterate over a GLSL loop, so we provide this workaround\n // for (int i = 1; i < defines.bounces + 1, i += 1)\n // equivelant to\n ".concat(unrollLoop('i', 2, defines.BOUNCES + 1, 1, "\n if (path.abort) {\n return vec4(path.li, path.alpha);\n }\n intersectScene(path.ray, si);\n bounce(path, i, si);\n "), "\n\n return vec4(path.li, path.alpha);\n }\n\n void main() {\n initRandom();\n\n vec2 vCoordAntiAlias = vCoord + jitter;\n\n vec3 direction = normalize(vec3(vCoordAntiAlias - 0.5, -1.0) * vec3(camera.aspect, 1.0, camera.fov));\n\n // Thin lens model with depth-of-field\n // http://www.pbr-book.org/3ed-2018/Camera_Models/Projective_Camera_Models.html#TheThinLensModelandDepthofField\n // vec2 lensPoint = camera.aperture * sampleCircle(randomSampleVec2());\n // vec3 focusPoint = -direction * camera.focus / direction.z; // intersect ray direction with focus plane\n\n // vec3 origin = vec3(lensPoint, 0.0);\n // direction = normalize(focusPoint - origin);\n\n // origin = vec3(camera.transform * vec4(origin, 1.0));\n // direction = mat3(camera.transform) * direction;\n\n vec3 origin = camera.transform[3].xyz;\n direction = mat3(camera.transform) * direction;\n\n Ray cam;\n initRay(cam, origin, direction);\n\n vec4 liAndAlpha = integrator(cam);\n\n if (!(liAndAlpha.x < INF && liAndAlpha.x > -EPS)) {\n liAndAlpha = vec4(0, 0, 0, 1);\n }\n\n out_light = liAndAlpha;\n\n // Stratified Sampling Sample Count Test\n // ---------------\n // Uncomment the following code\n // Then observe the colors of the image\n // If:\n // * The resulting image is pure black\n // Extra samples are being passed to the shader that aren't being used.\n // * The resulting image contains red\n // Not enough samples are being passed to the shader\n // * The resulting image contains only white with some black\n // All samples are used by the shader. Correct result!\n\n // out_light = vec4(0, 0, 0, 1);\n // if (sampleIndex == SAMPLING_DIMENSIONS) {\n // out_light = vec4(1, 1, 1, 1);\n // } else if (sampleIndex > SAMPLING_DIMENSIONS) {\n // out_light = vec4(1, 0, 0, 1);\n // }\n}\n"); } }; @@ -2559,8 +2559,8 @@ var samplingDimensions = []; for (var i = 1; i <= bounces; i++) { - // specular or diffuse reflection, light importance sampling, material sampling, next path direction - samplingDimensions.push(2, 2, 2, 2); + // specular or diffuse reflection, light importance sampling, next path direction + samplingDimensions.push(2, 2, 2); if (i >= 2) { // russian roulette sampling @@ -3005,6 +3005,7 @@ // higher number results in faster convergence over time, but with lower quality initial samples var strataCount = 6; + var desiredTimeForPreview = 14; var decomposedScene = decomposeScene(scene); var mergedMesh = mergeMeshesToGeometry(decomposedScene.meshes); var materialBuffer = makeMaterialBuffer(gl, mergedMesh.materials); @@ -3173,7 +3174,6 @@ } function setPreviewBufferDimensions() { - var desiredTimeForPreview = 10; var numPixelsForPreview = desiredTimeForPreview / tileRender.getTimePerPixel(); var aspectRatio = screenWidth / screenHeight; previewWidth = Math.round(clamp(Math.sqrt(numPixelsForPreview * aspectRatio), 1, screenWidth)); diff --git a/build/RayTracingRenderer.js b/build/RayTracingRenderer.js index c668a7b..cb994a9 100644 --- a/build/RayTracingRenderer.js +++ b/build/RayTracingRenderer.js @@ -2123,8 +2123,6 @@ vec3 getMatNormal(int materialIndex, vec2 uv, vec3 normal, vec3 dp1, vec3 dp2, v #define THICK_GLASS 2 #define SHADOW_CATCHER 3 - #define SAMPLES_PER_MATERIAL 8 - const float IOR = 1.5; const float INV_IOR = 1.0 / IOR; @@ -2190,11 +2188,11 @@ vec3 getMatNormal(int materialIndex, vec2 uv, vec3 normal, vec3 dp1, vec3 dp2, v struct Path { Ray ray; vec3 li; - vec3 albedo; float alpha; vec3 beta; bool specularBounce; bool abort; + float misWeight; }; uniform Camera camera; @@ -2571,6 +2569,22 @@ float randomSample() { vec2 randomSampleVec2() { return vec2(randomSample(), randomSample()); } + +struct MaterialSamples { + vec2 s1; + vec2 s2; + vec2 s3; +}; + +MaterialSamples getRandomMaterialSamples() { + MaterialSamples samples; + + samples.s1 = randomSampleVec2(); + samples.s2 = randomSampleVec2(); + samples.s3 = randomSampleVec2(); + + return samples; +} `; // Sample the environment map using a cumulative distribution function as described in @@ -2844,255 +2858,205 @@ float powerHeuristic(float f, float g) { var sampleMaterial = ` -vec3 importanceSampleLight(SurfaceInteraction si, vec3 viewDir, bool lastBounce, vec2 random) { - vec3 li; - - float lightPdf; - vec2 uv; - vec3 lightDir = sampleEnvmap(random, uv, lightPdf); - - float cosThetaL = dot(si.normal, lightDir); - - float orientation = dot(si.faceNormal, viewDir) * cosThetaL; - if (orientation < 0.0) { - return li; - } - - float diffuseWeight = 1.0; - Ray ray; - initRay(ray, si.position + EPS * lightDir, lightDir); - if (intersectSceneShadow(ray)) { - if (lastBounce) { - diffuseWeight = 0.0; - } else { - return li; - } - } +void sampleMaterial(SurfaceInteraction si, int bounce, inout Path path) { + bool lastBounce = bounce == BOUNCES; + mat3 basis = orthonormalBasis(si.normal); + vec3 viewDir = -path.ray.d; - vec3 irr = textureLinear(envmap, uv).xyz; + MaterialSamples samples = getRandomMaterialSamples(); - float scatteringPdf; - vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, diffuseWeight, scatteringPdf); + vec2 diffuseOrSpecular = samples.s1; + vec2 lightDirSample = samples.s2; + vec2 bounceDirSample = samples.s3; - float weight = powerHeuristic(lightPdf, scatteringPdf); + // Step 1: Add direct illumination of the light source (the hdr map) + // On every bounce but the last, importance sample the light source + // On the last bounce, multiple importance sample the brdf AND the light source, determined by random var - li = brdf * irr * abs(cosThetaL) * weight / lightPdf; + vec3 lightDir; + vec2 uv; + float lightPdf; + bool brdfSample = false; - return li; -} + if (lastBounce && diffuseOrSpecular.x < 0.5) { + // reuse this sample by multiplying by 2 to bring sample from [0, 0.5), to [0, 1) + lightDir = 2.0 * diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ? + lightDirDiffuse(si.faceNormal, viewDir, basis, lightDirSample) : + lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, lightDirSample); -vec3 importanceSampleMaterial(SurfaceInteraction si, vec3 viewDir, bool lastBounce, vec3 lightDir) { - vec3 li; + uv = cartesianToEquirect(lightDir); + lightPdf = envmapPdf(uv); + brdfSample = true; + } else { + lightDir = sampleEnvmap(lightDirSample, uv, lightPdf); + } float cosThetaL = dot(si.normal, lightDir); + float occluded = 1.0; + float orientation = dot(si.faceNormal, viewDir) * cosThetaL; if (orientation < 0.0) { - return li; + // light dir points towards surface. invalid dir. + occluded = 0.0; } float diffuseWeight = 1.0; - Ray ray; - initRay(ray, si.position + EPS * lightDir, lightDir); - if (intersectSceneShadow(ray)) { + + initRay(path.ray, si.position + EPS * lightDir, lightDir); + if (intersectSceneShadow(path.ray)) { if (lastBounce) { diffuseWeight = 0.0; } else { - return li; + occluded = 0.0; } } - vec2 uv = cartesianToEquirect(lightDir); - - float lightPdf = envmapPdf(uv); - vec3 irr = textureLinear(envmap, uv).rgb; float scatteringPdf; vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, diffuseWeight, scatteringPdf); - float weight = powerHeuristic(scatteringPdf, lightPdf); + float weight; + if (lastBounce) { + weight = brdfSample ? + 2.0 * powerHeuristic(scatteringPdf, lightPdf) / scatteringPdf : + 2.0 * powerHeuristic(lightPdf, scatteringPdf) / lightPdf; + } else { + weight = powerHeuristic(lightPdf, scatteringPdf) / lightPdf; + } - li += brdf * irr * abs(cosThetaL) * weight / scatteringPdf; + path.li += path.beta * occluded * brdf * irr * abs(cosThetaL) * weight;; - return li; -} + // Step 2: Setup ray direction for next bounce by importance sampling the BRDF -void sampleMaterial(SurfaceInteraction si, int bounce, inout Path path) { - mat3 basis = orthonormalBasis(si.normal); - vec3 viewDir = -path.ray.d; - - vec2 diffuseOrSpecular = randomSampleVec2(); + if (lastBounce) { + return; + } - vec3 lightDir = diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ? - lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()) : - lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomSampleVec2()); + lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ? + lightDirDiffuse(si.faceNormal, viewDir, basis, bounceDirSample) : + lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, bounceDirSample); - bool lastBounce = bounce == BOUNCES; + cosThetaL = dot(si.normal, lightDir); - // Add path contribution - path.li += path.beta * ( - importanceSampleLight(si, viewDir, lastBounce, randomSampleVec2()) + - importanceSampleMaterial(si, viewDir, lastBounce, lightDir) - ); - - // Get new path direction + orientation = dot(si.faceNormal, viewDir) * cosThetaL; + path.abort = orientation < 0.0; - if (lastBounce) { + if (path.abort) { return; } - lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ? - lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()) : - lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomSampleVec2()); + brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf); - float cosThetaL = dot(si.normal, lightDir); + uv = cartesianToEquirect(lightDir); + lightPdf = envmapPdf(uv); - float scatteringPdf; - vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf); + path.misWeight = powerHeuristic(scatteringPdf, lightPdf); path.beta *= abs(cosThetaL) * brdf / scatteringPdf; - initRay(path.ray, si.position + EPS * lightDir, lightDir); - - // If new ray direction is pointing into the surface, - // the light path is physically impossible and we terminate the path. - float orientation = dot(si.faceNormal, viewDir) * cosThetaL; - path.abort = orientation < 0.0; - path.specularBounce = false; -} + initRay(path.ray, si.position + EPS * lightDir, lightDir); +} `; var sampleShadowCatcher = ` #ifdef USE_SHADOW_CATCHER -float importanceSampleLightShadowCatcher(SurfaceInteraction si, vec3 viewDir, vec2 random, inout float alpha) { - float li; - - float lightPdf; - vec2 uv; - vec3 lightDir = sampleEnvmap(random, uv, lightPdf); +void sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Path path) { + bool lastBounce = bounce == BOUNCES; + mat3 basis = orthonormalBasis(si.normal); + vec3 viewDir = -path.ray.d; + vec3 color = bounce == 1 || path.specularBounce ? sampleBackgroundFromDirection(-viewDir) : sampleEnvmapFromDirection(-viewDir); - float cosThetaL = dot(si.normal, lightDir); + si.color = vec3(1, 1, 1); - float orientation = dot(si.faceNormal, viewDir) * cosThetaL; - if (orientation < 0.0) { - return li; - } + MaterialSamples samples = getRandomMaterialSamples(); - float occluded = 1.0; + vec2 diffuseOrSpecular = samples.s1; + vec2 lightDirSample = samples.s2; + vec2 bounceDirSample = samples.s3; - Ray ray; - initRay(ray, si.position + EPS * lightDir, lightDir); - if (intersectSceneShadow(ray)) { - occluded = 0.0; + vec3 lightDir; + vec2 uv; + float lightPdf; + bool brdfSample = false; + + if (diffuseOrSpecular.x < 0.5) { + lightDir = 2.0 * diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ? + lightDirDiffuse(si.faceNormal, viewDir, basis, lightDirSample) : + lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, lightDirSample); + uv = cartesianToEquirect(lightDir); + lightPdf = envmapPdf(uv); + brdfSample = true; + } else { + lightDir = sampleEnvmap(lightDirSample, uv, lightPdf); } - float irr = dot(luminance, textureLinear(envmap, uv).rgb); - - // lambertian BRDF - float brdf = INVPI; - float scatteringPdf = abs(cosThetaL) * INVPI; - - float weight = powerHeuristic(lightPdf, scatteringPdf); - - float lightEq = irr * brdf * abs(cosThetaL) * weight / lightPdf; - - alpha += lightEq; - li += occluded * lightEq; - - return li; -} - -float importanceSampleMaterialShadowCatcher(SurfaceInteraction si, vec3 viewDir, vec3 lightDir, inout float alpha) { - float li; - float cosThetaL = dot(si.normal, lightDir); + float liContrib = 1.0; + float orientation = dot(si.faceNormal, viewDir) * cosThetaL; if (orientation < 0.0) { - return li; + liContrib = 0.0; } float occluded = 1.0; - - Ray ray; - initRay(ray, si.position + EPS * lightDir, lightDir); - if (intersectSceneShadow(ray)) { + initRay(path.ray, si.position + EPS * lightDir, lightDir); + if (intersectSceneShadow(path.ray)) { occluded = 0.0; } - vec2 uv = cartesianToEquirect(lightDir); - - float lightPdf = envmapPdf(uv); - float irr = dot(luminance, textureLinear(envmap, uv).rgb); - // lambertian BRDF - float brdf = INVPI; - float scatteringPdf = abs(cosThetaL) * INVPI; - - float weight = powerHeuristic(scatteringPdf, lightPdf); - - float lightEq = irr * brdf * abs(cosThetaL) * weight / scatteringPdf; - - alpha += lightEq; - li += occluded * lightEq; - - return li; -} - -void sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Path path) { - mat3 basis = orthonormalBasis(si.normal); - vec3 viewDir = -path.ray.d; - vec3 color = bounce > 1 && !path.specularBounce ? sampleEnvmapFromDirection(-viewDir) : sampleBackgroundFromDirection(-viewDir); + float scatteringPdf; + vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf); - vec3 lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()); + float weight = brdfSample ? + 2.0 * powerHeuristic(scatteringPdf, lightPdf) / scatteringPdf : + 2.0 * powerHeuristic(lightPdf, scatteringPdf) / lightPdf; - float alphaBounce = 0.0; + float liEq = liContrib * brdf.r * irr * abs(cosThetaL) * weight; - vec3 li = path.beta * color * ( - importanceSampleLightShadowCatcher(si, viewDir, randomSampleVec2(), alphaBounce) + - importanceSampleMaterialShadowCatcher(si, viewDir, lightDir, alphaBounce) - ); + float alpha = liEq; + path.alpha *= alpha; + path.li *= alpha; - // alphaBounce contains the lighting of the shadow catcher *without* shadows - alphaBounce = alphaBounce == 0.0 ? 1.0 : alphaBounce; + path.li += occluded * path.beta * color * liEq; - // in post processing step, we divide by alpha to obtain the percentage of light relative to shadow for the shadow catcher - path.alpha *= alphaBounce; + if (lastBounce) { + return; + } - // we only want the alpha division to affect the shadow catcher - // factor in alpha to the previous light, so that dividing by alpha with the previous light cancels out this contribution - path.li *= alphaBounce; + lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ? + lightDirDiffuse(si.faceNormal, viewDir, basis, bounceDirSample) : + lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, bounceDirSample); - // add path contribution - path.li += li; + cosThetaL = dot(si.normal, lightDir); - // Get new path direction + orientation = dot(si.faceNormal, viewDir) * cosThetaL; + path.abort = orientation < 0.0; - lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()); + if (path.abort) { + return; + } - float cosThetaL = dot(si.normal, lightDir); + brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf); - // lambertian brdf with terms cancelled - path.beta *= color; + uv = cartesianToEquirect(lightDir); + lightPdf = envmapPdf(uv); - initRay(path.ray, si.position + EPS * lightDir, lightDir); + path.misWeight = 0.0; - // If new ray direction is pointing into the surface, - // the light path is physically impossible and we terminate the path. - float orientation = dot(si.faceNormal, viewDir) * cosThetaL; - path.abort = orientation < 0.0; + path.beta = color * abs(cosThetaL) * brdf.r / scatteringPdf; path.specularBounce = false; - // advance dimension index by unused stratified samples - const int usedSamples = 6; - sampleIndex += SAMPLES_PER_MATERIAL - usedSamples; + initRay(path.ray, si.position + EPS * lightDir, lightDir); } #endif @@ -3104,17 +3068,20 @@ void sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Path path) { #ifdef USE_GLASS void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) { + bool lastBounce = bounce == BOUNCES; vec3 viewDir = -path.ray.d; float cosTheta = dot(si.normal, viewDir); + MaterialSamples samples = getRandomMaterialSamples(); + + float reflectionOrRefraction = samples.s1.x; + float F = si.materialType == THIN_GLASS ? fresnelSchlick(abs(cosTheta), R0) : // thin glass fresnelSchlickTIR(cosTheta, R0, IOR); // thick glass vec3 lightDir; - float reflectionOrRefraction = randomSample(); - if (reflectionOrRefraction < F) { lightDir = reflect(-viewDir, si.normal); } else { @@ -3124,13 +3091,13 @@ void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) { path.beta *= si.color; } + path.misWeight = 1.0; + initRay(path.ray, si.position + EPS * lightDir, lightDir); - // advance sample index by unused stratified samples - const int usedSamples = 1; - sampleIndex += SAMPLES_PER_MATERIAL - usedSamples; + path.li += lastBounce ? path.beta * sampleBackgroundFromDirection(lightDir) : vec3(0.0); - path.li += bounce == BOUNCES ? path.beta * sampleBackgroundFromDirection(lightDir) : vec3(0.0); + path.specularBounce = true; } #endif @@ -3156,36 +3123,41 @@ void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) { outputs: ['light'], source: (defines) => ` void bounce(inout Path path, int i, inout SurfaceInteraction si) { + if (!si.hit) { - if (path.specularBounce) { - path.li += path.beta * sampleBackgroundFromDirection(path.ray.d); - } + vec3 irr = path.specularBounce ? sampleBackgroundFromDirection(path.ray.d) : sampleEnvmapFromDirection(path.ray.d); + // hit a light source (the hdr map) + // add contribution from light source + // path.misWeight is the multiple importance sampled weight of this light source + path.li += path.misWeight * path.beta * irr; path.abort = true; - } else { - #ifdef USE_GLASS - if (si.materialType == THIN_GLASS || si.materialType == THICK_GLASS) { - sampleGlassSpecular(si, i, path); - } - #endif - #ifdef USE_SHADOW_CATCHER - if (si.materialType == SHADOW_CATCHER) { - sampleShadowCatcher(si, i, path); - } - #endif - if (si.materialType == STANDARD) { - sampleMaterial(si, i, path); + return; + } + + #ifdef USE_GLASS + if (si.materialType == THIN_GLASS || si.materialType == THICK_GLASS) { + sampleGlassSpecular(si, i, path); + } + #endif + #ifdef USE_SHADOW_CATCHER + if (si.materialType == SHADOW_CATCHER) { + sampleShadowCatcher(si, i, path); } + #endif + if (si.materialType == STANDARD) { + sampleMaterial(si, i, path); + } - // Russian Roulette sampling - if (i >= 2) { - float q = 1.0 - dot(path.beta, luminance); - if (randomSample() < q) { - path.abort = true; - } - path.beta /= 1.0 - q; + // Russian Roulette sampling + if (i >= 2) { + float q = 1.0 - dot(path.beta, luminance); + if (randomSample() < q) { + path.abort = true; } + path.beta /= 1.0 - q; } + } // Path tracing integrator as described in @@ -3198,6 +3170,7 @@ void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) { path.beta = vec3(1.0); path.specularBounce = true; path.abort = false; + path.misWeight = 1.0; SurfaceInteraction si; @@ -3268,11 +3241,11 @@ void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) { // * The resulting image contains only white with some black // All samples are used by the shader. Correct result! - // fragColor = vec4(0, 0, 0, 1); + // out_light = vec4(0, 0, 0, 1); // if (sampleIndex == SAMPLING_DIMENSIONS) { - // fragColor = vec4(1, 1, 1, 1); + // out_light = vec4(1, 1, 1, 1); // } else if (sampleIndex > SAMPLING_DIMENSIONS) { - // fragColor = vec4(1, 0, 0, 1); + // out_light = vec4(1, 0, 0, 1); // } } ` @@ -3395,8 +3368,8 @@ void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) { const samplingDimensions = []; for (let i = 1; i <= bounces; i++) { - // specular or diffuse reflection, light importance sampling, material sampling, next path direction - samplingDimensions.push(2, 2, 2, 2); + // specular or diffuse reflection, light importance sampling, next path direction + samplingDimensions.push(2, 2, 2); if (i >= 2) { // russian roulette sampling // this step is skipped on the first bounce @@ -4094,6 +4067,8 @@ void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) { // higher number results in faster convergence over time, but with lower quality initial samples const strataCount = 6; + const desiredTimeForPreview = 14; + const decomposedScene = decomposeScene(scene); const mergedMesh = mergeMeshesToGeometry(decomposedScene.meshes); @@ -4223,7 +4198,6 @@ void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) { } function setPreviewBufferDimensions() { - const desiredTimeForPreview = 10; const numPixelsForPreview = desiredTimeForPreview / tileRender.getTimePerPixel(); const aspectRatio = screenWidth / screenHeight; diff --git a/package-lock.json b/package-lock.json index 19973c5..2a0708a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ray-tracing-renderer", - "version": "0.6.4", + "version": "0.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 89ea093..4b966f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ray-tracing-renderer", - "version": "0.6.4", + "version": "0.7.0", "description": "A [Three.js](https://github.com/mrdoob/three.js/) renderer which utilizes path tracing to render a scene with true photorealism. The renderer supports global illumination, reflections, soft shadows, and realistic environment lighting.", "main": "build/RayTracingRenderer.js", "scripts": {