From 7c875ca88f1aa71678bc00ca3cb42f69e2ee0765 Mon Sep 17 00:00:00 2001 From: Lucas Crane Date: Wed, 19 Feb 2020 16:01:39 -0800 Subject: [PATCH] Next event estimation (#71) * test next event estimation * start implementing shadow catcher * add shadow catcher support (kindof) * big cleanup * more tweaks * revert scene changes * set strataCount back to default --- scenes/renderer-test/main.js | 2 + scenes/sample-models/main.js | 2 +- src/renderer/RayTracePass.js | 4 +- src/renderer/RenderingPipeline.js | 3 +- src/renderer/glsl/chunks/random.glsl | 16 ++ src/renderer/glsl/chunks/rayTraceCore.glsl | 4 +- .../glsl/chunks/sampleGlassSpecular.glsl | 15 +- src/renderer/glsl/chunks/sampleMaterial.glsl | 139 ++++++++--------- .../glsl/chunks/sampleShadowCatcher.glsl | 147 +++++++----------- src/renderer/glsl/rayTrace.frag | 58 +++---- 10 files changed, 183 insertions(+), 207 deletions(-) diff --git a/scenes/renderer-test/main.js b/scenes/renderer-test/main.js index 5118941..00285a9 100644 --- a/scenes/renderer-test/main.js +++ b/scenes/renderer-test/main.js @@ -197,6 +197,8 @@ function init() { const geo = new THREE.PlaneBufferGeometry(1000, 1000); const mat = new THREE.MeshStandardMaterial(); mat.shadowCatcher = true; + mat.roughness = 0.5; + mat.metalness = 0.0; const mesh = new THREE.Mesh(geo, mat); mesh.rotateX(Math.PI / 2); model.add(mesh); diff --git a/scenes/sample-models/main.js b/scenes/sample-models/main.js index f6b1ece..f43263f 100644 --- a/scenes/sample-models/main.js +++ b/scenes/sample-models/main.js @@ -105,7 +105,7 @@ function createGroundMesh() { const geo = new THREE.PlaneBufferGeometry(100, 100); const mat = new THREE.MeshStandardMaterial(); mat.color.set(0xffffff); - mat.roughness = 1.0; + mat.roughness = 0.5; mat.metalness = 0.0; mat.shadowCatcher = true; const mesh = new THREE.Mesh(geo, mat); diff --git a/src/renderer/RayTracePass.js b/src/renderer/RayTracePass.js index a60de22..ef8fcac 100644 --- a/src/renderer/RayTracePass.js +++ b/src/renderer/RayTracePass.js @@ -21,8 +21,8 @@ export function makeRayTracePass(gl, { 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 diff --git a/src/renderer/RenderingPipeline.js b/src/renderer/RenderingPipeline.js index c79bf48..3756ed8 100644 --- a/src/renderer/RenderingPipeline.js +++ b/src/renderer/RenderingPipeline.js @@ -30,6 +30,8 @@ export function makeRenderingPipeline({ // 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); @@ -159,7 +161,6 @@ export function makeRenderingPipeline({ } function setPreviewBufferDimensions() { - const desiredTimeForPreview = 10; const numPixelsForPreview = desiredTimeForPreview / tileRender.getTimePerPixel(); const aspectRatio = screenWidth / screenHeight; diff --git a/src/renderer/glsl/chunks/random.glsl b/src/renderer/glsl/chunks/random.glsl index 7be8ff4..f29dac6 100644 --- a/src/renderer/glsl/chunks/random.glsl +++ b/src/renderer/glsl/chunks/random.glsl @@ -33,4 +33,20 @@ 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; +} `; diff --git a/src/renderer/glsl/chunks/rayTraceCore.glsl b/src/renderer/glsl/chunks/rayTraceCore.glsl index 818879f..da597c7 100644 --- a/src/renderer/glsl/chunks/rayTraceCore.glsl +++ b/src/renderer/glsl/chunks/rayTraceCore.glsl @@ -4,8 +4,6 @@ export default ` #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; @@ -71,11 +69,11 @@ export default ` struct Path { Ray ray; vec3 li; - vec3 albedo; float alpha; vec3 beta; bool specularBounce; bool abort; + float misWeight; }; uniform Camera camera; diff --git a/src/renderer/glsl/chunks/sampleGlassSpecular.glsl b/src/renderer/glsl/chunks/sampleGlassSpecular.glsl index 0849541..e7c0c2c 100644 --- a/src/renderer/glsl/chunks/sampleGlassSpecular.glsl +++ b/src/renderer/glsl/chunks/sampleGlassSpecular.glsl @@ -3,17 +3,20 @@ export default ` #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 { @@ -23,13 +26,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 diff --git a/src/renderer/glsl/chunks/sampleMaterial.glsl b/src/renderer/glsl/chunks/sampleMaterial.glsl index f40b924..da44c93 100644 --- a/src/renderer/glsl/chunks/sampleMaterial.glsl +++ b/src/renderer/glsl/chunks/sampleMaterial.glsl @@ -3,123 +3,106 @@ export default ` -vec3 importanceSampleLight(SurfaceInteraction si, vec3 viewDir, bool lastBounce, vec2 random) { - vec3 li; +void sampleMaterial(SurfaceInteraction si, int bounce, inout Path path) { + bool lastBounce = bounce == BOUNCES; + mat3 basis = orthonormalBasis(si.normal); + vec3 viewDir = -path.ray.d; - float lightPdf; - vec2 uv; - vec3 lightDir = sampleEnvmap(random, uv, lightPdf); + MaterialSamples samples = getRandomMaterialSamples(); - float cosThetaL = dot(si.normal, lightDir); + vec2 diffuseOrSpecular = samples.s1; + vec2 lightDirSample = samples.s2; + vec2 bounceDirSample = samples.s3; - float orientation = dot(si.faceNormal, viewDir) * cosThetaL; - if (orientation < 0.0) { - return li; - } + // 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 - float diffuseWeight = 1.0; - Ray ray; - initRay(ray, si.position + EPS * lightDir, lightDir); - if (intersectSceneShadow(ray)) { - if (lastBounce) { - diffuseWeight = 0.0; - } else { - return li; - } + vec3 lightDir; + vec2 uv; + float lightPdf; + bool brdfSample = false; + + 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); + + uv = cartesianToEquirect(lightDir); + lightPdf = envmapPdf(uv); + brdfSample = true; + } else { + lightDir = sampleEnvmap(lightDirSample, uv, lightPdf); } - vec3 irr = textureLinear(envmap, uv).xyz; - - float scatteringPdf; - vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, diffuseWeight, scatteringPdf); - - float weight = powerHeuristic(lightPdf, scatteringPdf); - - li = brdf * irr * abs(cosThetaL) * weight / lightPdf; - - return li; -} - -vec3 importanceSampleMaterial(SurfaceInteraction si, vec3 viewDir, bool lastBounce, vec3 lightDir) { - vec3 li; - 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); - - li += brdf * irr * abs(cosThetaL) * weight / scatteringPdf; - - return li; -} + float weight; + if (lastBounce) { + weight = brdfSample ? + 2.0 * powerHeuristic(scatteringPdf, lightPdf) / scatteringPdf : + 2.0 * powerHeuristic(lightPdf, scatteringPdf) / lightPdf; + } else { + weight = powerHeuristic(lightPdf, scatteringPdf) / lightPdf; + } -void sampleMaterial(SurfaceInteraction si, int bounce, inout Path path) { - mat3 basis = orthonormalBasis(si.normal); - vec3 viewDir = -path.ray.d; + path.li += path.beta * occluded * brdf * irr * abs(cosThetaL) * weight;; - vec2 diffuseOrSpecular = randomSampleVec2(); + // Step 2: Setup ray direction for next bounce by importance sampling the BRDF - 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()); + if (lastBounce) { + return; + } - bool lastBounce = bounce == BOUNCES; + 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 += path.beta * ( - importanceSampleLight(si, viewDir, lastBounce, randomSampleVec2()) + - importanceSampleMaterial(si, viewDir, lastBounce, lightDir) - ); + cosThetaL = dot(si.normal, 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); +} `; diff --git a/src/renderer/glsl/chunks/sampleShadowCatcher.glsl b/src/renderer/glsl/chunks/sampleShadowCatcher.glsl index 0fbe08e..419a0fd 100644 --- a/src/renderer/glsl/chunks/sampleShadowCatcher.glsl +++ b/src/renderer/glsl/chunks/sampleShadowCatcher.glsl @@ -2,130 +2,97 @@ export default ` #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 diff --git a/src/renderer/glsl/rayTrace.frag b/src/renderer/glsl/rayTrace.frag index 2076efa..7364929 100644 --- a/src/renderer/glsl/rayTrace.frag +++ b/src/renderer/glsl/rayTrace.frag @@ -32,36 +32,41 @@ includes: [ 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 @@ -74,6 +79,7 @@ source: (defines) => ` path.beta = vec3(1.0); path.specularBounce = true; path.abort = false; + path.misWeight = 1.0; SurfaceInteraction si; @@ -144,11 +150,11 @@ source: (defines) => ` // * 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); // } } `