diff --git a/src/RayTracingRenderer.js b/src/RayTracingRenderer.js index 769b351..05e1f9f 100644 --- a/src/RayTracingRenderer.js +++ b/src/RayTracingRenderer.js @@ -1,5 +1,5 @@ import { loadExtensions } from './renderer/glUtil'; -import { makeSceneSampler } from './renderer/sceneSampler'; +import { makeRenderingPipeline } from './renderer/renderingPipeline'; import * as THREE from 'three'; const glRequiredExtensions = [ @@ -25,7 +25,7 @@ function RayTracingRenderer(params = {}) { const optionalExtensions = loadExtensions(gl, glOptionalExtensions); // private properties - let sceneSampler = null; + let pipeline = null; const size = new THREE.Vector2(); let renderTime = 22; let pixelRatio = 1; @@ -54,9 +54,9 @@ function RayTracingRenderer(params = {}) { const bounces = module.bounces; - sceneSampler = makeSceneSampler({gl, optionalExtensions, scene, toneMappingParams, bounces}); + pipeline = makeRenderingPipeline({gl, optionalExtensions, scene, toneMappingParams, bounces}); - sceneSampler.onSampleRendered = (...args) => { + pipeline.onSampleRendered = (...args) => { if (module.onSampleRendered) { module.onSampleRendered(...args); } @@ -68,8 +68,8 @@ function RayTracingRenderer(params = {}) { } function restartTimer() { - if (sceneSampler) { - sceneSampler.restartTimer(); + if (pipeline) { + pipeline.restartTimer(); } } @@ -83,8 +83,8 @@ function RayTracingRenderer(params = {}) { canvas.style.height = `${ size.height }px`; } - if (sceneSampler) { - sceneSampler.setSize(size.width * pixelRatio, size.height * pixelRatio); + if (pipeline) { + pipeline.setSize(size.width * pixelRatio, size.height * pixelRatio); } }; @@ -108,8 +108,8 @@ function RayTracingRenderer(params = {}) { module.setRenderTime = (time) => { renderTime = time; - if (sceneSampler) { - sceneSampler.setRenderTime(time); + if (pipeline) { + pipeline.setRenderTime(time); } }; @@ -118,14 +118,14 @@ function RayTracingRenderer(params = {}) { }; module.getTotalSamplesRendered = () => { - if (sceneSampler) { - return sceneSampler.getTotalSamplesRendered(); + if (pipeline) { + return pipeline.getTotalSamplesRendered(); } }; module.sendToScreen = () => { - if (sceneSampler) { - sceneSampler.hdrBufferToScreen(); + if (pipeline) { + pipeline.hdrBufferToScreen(); } }; @@ -151,14 +151,14 @@ function RayTracingRenderer(params = {}) { if (module.renderToScreen) { if(module.maxHardwareUsage) { // render new sample for the entire screen - sceneSampler.drawFull(camera); + pipeline.drawFull(camera); } else { // render new sample for a tiled subset of the screen - sceneSampler.drawTile(camera); + pipeline.drawTile(camera); } } else { - sceneSampler.drawOffscreenTile(camera); + pipeline.drawOffscreenTile(camera); } }; @@ -170,7 +170,7 @@ function RayTracingRenderer(params = {}) { module.dispose = () => { document.removeEventListener('visibilitychange', restartTimer); - sceneSampler = false; + pipeline = false; }; return module; diff --git a/src/renderer/glsl/chunks/random.glsl b/src/renderer/glsl/chunks/random.glsl index 486b972..ba47c6f 100644 --- a/src/renderer/glsl/chunks/random.glsl +++ b/src/renderer/glsl/chunks/random.glsl @@ -1,20 +1,26 @@ -// Random number generation as described by -// http://www.reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11/ - export default function(params) { return ` -// higher quality but slower hashing function -uint wangHash(uint x) { - x = (x ^ 61u) ^ (x >> 16u); - x *= 9u; - x = x ^ (x >> 4u); - x *= 0x27d4eb2du; - x = x ^ (x >> 15u); - return x; -} +// Noise texture used to generate a different random number for each pixel. +// We use blue noise in particular, but any type of noise will work. +uniform sampler2D noise; + +uniform float stratifiedSamples[SAMPLING_DIMENSIONS]; +uniform float strataSize; +uniform float useStratifiedSampling; + +// Every time we call randomSample() in the shader, and for every call to render, +// we want that specific bit of the shader to fetch a sample from the same position in stratifiedSamples +// This allows us to use stratified sampling for each random variable in our path tracing +int sampleIndex = 0; + +const highp float maxUint = 1.0 / 4294967295.0; + +float pixelSeed; +highp uint randState; -// lower quality but faster hashing function +// simple integer hashing function +// https://en.wikipedia.org/wiki/Xorshift uint xorshift(uint x) { x ^= x << 13u; x ^= x >> 17u; @@ -22,41 +28,34 @@ uint xorshift(uint x) { return x; } -uniform float seed; // Random number [0, 1) -uniform float strataStart[STRATA_DIMENSIONS]; -uniform float strataSize; +void initRandom() { + vec2 noiseSize = vec2(textureSize(noise, 0)); -const highp float maxUint = 1.0 / 4294967295.0; -highp uint randState; -int strataDimension; + // tile the small noise texture across the entire screen + pixelSeed = texture(noise, vCoord / (pixelSize * noiseSize)).r; -// init state with high quality hashing function to avoid patterns across the 2d image -void initRandom() { - randState = wangHash(floatBitsToUint(seed)); - randState *= wangHash(floatBitsToUint(vCoord.x)); - randState *= wangHash(floatBitsToUint(vCoord.y)); - randState = wangHash(randState); - strataDimension = 0; + // white noise used if stratified sampling is disabled + // produces more balanced path tracing for 1 sample-per-pixel renders + randState = xorshift(xorshift(floatBitsToUint(vCoord.x)) * xorshift(floatBitsToUint(vCoord.y))); } -float random() { +float randomSample() { randState = xorshift(randState); - float f = float(randState) * maxUint; - // transform random number between [0, 1] to (0, 1) - return EPS + (1.0 - 2.0 * EPS) * f; -} + float stratifiedSample = stratifiedSamples[sampleIndex++]; -vec2 randomVec2() { - return vec2(random(), random()); -} + float random = mix( + float(randState) * maxUint, // white noise + fract((stratifiedSample + pixelSeed) * strataSize), // blue noise + stratified samples + useStratifiedSampling + ); -float randomStrata() { - return strataStart[strataDimension++] + strataSize * random(); + // transform random number between [0, 1] to (0, 1) + return EPS + (1.0 - 2.0 * EPS) * random; } -vec2 randomStrataVec2() { - return vec2(randomStrata(), randomStrata()); +vec2 randomSampleVec2() { + return vec2(randomSample(), randomSample()); } `; }; diff --git a/src/renderer/glsl/chunks/sampleGlassMicrofacet.glsl b/src/renderer/glsl/chunks/sampleGlassMicrofacet.glsl index 70c0e8b..31e0e6d 100644 --- a/src/renderer/glsl/chunks/sampleGlassMicrofacet.glsl +++ b/src/renderer/glsl/chunks/sampleGlassMicrofacet.glsl @@ -150,18 +150,18 @@ vec3 sampleGlassMicrofacet(SurfaceInteraction si, int bounce, inout Ray ray, ino float F = fresnelSchlickTIR(cosThetaV, R0, IOR); // thick glass - vec2 reflectionOrRefraction = randomStrataVec2(); + vec2 reflectionOrRefraction = randomSampleVec2(); vec3 lightDir; bool lightRefract; float pdf; if (reflectionOrRefraction.x < F) { - lightDir = lightDirSpecular(si.normal, viewDir, basis, si.roughness, randomStrataVec2()); + lightDir = lightDirSpecular(si.normal, viewDir, basis, si.roughness, randomSampleVec2()); lightRefract = false; pdf = F; } else { - lightDir = lightDirRefraction(si.normal, viewDir, basis, si.roughness, randomStrataVec2()); + lightDir = lightDirRefraction(si.normal, viewDir, basis, si.roughness, randomSampleVec2()); lightRefract = true; pdf = 1.0 - F; } @@ -169,7 +169,7 @@ vec3 sampleGlassMicrofacet(SurfaceInteraction si, int bounce, inout Ray ray, ino bool lastBounce = bounce == BOUNCES; vec3 li = beta * ( - glassImportanceSampleLight(si, viewDir, lightRefract, lastBounce, randomStrataVec2()) + + glassImportanceSampleLight(si, viewDir, lightRefract, lastBounce, randomSampleVec2()) + glassImportanceSampleMaterial(si, viewDir, lightRefract, lastBounce, lightDir) ); @@ -180,13 +180,13 @@ vec3 sampleGlassMicrofacet(SurfaceInteraction si, int bounce, inout Ray ray, ino vec3 brdf; if (reflectionOrRefraction.y < F) { - lightDir = lightDirSpecular(si.normal, viewDir, basis, si.roughness, randomStrataVec2()); + lightDir = lightDirSpecular(si.normal, viewDir, basis, si.roughness, randomSampleVec2()); cosThetaL = dot(si.normal, lightDir); brdf = glassReflection(si, viewDir, lightDir, cosThetaL, scatteringPdf); scatteringPdf *= F; lightRefract = false; } else { - lightDir = lightDirRefraction(si.normal, viewDir, basis, si.roughness, randomStrataVec2()); + lightDir = lightDirRefraction(si.normal, viewDir, basis, si.roughness, randomSampleVec2()); cosThetaL = dot(si.normal, lightDir); brdf = glassRefraction(si, viewDir, lightDir, cosThetaL, scatteringPdf); scatteringPdf *= 1.0 - F; diff --git a/src/renderer/glsl/chunks/sampleGlassSpecular.glsl b/src/renderer/glsl/chunks/sampleGlassSpecular.glsl index 34c4cdc..6e841ab 100644 --- a/src/renderer/glsl/chunks/sampleGlassSpecular.glsl +++ b/src/renderer/glsl/chunks/sampleGlassSpecular.glsl @@ -13,7 +13,7 @@ vec3 sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 lightDir; - float reflectionOrRefraction = randomStrata(); + float reflectionOrRefraction = randomSample(); if (reflectionOrRefraction < F) { lightDir = reflect(-viewDir, si.normal); @@ -26,9 +26,9 @@ vec3 sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Ray ray, inout initRay(ray, si.position + EPS * lightDir, lightDir); - // advance strata index by unused stratified samples - const int usedStrata = 1; - strataDimension += STRATA_PER_MATERIAL - usedStrata; + // advance sample index by unused stratified samples + const int usedDimensions = 1; + sampleIndex += DIMENSIONS_PER_MATERIAL - usedDimensions; return bounce == BOUNCES ? beta * sampleEnvmapFromDirection(lightDir) : vec3(0.0); } diff --git a/src/renderer/glsl/chunks/sampleMaterial.glsl b/src/renderer/glsl/chunks/sampleMaterial.glsl index 1ce81d3..fa8c8f5 100644 --- a/src/renderer/glsl/chunks/sampleMaterial.glsl +++ b/src/renderer/glsl/chunks/sampleMaterial.glsl @@ -82,25 +82,25 @@ vec3 sampleMaterial(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 mat3 basis = orthonormalBasis(si.normal); vec3 viewDir = -ray.d; - vec2 diffuseOrSpecular = randomStrataVec2(); + vec2 diffuseOrSpecular = randomSampleVec2(); vec3 lightDir = diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ? - lightDirDiffuse(si.faceNormal, viewDir, basis, randomStrataVec2()) : - lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomStrataVec2()); + lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()) : + lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomSampleVec2()); bool lastBounce = bounce == BOUNCES; // Add path contribution vec3 li = beta * ( - importanceSampleLight(si, viewDir, lastBounce, randomStrataVec2()) + + importanceSampleLight(si, viewDir, lastBounce, randomSampleVec2()) + importanceSampleMaterial(si, viewDir, lastBounce, lightDir) ); // Get new path direction lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ? - lightDirDiffuse(si.faceNormal, viewDir, basis, randomStrataVec2()) : - lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomStrataVec2()); + lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()) : + lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomSampleVec2()); float cosThetaL = dot(si.normal, lightDir); diff --git a/src/renderer/glsl/chunks/sampleShadowCatcher.glsl b/src/renderer/glsl/chunks/sampleShadowCatcher.glsl index 8a54567..42f5836 100644 --- a/src/renderer/glsl/chunks/sampleShadowCatcher.glsl +++ b/src/renderer/glsl/chunks/sampleShadowCatcher.glsl @@ -84,13 +84,13 @@ vec3 sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 viewDir = -ray.d; vec3 color = sampleEnvmapFromDirection(-viewDir); - vec3 lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomStrataVec2()); + vec3 lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()); float alphaBounce = 0.0; // Add path contribution vec3 li = beta * color * ( - importanceSampleLightShadowCatcher(si, viewDir, randomStrataVec2(), alphaBounce) + + importanceSampleLightShadowCatcher(si, viewDir, randomSampleVec2(), alphaBounce) + importanceSampleMaterialShadowCatcher(si, viewDir, lightDir, alphaBounce) ); @@ -106,7 +106,7 @@ vec3 sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Ray ray, inout // Get new path direction - lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomStrataVec2()); + lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()); float cosThetaL = dot(si.normal, lightDir); @@ -120,9 +120,9 @@ vec3 sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Ray ray, inout float orientation = dot(si.faceNormal, viewDir) * cosThetaL; abort = orientation < 0.0; - // advance strata index by unused stratified samples - const int usedStrata = 6; - strataDimension += STRATA_PER_MATERIAL - usedStrata; + // advance dimension index by unused stratified samples + const int usedDimensions = 6; + sampleIndex += DIMENSIONS_PER_MATERIAL - usedDimensions; return li; } diff --git a/src/renderer/glsl/rayTrace.frag b/src/renderer/glsl/rayTrace.frag index 8ac0cb0..c906665 100644 --- a/src/renderer/glsl/rayTrace.frag +++ b/src/renderer/glsl/rayTrace.frag @@ -31,7 +31,7 @@ ${addDefines(params)} #define THICK_GLASS 2 #define SHADOW_CATCHER 3 -#define STRATA_PER_MATERIAL 8 +#define DIMENSIONS_PER_MATERIAL 8 const float IOR = 1.5; const float INV_IOR = 1.0 / IOR; @@ -153,7 +153,7 @@ void bounce(inout Path path, int i) { // Russian Roulette sampling if (i >= 2) { float q = 1.0 - dot(path.beta, luminance); - if (randomStrata() < q) { + if (randomSample() < q) { path.abort = true; } path.beta /= 1.0 - q; @@ -187,13 +187,13 @@ vec4 integrator(inout Ray ray) { void main() { initRandom(); - vec2 vCoordAntiAlias = vCoord + pixelSize * (randomStrataVec2() - 0.5); + vec2 vCoordAntiAlias = vCoord + pixelSize * (randomSampleVec2() - 0.5); vec3 direction = normalize(vec3(vCoordAntiAlias - 0.5, -1.0) * vec3(camera.aspect, 1.0, camera.fov)); // Thin lens model with depth-of-field // http://www.pbr-book.org/3ed-2018/Camera_Models/Projective_Camera_Models.html#TheThinLensModelandDepthofField - vec2 lensPoint = camera.aperture * sampleCircle(randomStrataVec2()); + vec2 lensPoint = camera.aperture * sampleCircle(randomSampleVec2()); vec3 focusPoint = -direction * camera.focus / direction.z; // intersect ray direction with focus plane vec3 origin = vec3(lensPoint, 0.0); @@ -226,9 +226,9 @@ void main() { // All samples are used by the shader. Correct result! // fragColor = vec4(0, 0, 0, 1); - // if (strataDimension == STRATA_DIMENSIONS) { + // if (sampleIndex == SAMPLING_DIMENSIONS) { // fragColor = vec4(1, 1, 1, 1); - // } else if (strataDimension > STRATA_DIMENSIONS) { + // } else if (sampleIndex > SAMPLING_DIMENSIONS) { // fragColor = vec4(1, 0, 0, 1); // } } diff --git a/src/renderer/rayTracingShader.js b/src/renderer/rayTracingShader.js index 9e3f734..c93a3f0 100644 --- a/src/renderer/rayTracingShader.js +++ b/src/renderer/rayTracingShader.js @@ -1,96 +1,18 @@ +import * as THREE from 'three'; import fragString from './glsl/rayTrace.frag'; import { createShader, createProgram, getUniforms } from './glUtil'; import { mergeMeshesToGeometry } from './mergeMeshesToGeometry'; import { bvhAccel, flattenBvh } from './bvhAccel'; import { envmapDistribution } from './envmapDistribution'; import { generateEnvMapFromSceneComponents } from './envMapCreation'; -import { makeStratifiedRandomCombined } from './stratifiedRandomCombined'; import { getTexturesFromMaterials, mergeTexturesFromMaterials } from './texturesFromMaterials'; import { makeTexture } from './texture'; import { uploadBuffers } from './uploadBuffers'; import { ThinMaterial, ThickMaterial, ShadowCatcherMaterial } from '../constants'; -import * as THREE from 'three'; import { clamp } from './util'; +import { makeStratifiedSamplerCombined } from './stratifiedSamplerCombined'; -function textureDimensionsFromArray(count) { - const columnsLog = Math.round(Math.log2(Math.sqrt(count))); - const columns = 2 ** columnsLog; - const rows = Math.ceil(count / columns); - return { - columnsLog, - columns, - rows, - size: rows * columns, - }; -} - -function maxImageSize(images) { - const maxSize = { - width: 0, - height: 0 - }; - - for (const image of images) { - maxSize.width = Math.max(maxSize.width, image.width); - maxSize.height = Math.max(maxSize.height, image.height); - } - - const relativeSizes = []; - for (const image of images) { - relativeSizes.push(image.width / maxSize.width); - relativeSizes.push(image.height / maxSize.height); - } - - return { maxSize, relativeSizes }; -} - -// expand array to the given length -function padArray(typedArray, length) { - const newArray = new typedArray.constructor(length); - newArray.set(typedArray); - return newArray; -} - -function isHDRTexture(texture) { - return texture.map - && texture.map.image - && (texture.map.encoding === THREE.RGBEEncoding || texture.map.encoding === THREE.LinearEncoding); -} - -function decomposeScene(scene) { - const meshes = []; - const directionalLights = []; - const environmentLights = []; - scene.traverse(child => { - if (child instanceof THREE.Mesh) { - if (!child.geometry || !child.geometry.getAttribute('position')) { - console.log(child, 'must have a geometry property with a position attribute'); - } - else if (!(child.material instanceof THREE.MeshStandardMaterial)) { - console.log(child, 'must use MeshStandardMaterial in order to be rendered.'); - } else { - meshes.push(child); - } - } - if (child instanceof THREE.DirectionalLight) { - directionalLights.push(child); - } - if (child instanceof THREE.EnvironmentLight) { - if (environmentLights.length > 1) { - console.warn('Only one environment light can be used per scene'); - } - else if (isHDRTexture(child)) { - environmentLights.push(child); - } else { - console.warn('Environment light has invalid map'); - } - } - }); - - return { - meshes, directionalLights, environmentLights - }; -} +//Important TODO: Refactor this file to get rid of duplicate and confusing code export function makeRayTracingShader({ gl, @@ -105,17 +27,15 @@ export function makeRayTracingShader({ const { OES_texture_float_linear } = optionalExtensions; - // Use stratified sampling for random variables to reduce clustering of samples thus improving rendering quality. - // Each element of this array specifies how many dimensions belong to each set of stratified samples - const strataDimensions = []; - strataDimensions.push(2, 2); // anti-aliasing, depth-of-field + const samplingDimensions = []; + samplingDimensions.push(2, 2); // anti aliasing, depth of field for (let i = 0; i < bounces; i++) { - // specular or diffuse reflection, light importance sampling, material importance sampling, next path direction - strataDimensions.push(2, 2, 2, 2); + // specular or diffuse reflection, light importance sampling, material sampling, next path direction + samplingDimensions.push(2, 2, 2, 2); if (i >= 1) { // russian roulette sampling // this step is skipped on the first bounce - strataDimensions.push(1); + samplingDimensions.push(1); } } @@ -159,7 +79,7 @@ export function makeRayTracingShader({ BOUNCES: bounces, USE_GLASS: useGlass, USE_SHADOW_CATCHER: useShadowCatcher, - STRATA_DIMENSIONS: strataDimensions.reduce((a, b) => a + b) + SAMPLING_DIMENSIONS: samplingDimensions.reduce((a, b) => a + b) })); const program = createProgram(gl, fullscreenQuad.vertexShader, fragmentShader); @@ -294,13 +214,21 @@ export function makeRayTracingShader({ const { program, uniforms } = initScene(); - let random = null; - function setSize(width, height) { gl.useProgram(program); gl.uniform2f(uniforms.pixelSize, 1 / width, 1 / height); } + // noiseImage is a 32-bit PNG image + function setNoise(noiseImage) { + textureAllocator.bind(uniforms.noise, makeTexture(gl, { + data: noiseImage, + minFilter: gl.NEAREST, + magFilter: gl.NEAREST, + storage: 'float' + })); + } + function setCamera(camera) { gl.useProgram(program); gl.uniformMatrix4fv(uniforms['camera.transform'], false, camera.matrixWorld.elements); @@ -310,15 +238,32 @@ export function makeRayTracingShader({ gl.uniform1f(uniforms['camera.aperture'], camera.aperture || 0); } - function setStrataCount(count) { - random = makeStratifiedRandomCombined(count, strataDimensions); + let samples; + + function nextSeed() { + gl.useProgram(program); + gl.uniform1fv(uniforms['stratifiedSamples[0]'], samples.next()); + } + + function setStrataCount(strataCount) { + gl.useProgram(program); + + if (strataCount > 1 && strataCount !== samples.strataCount) { + // reinitailizing random has a performance cost. we can skip it if + // * strataCount is 1, since a strataCount of 1 works with any sized StratifiedRandomCombined + // * random already has the same strata count as desired + samples = makeStratifiedSamplerCombined(strataCount, samplingDimensions); + } else { + samples.restart(); + } + + gl.uniform1f(uniforms.strataSize, 1.0 / strataCount); + nextSeed(); } - function updateSeed() { + function useStratifiedSampling(stratifiedSampling) { gl.useProgram(program); - gl.uniform1f(uniforms.strataSize, 1.0 / random.strataCount); - gl.uniform1fv(uniforms['strataStart[0]'], random.next()); - gl.uniform1f(uniforms.seed, Math.random()); + gl.uniform1f(uniforms.useStratifiedSampling, stratifiedSampling ? 1.0 : 0.0); } function draw() { @@ -326,11 +271,95 @@ export function makeRayTracingShader({ fullscreenQuad.draw(); } + samples = makeStratifiedSamplerCombined(1, samplingDimensions); + return Object.freeze({ - setSize, + draw, + nextSeed, setCamera, + setNoise, + setSize, setStrataCount, - updateSeed, - draw + useStratifiedSampling }); } + +function textureDimensionsFromArray(count) { + const columnsLog = Math.round(Math.log2(Math.sqrt(count))); + const columns = 2 ** columnsLog; + const rows = Math.ceil(count / columns); + return { + columnsLog, + columns, + rows, + size: rows * columns, + }; +} + +function maxImageSize(images) { + const maxSize = { + width: 0, + height: 0 + }; + + for (const image of images) { + maxSize.width = Math.max(maxSize.width, image.width); + maxSize.height = Math.max(maxSize.height, image.height); + } + + const relativeSizes = []; + for (const image of images) { + relativeSizes.push(image.width / maxSize.width); + relativeSizes.push(image.height / maxSize.height); + } + + return { maxSize, relativeSizes }; +} + +// expand array to the given length +function padArray(typedArray, length) { + const newArray = new typedArray.constructor(length); + newArray.set(typedArray); + return newArray; +} + +function isHDRTexture(texture) { + return texture.map + && texture.map.image + && (texture.map.encoding === THREE.RGBEEncoding || texture.map.encoding === THREE.LinearEncoding); +} + +function decomposeScene(scene) { + const meshes = []; + const directionalLights = []; + const environmentLights = []; + scene.traverse(child => { + if (child instanceof THREE.Mesh) { + if (!child.geometry || !child.geometry.getAttribute('position')) { + console.warn(child, 'must have a geometry property with a position attribute'); + } + else if (!(child.material instanceof THREE.MeshStandardMaterial)) { + console.warn(child, 'must use MeshStandardMaterial in order to be rendered.'); + } else { + meshes.push(child); + } + } + if (child instanceof THREE.DirectionalLight) { + directionalLights.push(child); + } + if (child instanceof THREE.EnvironmentLight) { + if (environmentLights.length > 1) { + console.warn(environmentLights, 'only one environment light can be used per scene'); + } + else if (isHDRTexture(child)) { + environmentLights.push(child); + } else { + console.warn(child, 'environment light uses invalid map'); + } + } + }); + + return { + meshes, directionalLights, environmentLights + }; +} diff --git a/src/renderer/sceneSampler.js b/src/renderer/renderingPipeline.js similarity index 80% rename from src/renderer/sceneSampler.js rename to src/renderer/renderingPipeline.js index a6d0c5b..e0b5f18 100644 --- a/src/renderer/sceneSampler.js +++ b/src/renderer/renderingPipeline.js @@ -6,8 +6,11 @@ import { numberArraysEqual, clamp } from './util'; import { makeTileRender } from './tileRender'; import { LensCamera } from '../LensCamera'; import { makeTextureAllocator } from './textureAllocator'; +import noiseBase64 from './texture/noise'; -export function makeSceneSampler({ +// Important TODO: Refactor this file to get rid of duplicate and confusing code + +export function makeRenderingPipeline({ gl, optionalExtensions, scene, @@ -15,11 +18,20 @@ export function makeSceneSampler({ bounces, // number of global illumination bounces }) { + let ready = false; + const fullscreenQuad = makeFullscreenQuad(gl); const textureAllocator = makeTextureAllocator(gl); const rayTracingShader = makeRayTracingShader({gl, optionalExtensions, fullscreenQuad, textureAllocator, scene, bounces}); const toneMapShader = makeToneMapShader({gl, optionalExtensions, fullscreenQuad, textureAllocator, toneMappingParams}); + const noiseImage = new Image(); + noiseImage.src = noiseBase64; + noiseImage.onload = () => { + rayTracingShader.setNoise(noiseImage); + ready = true; + }; + const useLinearFiltering = optionalExtensions.OES_texture_float_linear; const hdrBuffer = makeRenderTargetFloat(gl); // full resolution buffer representing the rendered scene with HDR lighting @@ -30,11 +42,12 @@ export function makeSceneSampler({ const lastCamera = new LensCamera(); - // how many samples to render with simple noise before switching to stratified noise - const numSimpleSamples = 4; + // how many samples to render with uniform noise before switching to stratified noise + const numUniformSamples = 6; // how many partitions of stratified noise should be created - const strataSize = 6; + // higher number results in faster convergence over time, but with lower quality initial samples + const strataCount = 6; let sampleCount = 0; @@ -47,13 +60,12 @@ export function makeSceneSampler({ sampleCount = 0; tileRender.reset(); - rayTracingShader.setStrataCount(1); - rayTracingShader.updateSeed(); } function initFirstSample(camera) { lastCamera.copy(camera); rayTracingShader.setCamera(camera); + rayTracingShader.useStratifiedSampling(false); clear(); } @@ -117,8 +129,21 @@ export function makeSceneSampler({ }); } + function updateSeed() { + if (sampleCount === 2) { + rayTracingShader.useStratifiedSampling(true); + rayTracingShader.setStrataCount(1); + } else if (sampleCount === numUniformSamples) { + rayTracingShader.setStrataCount(strataCount); + } else { + rayTracingShader.nextSeed(); + } + } + function drawTile(camera) { - if (!camerasEqual(camera, lastCamera)) { + if (!ready) { + return; + } else if (!camerasEqual(camera, lastCamera)) { initFirstSample(camera); setPreviewBufferDimensions(); renderPreview(); @@ -127,11 +152,7 @@ export function makeSceneSampler({ if (isFirstTile) { sampleCount++; - rayTracingShader.updateSeed(); - - if (sampleCount === numSimpleSamples) { - rayTracingShader.setStrataCount(strataSize); - } + updateSeed(); } renderTile(x, y, tileWidth, tileHeight); @@ -144,7 +165,9 @@ export function makeSceneSampler({ } function drawOffscreenTile(camera) { - if (!camerasEqual(camera, lastCamera)) { + if (!ready) { + return; + } else if (!camerasEqual(camera, lastCamera)) { initFirstSample(camera); } @@ -152,12 +175,7 @@ export function makeSceneSampler({ if (isFirstTile) { sampleCount++; - rayTracingShader.updateSeed(); - - - if (sampleCount === numSimpleSamples) { - rayTracingShader.setStrataCount(strataSize); - } + updateSeed(); } renderTile(x, y, tileWidth, tileHeight); @@ -168,17 +186,15 @@ export function makeSceneSampler({ } function drawFull(camera) { - if (!camerasEqual(camera, lastCamera)) { + if (!ready) { + return; + } else if (!camerasEqual(camera, lastCamera)) { initFirstSample(camera); } - if (sampleCount === numSimpleSamples) { - rayTracingShader.setStrataCount(strataSize); - } - sampleCount++; - rayTracingShader.updateSeed(); + updateSeed(); addSampleToBuffer(hdrBuffer); hdrBufferToScreen(); } diff --git a/src/renderer/stratifiedRandom.js b/src/renderer/stratifiedRandom.js deleted file mode 100644 index 3a2d29e..0000000 --- a/src/renderer/stratifiedRandom.js +++ /dev/null @@ -1,47 +0,0 @@ -// Stratified Sampling -// http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html -// Repeatedly generating random numbers between [0, 1) has the effect of producing numbers that are coincidentally clustered together, -// instead of being evenly spaced across the domain. -// This produces low quality results for the path tracer since clustered numbers send too many rays in similar directions. - -// We can reduce the amount of clustering of random numbers by using stratified sampling. -// This is done by partitioning [0, 1) into smaller subsets and randomly sampling from each subsets instead. - -import { shuffle } from "./util"; - -export function makeStratifiedRandom(strataCount, dimensions) { - const samples = []; - const l = strataCount ** dimensions; - for (let i = 0; i < l; i++) { - samples[i] = i; - } - - let index = samples.length; - - const randomNums = []; - - function reset() { - index = 0; - shuffle(samples); - } - - function next() { - if (index >= samples.length) { - reset(); - } - let sample = samples[index++]; - - for (let i = 0; i < dimensions; i++) { - randomNums[i] = (sample % strataCount) / strataCount; - sample = Math.floor(sample / strataCount); - } - - return randomNums; - } - - return Object.freeze({ - reset, - next, - strataCount - }); -} diff --git a/src/renderer/stratifiedRandomCombined.js b/src/renderer/stratifiedRandomCombined.js deleted file mode 100644 index 90fe84c..0000000 --- a/src/renderer/stratifiedRandomCombined.js +++ /dev/null @@ -1,43 +0,0 @@ -// Stratified Sampling -// http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html -// It is computationally unfeasible to compute stratified sampling for large dimensions (>2) -// Instead, we can compute stratified sampling for lower dimensional patterns that sum to the high dimension -// e.g. instead of sampling a 6D domain, we sample a 2D + 2D + 2D domain. -// This reaps most of the benifits of stratification while still remaining computable in a reasonable amount of time - -import { makeStratifiedRandom } from "./stratifiedRandom"; - -export function makeStratifiedRandomCombined(strataCount, listOfDimensions) { - const strataObjs = []; - for (const dim of listOfDimensions) { - strataObjs.push(makeStratifiedRandom(strataCount, dim)); - } - - const randomNums = []; - - function reset() { - for (const strata of strataObjs) { - strata.reset(); - } - } - - function next() { - let i = 0; - - for (const strata of strataObjs) { - const nums = strata.next(); - - for (const num of nums) { - randomNums[i++] = num; - } - } - - return randomNums; - } - - return Object.freeze({ - next, - reset, - strataCount - }); -} diff --git a/src/renderer/stratifiedSampler.js b/src/renderer/stratifiedSampler.js new file mode 100644 index 0000000..efdd8fd --- /dev/null +++ b/src/renderer/stratifiedSampler.js @@ -0,0 +1,58 @@ +/* +Stratified Sampling +http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html + +Repeatedly sampling random numbers between [0, 1) has the effect of producing numbers that are coincidentally clustered together, +instead of being evenly spaced across the domain. +This produces low quality results for the path tracer since clustered samples send too many rays in similar directions. + +We can reduce the amount of clustering of random numbers by using stratified sampling. +Stratification divides the [0, 1) range into partitions, or stratum, of equal size. +Each invocation of the stratified sampler draws one uniform random number from one stratum from a shuffled sequence of stratums. +When every stratum has been sampled once, this sequence is shuffled again and the process repeats. + +The returned sample ranges between [0, numberOfStratum). +The integer part ideintifies the stratum (the first stratum being 0). +The fractional part is the random number. + +To obtain the stratified sample between [0, 1), divide the returned sample by the stratum count. +*/ + +import { shuffle } from "./util"; + +export function makeStratifiedSampler(strataCount, dimensions) { + const strata = []; + const l = strataCount ** dimensions; + for (let i = 0; i < l; i++) { + strata[i] = i; + } + + let index = strata.length; + + const sample = []; + + function restart() { + index = 0; + } + + function next() { + if (index >= strata.length) { + shuffle(strata); + restart(); + } + let stratum = strata[index++]; + + for (let i = 0; i < dimensions; i++) { + sample[i] = stratum % strataCount + Math.random(); + stratum = Math.floor(stratum / strataCount); + } + + return sample; + } + + return Object.freeze({ + next, + restart, + strataCount + }); +} diff --git a/src/renderer/stratifiedSamplerCombined.js b/src/renderer/stratifiedSamplerCombined.js new file mode 100644 index 0000000..c30ec9f --- /dev/null +++ b/src/renderer/stratifiedSamplerCombined.js @@ -0,0 +1,47 @@ +/* +Stratified Sampling +http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html + +It is computationally unfeasible to compute stratified sampling for large dimensions (>2) +Instead, we can compute stratified sampling for lower dimensional patterns that sum to the high dimension +e.g. instead of sampling a 6D domain, we sample a 2D + 2D + 2D domain. +This reaps many benefits of stratification while still allowing for small strata sizes. +*/ + +import { makeStratifiedSampler } from "./stratifiedSampler"; + +export function makeStratifiedSamplerCombined(strataCount, listOfDimensions) { + const strataObjs = []; + + for (const dim of listOfDimensions) { + strataObjs.push(makeStratifiedSampler(strataCount, dim)); + } + + const combined = []; + + function next() { + let i = 0; + + for (const strata of strataObjs) { + const nums = strata.next(); + + for (const num of nums) { + combined[i++] = num; + } + } + + return combined; + } + + function restart() { + for (const strata of strataObjs) { + strata.restart(); + } + } + + return Object.freeze({ + next, + restart, + strataCount + }); +} diff --git a/src/renderer/texture.js b/src/renderer/texture.js index e5273d8..79fdc90 100644 --- a/src/renderer/texture.js +++ b/src/renderer/texture.js @@ -47,7 +47,7 @@ export function makeTexture(gl, params) { channels = clamp(channels, 1, 4); const format = [ - gl.R, + gl.RED, gl.RG, gl.RGB, gl.RGBA diff --git a/src/renderer/texture/HDR_L_0.png b/src/renderer/texture/HDR_L_0.png new file mode 100644 index 0000000..5730c03 Binary files /dev/null and b/src/renderer/texture/HDR_L_0.png differ diff --git a/src/renderer/texture/noise.js b/src/renderer/texture/noise.js new file mode 100644 index 0000000..62ec9d4 --- /dev/null +++ b/src/renderer/texture/noise.js @@ -0,0 +1 @@ +export default 'data:image/png;base64,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'; \ No newline at end of file diff --git a/src/renderer/texture/readme.txt b/src/renderer/texture/readme.txt new file mode 100644 index 0000000..a6aee3f --- /dev/null +++ b/src/renderer/texture/readme.txt @@ -0,0 +1,5 @@ +noise.js is a 64x64 resolution precomputed blue noise texture. +It was created by Christoph Peters and downloaded from http://momentsingraphics.de/BlueNoise.html + +The included PNG is converted to base64 and stored as a string inside of noise.js. +Storing the texture as a base64 instead of an image lets us include the texture in the javascript bundle directly.