diff --git a/build/RayTracingRenderer.es5.js b/build/RayTracingRenderer.es5.js index 6fcb346..cbc59ca 100644 --- a/build/RayTracingRenderer.es5.js +++ b/build/RayTracingRenderer.es5.js @@ -1,8 +1,8 @@ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) : typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) : - (factory((global.RayTracingRenderer = {}),global.THREE)); -}(this, (function (exports,THREE$1) { 'use strict'; + (global = global || self, factory(global.RayTracingRenderer = {}, global.THREE)); +}(this, function (exports, THREE$1) { 'use strict'; var ThinMaterial = 1; var ThickMaterial = 2; @@ -128,6 +128,10 @@ return _get(target, property, receiver || target); } + function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); + } + function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } @@ -140,14 +144,48 @@ } } + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; + } + function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } + function _iterableToArrayLimit(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + var LensCamera = /*#__PURE__*/ function (_PerspectiveCamera) { @@ -214,6 +252,39 @@ return SoftDirectionalLight; }(THREE$1.DirectionalLight); + var EnvironmentLight = + /*#__PURE__*/ + function (_Light) { + _inherits(EnvironmentLight, _Light); + + function EnvironmentLight(map) { + var _getPrototypeOf2; + + var _this; + + _classCallCheck(this, EnvironmentLight); + + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(EnvironmentLight)).call.apply(_getPrototypeOf2, [this].concat(args))); + _this.map = map; + return _this; + } + + _createClass(EnvironmentLight, [{ + key: "copy", + value: function copy(source) { + _get(_getPrototypeOf(EnvironmentLight.prototype), "copy", this).call(this, source); + + this.map = source.map; + } + }]); + + return EnvironmentLight; + }(THREE$1.Light); + var RayTracingMaterial = /*#__PURE__*/ function (_MeshStandardMaterial) { @@ -265,8 +336,8 @@ _iteratorError = err; } finally { try { - if (!_iteratorNormalCompletion && _iterator.return != null) { - _iterator.return(); + if (!_iteratorNormalCompletion && _iterator["return"] != null) { + _iterator["return"](); } } finally { if (_didIteratorError) { @@ -464,27 +535,25 @@ // Manually performs linear filtering if the extension OES_texture_float_linear is not supported function textureLinear (params) { - return "\n\n ".concat(params.OES_texture_float_linear ? '#define OES_texture_float_linear' : '', "\n\n vec4 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"); + return "\n\n vec4 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"; } function intersect (params) { - return "\n\n#define BVH_COLUMNS ".concat(params.bvhColumnsLog, "\n#define INDEX_COLUMNS ").concat(params.indexColumnsLog, "\n#define VERTEX_COLUMNS ").concat(params.vertexColumnsLog, "\n#define STACK_SIZE ").concat(params.maxBvhDepth, "\n#define NUM_TRIS ").concat(params.numTris, "\n#define NUM_MATERIALS ").concat(params.numMaterials, "\n").concat(params.numDiffuseMaps > 0 ? "#define NUM_DIFFUSE_MAPS ".concat(params.numDiffuseMaps) : '', "\n").concat(params.numNormalMaps > 0 ? "#define NUM_NORMAL_MAPS ".concat(params.numNormalMaps) : '', "\n").concat(params.numPbrMaps > 0 ? "#define NUM_PBR_MAPS ".concat(params.numPbrMaps) : '', "\n\nuniform highp isampler2D indices;\nuniform sampler2D positions;\nuniform sampler2D normals;\nuniform sampler2D uvs;\nuniform sampler2D bvh;\n\nuniform Materials {\n vec4 colorAndMaterialType[NUM_MATERIALS];\n vec4 roughnessMetalnessNormalScale[NUM_MATERIALS];\n\n #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS) || defined(NUM_PBR_MAPS)\n ivec4 diffuseNormalRoughnessMetalnessMapIndex[NUM_MATERIALS];\n #endif\n\n #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS)\n vec4 diffuseNormalMapSize[").concat(Math.max(params.numDiffuseMaps, params.numNormalMaps), "];\n #endif\n\n #if defined(NUM_PBR_MAPS)\n vec2 pbrMapSize[NUM_PBR_MAPS];\n #endif\n} materials;\n\n#ifdef NUM_DIFFUSE_MAPS\n uniform mediump sampler2DArray diffuseMap;\n#endif\n\n#ifdef NUM_NORMAL_MAPS\n uniform mediump sampler2DArray normalMap;\n#endif\n\n#ifdef NUM_PBR_MAPS\n uniform mediump sampler2DArray pbrMap;\n#endif\n\nstruct Triangle {\n vec3 p0;\n vec3 p1;\n vec3 p2;\n};\n\nvoid surfaceInteractionFromIntersection(inout SurfaceInteraction si, Triangle tri, vec3 barycentric, ivec3 index, vec3 faceNormal, int materialIndex) {\n si.hit = true;\n si.faceNormal = faceNormal;\n si.position = barycentric.x * tri.p0 + barycentric.y * tri.p1 + barycentric.z * tri.p2;\n ivec2 i0 = unpackTexel(index.x, VERTEX_COLUMNS);\n ivec2 i1 = unpackTexel(index.y, VERTEX_COLUMNS);\n ivec2 i2 = unpackTexel(index.z, VERTEX_COLUMNS);\n\n vec3 n0 = texelFetch(normals, i0, 0).xyz;\n vec3 n1 = texelFetch(normals, i1, 0).xyz;\n vec3 n2 = texelFetch(normals, i2, 0).xyz;\n si.normal = normalize(barycentric.x * n0 + barycentric.y * n1 + barycentric.z * n2);\n\n si.color = materials.colorAndMaterialType[materialIndex].xyz;\n si.roughness = materials.roughnessMetalnessNormalScale[materialIndex].x;\n si.metalness = materials.roughnessMetalnessNormalScale[materialIndex].y;\n\n si.materialType = int(materials.colorAndMaterialType[materialIndex].w);\n\n #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS) || defined(NUM_PBR_MAPS)\n vec2 uv0 = texelFetch(uvs, i0, 0).xy;\n vec2 uv1 = texelFetch(uvs, i1, 0).xy;\n vec2 uv2 = texelFetch(uvs, i2, 0).xy;\n vec2 uv = fract(barycentric.x * uv0 + barycentric.y * uv1 + barycentric.z * uv2);\n #endif\n\n #ifdef NUM_DIFFUSE_MAPS\n int diffuseMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].x;\n if (diffuseMapIndex >= 0) {\n si.color *= texture(diffuseMap, vec3(uv * materials.diffuseNormalMapSize[diffuseMapIndex].xy, diffuseMapIndex)).rgb;\n }\n #endif\n\n #ifdef NUM_NORMAL_MAPS\n int normalMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].y;\n if (normalMapIndex >= 0) {\n vec2 duv02 = uv0 - uv2;\n vec2 duv12 = uv1 - uv2;\n vec3 dp02 = tri.p0 - tri.p2;\n vec3 dp12 = tri.p1 - tri.p2;\n\n // Method One\n // http://www.pbr-book.org/3ed-2018/Shapes/Triangle_Meshes.html#fragment-Computetrianglepartialderivatives-0\n // Compute tangent vectors relative to the face normal. These vectors won't necessarily be orthogonal to the smoothed normal\n // This means the TBN matrix won't be orthogonal which is technically incorrect.\n // This is Three.js's method (https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/normalmap_pars_fragment.glsl.js)\n // --------------\n // float scale = sign(duv02.x * duv12.y - duv02.y * duv12.x);\n // vec3 dpdu = normalize((duv12.y * dp02 - duv02.y * dp12) * scale);\n // vec3 dpdv = normalize((-duv12.x * dp02 + duv02.x * dp12) * scale);\n\n // Method Two\n // Compute tangent vectors as in Method One but apply Gram-Schmidt process to make vectors orthogonal to smooth normal\n // This might inadvertently flip coordinate space orientation\n // --------------\n // float scale = sign(duv02.x * duv12.y - duv02.y * duv12.x);\n // vec3 dpdu = normalize((duv12.y * dp02 - duv02.y * dp12) * scale);\n // dpdu = (dpdu - dot(dpdu, si.normal) * si.normal); // Gram-Schmidt process\n // vec3 dpdv = cross(si.normal, dpdu) * scale;\n\n // Method Three\n // http://www.thetenthplanet.de/archives/1180\n // Compute co-tangent and co-bitangent vectors\n // These vectors are orthongal and maintain a consistent coordinate space\n // --------------\n vec3 dp12perp = cross(dp12, si.normal);\n vec3 dp02perp = cross(si.normal, dp02);\n vec3 dpdu = dp12perp * duv02.x + dp02perp * duv12.x;\n vec3 dpdv = dp12perp * duv02.y + dp02perp * duv12.y;\n float invmax = inversesqrt(max(dot(dpdu, dpdu), dot(dpdv, dpdv)));\n dpdu *= invmax;\n dpdv *= invmax;\n\n vec3 n = 2.0 * texture(normalMap, vec3(uv * materials.diffuseNormalMapSize[normalMapIndex].zw, normalMapIndex)).rgb - 1.0;\n n.xy *= materials.roughnessMetalnessNormalScale[materialIndex].zw;\n\n mat3 tbn = mat3(dpdu, dpdv, si.normal);\n\n si.normal = normalize(tbn * n);\n }\n #endif\n\n #ifdef NUM_PBR_MAPS\n int roughnessMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].z;\n int metalnessMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].w;\n if (roughnessMapIndex >= 0) {\n si.roughness *= texture(pbrMap, vec3(uv * materials.pbrMapSize[roughnessMapIndex].xy, roughnessMapIndex)).g;\n }\n if (metalnessMapIndex >= 0) {\n si.metalness *= texture(pbrMap, vec3(uv * materials.pbrMapSize[metalnessMapIndex].xy, metalnessMapIndex)).b;\n }\n #endif\n}\n\nstruct TriangleIntersect {\n float t;\n vec3 barycentric;\n};\n\n// Triangle-ray intersection\n// Faster than the classic M\xF6ller\u2013Trumbore intersection algorithm\n// http://www.pbr-book.org/3ed-2018/Shapes/Triangle_Meshes.html#TriangleIntersection\nTriangleIntersect intersectTriangle(Ray r, Triangle tri, int maxDim, vec3 shear) {\n TriangleIntersect ti;\n vec3 d = r.d;\n\n // translate vertices based on ray origin\n vec3 p0t = tri.p0 - r.o;\n vec3 p1t = tri.p1 - r.o;\n vec3 p2t = tri.p2 - r.o;\n\n // permute components of triangle vertices\n if (maxDim == 0) {\n p0t = p0t.yzx;\n p1t = p1t.yzx;\n p2t = p2t.yzx;\n } else if (maxDim == 1) {\n p0t = p0t.zxy;\n p1t = p1t.zxy;\n p2t = p2t.zxy;\n }\n\n // apply shear transformation to translated vertex positions\n p0t.xy += shear.xy * p0t.z;\n p1t.xy += shear.xy * p1t.z;\n p2t.xy += shear.xy * p2t.z;\n\n // compute edge function coefficients\n vec3 e = vec3(\n p1t.x * p2t.y - p1t.y * p2t.x,\n p2t.x * p0t.y - p2t.y * p0t.x,\n p0t.x * p1t.y - p0t.y * p1t.x\n );\n\n // check if intersection is inside triangle\n if (any(lessThan(e, vec3(0))) && any(greaterThan(e, vec3(0)))) {\n return ti;\n }\n\n float det = e.x + e.y + e.z;\n\n // not needed?\n // if (det == 0.) {\n // return ti;\n // }\n\n p0t.z *= shear.z;\n p1t.z *= shear.z;\n p2t.z *= shear.z;\n float tScaled = (e.x * p0t.z + e.y * p1t.z + e.z * p2t.z);\n\n // not needed?\n // if (sign(det) != sign(tScaled)) {\n // return ti;\n // }\n\n // check if closer intersection already exists\n if (abs(tScaled) > abs(r.tMax * det)) {\n return ti;\n }\n\n float invDet = 1. / det;\n ti.t = tScaled * invDet;\n ti.barycentric = e * invDet;\n\n return ti;\n}\n\nstruct Box {\n vec3 min;\n vec3 max;\n};\n\n// Branchless ray/box intersection\n// https://tavianator.com/fast-branchless-raybounding-box-intersections/\nfloat intersectBox(Ray r, Box b) {\n vec3 tBot = (b.min - r.o) * r.invD;\n vec3 tTop = (b.max - r.o) * r.invD;\n vec3 tNear = min(tBot, tTop);\n vec3 tFar = max(tBot, tTop);\n float t0 = max(tNear.x, max(tNear.y, tNear.z));\n float t1 = min(tFar.x, min(tFar.y, tFar.z));\n\n return (t0 > t1 || t0 > r.tMax) ? -1.0 : (t0 > 0.0 ? t0 : t1);\n}\n\nint maxDimension(vec3 v) {\n return v.x > v.y ? (v.x > v.z ? 0 : 2) : (v.y > v.z ? 1 : 2);\n}\n\n// Traverse BVH, find closest triangle intersection, and return surface information\nSurfaceInteraction intersectScene(inout Ray ray) {\n SurfaceInteraction si;\n\n int maxDim = maxDimension(abs(ray.d));\n\n // Permute space so that the z dimension is the one where the absolute value of the ray's direction is largest.\n // Then create a shear transformation that aligns ray direction with the +z axis\n vec3 shear;\n if (maxDim == 0) {\n shear = vec3(-ray.d.y, -ray.d.z, 1.0) * ray.invD.x;\n } else if (maxDim == 1) {\n shear = vec3(-ray.d.z, -ray.d.x, 1.0) * ray.invD.y;\n } else {\n shear = vec3(-ray.d.x, -ray.d.y, 1.0) * ray.invD.z;\n }\n\n int nodesToVisit[STACK_SIZE];\n int stack = 0;\n\n nodesToVisit[0] = 0;\n\n while(stack >= 0) {\n int i = nodesToVisit[stack--];\n\n vec4 r1 = fetchData(bvh, i, BVH_COLUMNS);\n vec4 r2 = fetchData(bvh, i + 1, BVH_COLUMNS);\n\n int splitAxisOrNumPrimitives = floatBitsToInt(r1.w);\n\n if (splitAxisOrNumPrimitives >= 0) {\n // Intersection is a bounding box. Test for box intersection and keep traversing BVH\n int splitAxis = splitAxisOrNumPrimitives;\n\n Box bbox = Box(r1.xyz, r2.xyz);\n\n if (intersectBox(ray, bbox) > 0.0) {\n // traverse near node to ray first, and far node to ray last\n if (ray.d[splitAxis] > 0.0) {\n nodesToVisit[++stack] = floatBitsToInt(r2.w);\n nodesToVisit[++stack] = i + 2;\n } else {\n nodesToVisit[++stack] = i + 2;\n nodesToVisit[++stack] = floatBitsToInt(r2.w);\n }\n }\n } else {\n ivec3 index = floatBitsToInt(r1.xyz);\n Triangle tri = Triangle(\n fetchData(positions, index.x, VERTEX_COLUMNS).xyz,\n fetchData(positions, index.y, VERTEX_COLUMNS).xyz,\n fetchData(positions, index.z, VERTEX_COLUMNS).xyz\n );\n TriangleIntersect hit = intersectTriangle(ray, tri, maxDim, shear);\n\n if (hit.t > 0.0) {\n ray.tMax = hit.t;\n int materialIndex = floatBitsToInt(r2.w);\n vec3 faceNormal = r2.xyz;\n surfaceInteractionFromIntersection(si, tri, hit.barycentric, index, faceNormal, materialIndex);\n }\n }\n }\n\n // Values must be clamped outside of intersection loop. Clamping inside the loop produces incorrect numbers on some devices.\n si.roughness = clamp(si.roughness, 0.03, 1.0);\n si.metalness = clamp(si.metalness, 0.0, 1.0);\n\n return si;\n}\n\nbool intersectSceneShadow(inout Ray ray) {\n int maxDim = maxDimension(abs(ray.d));\n\n // Permute space so that the z dimension is the one where the absolute value of the ray's direction is largest.\n // Then create a shear transformation that aligns ray direction with the +z axis\n vec3 shear;\n if (maxDim == 0) {\n shear = vec3(-ray.d.y, -ray.d.z, 1.0) * ray.invD.x;\n } else if (maxDim == 1) {\n shear = vec3(-ray.d.z, -ray.d.x, 1.0) * ray.invD.y;\n } else {\n shear = vec3(-ray.d.x, -ray.d.y, 1.0) * ray.invD.z;\n }\n\n int nodesToVisit[STACK_SIZE];\n int stack = 0;\n\n nodesToVisit[0] = 0;\n\n while(stack >= 0) {\n int i = nodesToVisit[stack--];\n\n vec4 r1 = fetchData(bvh, i, BVH_COLUMNS);\n vec4 r2 = fetchData(bvh, i + 1, BVH_COLUMNS);\n\n int splitAxisOrNumPrimitives = floatBitsToInt(r1.w);\n\n if (splitAxisOrNumPrimitives >= 0) {\n int splitAxis = splitAxisOrNumPrimitives;\n\n Box bbox = Box(r1.xyz, r2.xyz);\n\n if (intersectBox(ray, bbox) > 0.0) {\n if (ray.d[splitAxis] > 0.0) {\n nodesToVisit[++stack] = floatBitsToInt(r2.w);\n nodesToVisit[++stack] = i + 2;\n } else {\n nodesToVisit[++stack] = i + 2;\n nodesToVisit[++stack] = floatBitsToInt(r2.w);\n }\n }\n } else {\n ivec3 index = floatBitsToInt(r1.xyz);\n Triangle tri = Triangle(\n fetchData(positions, index.x, VERTEX_COLUMNS).xyz,\n fetchData(positions, index.y, VERTEX_COLUMNS).xyz,\n fetchData(positions, index.z, VERTEX_COLUMNS).xyz\n );\n\n if (intersectTriangle(ray, tri, maxDim, shear).t > 0.0) {\n return true;\n }\n }\n }\n\n return false;\n}\n"); + return "\n\nuniform highp isampler2D indices;\nuniform sampler2D positions;\nuniform sampler2D normals;\nuniform sampler2D uvs;\nuniform sampler2D bvh;\n\nuniform Materials {\n vec4 colorAndMaterialType[NUM_MATERIALS];\n vec4 roughnessMetalnessNormalScale[NUM_MATERIALS];\n\n #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS) || defined(NUM_PBR_MAPS)\n ivec4 diffuseNormalRoughnessMetalnessMapIndex[NUM_MATERIALS];\n #endif\n\n #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS)\n vec4 diffuseNormalMapSize[".concat(Math.max(params.NUM_DIFFUSE_MAPS, params.NUM_NORMAL_MAPS), "];\n #endif\n\n #if defined(NUM_PBR_MAPS)\n vec2 pbrMapSize[NUM_PBR_MAPS];\n #endif\n} materials;\n\n#ifdef NUM_DIFFUSE_MAPS\n uniform mediump sampler2DArray diffuseMap;\n#endif\n\n#ifdef NUM_NORMAL_MAPS\n uniform mediump sampler2DArray normalMap;\n#endif\n\n#ifdef NUM_PBR_MAPS\n uniform mediump sampler2DArray pbrMap;\n#endif\n\nstruct Triangle {\n vec3 p0;\n vec3 p1;\n vec3 p2;\n};\n\nvoid surfaceInteractionFromIntersection(inout SurfaceInteraction si, Triangle tri, vec3 barycentric, ivec3 index, vec3 faceNormal, int materialIndex) {\n si.hit = true;\n si.faceNormal = faceNormal;\n si.position = barycentric.x * tri.p0 + barycentric.y * tri.p1 + barycentric.z * tri.p2;\n ivec2 i0 = unpackTexel(index.x, VERTEX_COLUMNS);\n ivec2 i1 = unpackTexel(index.y, VERTEX_COLUMNS);\n ivec2 i2 = unpackTexel(index.z, VERTEX_COLUMNS);\n\n vec3 n0 = texelFetch(normals, i0, 0).xyz;\n vec3 n1 = texelFetch(normals, i1, 0).xyz;\n vec3 n2 = texelFetch(normals, i2, 0).xyz;\n si.normal = normalize(barycentric.x * n0 + barycentric.y * n1 + barycentric.z * n2);\n\n si.color = materials.colorAndMaterialType[materialIndex].xyz;\n si.roughness = materials.roughnessMetalnessNormalScale[materialIndex].x;\n si.metalness = materials.roughnessMetalnessNormalScale[materialIndex].y;\n\n si.materialType = int(materials.colorAndMaterialType[materialIndex].w);\n\n #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS) || defined(NUM_PBR_MAPS)\n vec2 uv0 = texelFetch(uvs, i0, 0).xy;\n vec2 uv1 = texelFetch(uvs, i1, 0).xy;\n vec2 uv2 = texelFetch(uvs, i2, 0).xy;\n vec2 uv = fract(barycentric.x * uv0 + barycentric.y * uv1 + barycentric.z * uv2);\n #endif\n\n #ifdef NUM_DIFFUSE_MAPS\n int diffuseMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].x;\n if (diffuseMapIndex >= 0) {\n si.color *= texture(diffuseMap, vec3(uv * materials.diffuseNormalMapSize[diffuseMapIndex].xy, diffuseMapIndex)).rgb;\n }\n #endif\n\n #ifdef NUM_NORMAL_MAPS\n int normalMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].y;\n if (normalMapIndex >= 0) {\n vec2 duv02 = uv0 - uv2;\n vec2 duv12 = uv1 - uv2;\n vec3 dp02 = tri.p0 - tri.p2;\n vec3 dp12 = tri.p1 - tri.p2;\n\n // Method One\n // http://www.pbr-book.org/3ed-2018/Shapes/Triangle_Meshes.html#fragment-Computetrianglepartialderivatives-0\n // Compute tangent vectors relative to the face normal. These vectors won't necessarily be orthogonal to the smoothed normal\n // This means the TBN matrix won't be orthogonal which is technically incorrect.\n // This is Three.js's method (https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/normalmap_pars_fragment.glsl.js)\n // --------------\n // float scale = sign(duv02.x * duv12.y - duv02.y * duv12.x);\n // vec3 dpdu = normalize((duv12.y * dp02 - duv02.y * dp12) * scale);\n // vec3 dpdv = normalize((-duv12.x * dp02 + duv02.x * dp12) * scale);\n\n // Method Two\n // Compute tangent vectors as in Method One but apply Gram-Schmidt process to make vectors orthogonal to smooth normal\n // This might inadvertently flip coordinate space orientation\n // --------------\n // float scale = sign(duv02.x * duv12.y - duv02.y * duv12.x);\n // vec3 dpdu = normalize((duv12.y * dp02 - duv02.y * dp12) * scale);\n // dpdu = (dpdu - dot(dpdu, si.normal) * si.normal); // Gram-Schmidt process\n // vec3 dpdv = cross(si.normal, dpdu) * scale;\n\n // Method Three\n // http://www.thetenthplanet.de/archives/1180\n // Compute co-tangent and co-bitangent vectors\n // These vectors are orthongal and maintain a consistent coordinate space\n // --------------\n vec3 dp12perp = cross(dp12, si.normal);\n vec3 dp02perp = cross(si.normal, dp02);\n vec3 dpdu = dp12perp * duv02.x + dp02perp * duv12.x;\n vec3 dpdv = dp12perp * duv02.y + dp02perp * duv12.y;\n float invmax = inversesqrt(max(dot(dpdu, dpdu), dot(dpdv, dpdv)));\n dpdu *= invmax;\n dpdv *= invmax;\n\n vec3 n = 2.0 * texture(normalMap, vec3(uv * materials.diffuseNormalMapSize[normalMapIndex].zw, normalMapIndex)).rgb - 1.0;\n n.xy *= materials.roughnessMetalnessNormalScale[materialIndex].zw;\n\n mat3 tbn = mat3(dpdu, dpdv, si.normal);\n\n si.normal = normalize(tbn * n);\n }\n #endif\n\n #ifdef NUM_PBR_MAPS\n int roughnessMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].z;\n int metalnessMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].w;\n if (roughnessMapIndex >= 0) {\n si.roughness *= texture(pbrMap, vec3(uv * materials.pbrMapSize[roughnessMapIndex].xy, roughnessMapIndex)).g;\n }\n if (metalnessMapIndex >= 0) {\n si.metalness *= texture(pbrMap, vec3(uv * materials.pbrMapSize[metalnessMapIndex].xy, metalnessMapIndex)).b;\n }\n #endif\n}\n\nstruct TriangleIntersect {\n float t;\n vec3 barycentric;\n};\n\n// Triangle-ray intersection\n// Faster than the classic M\xF6ller\u2013Trumbore intersection algorithm\n// http://www.pbr-book.org/3ed-2018/Shapes/Triangle_Meshes.html#TriangleIntersection\nTriangleIntersect intersectTriangle(Ray r, Triangle tri, int maxDim, vec3 shear) {\n TriangleIntersect ti;\n vec3 d = r.d;\n\n // translate vertices based on ray origin\n vec3 p0t = tri.p0 - r.o;\n vec3 p1t = tri.p1 - r.o;\n vec3 p2t = tri.p2 - r.o;\n\n // permute components of triangle vertices\n if (maxDim == 0) {\n p0t = p0t.yzx;\n p1t = p1t.yzx;\n p2t = p2t.yzx;\n } else if (maxDim == 1) {\n p0t = p0t.zxy;\n p1t = p1t.zxy;\n p2t = p2t.zxy;\n }\n\n // apply shear transformation to translated vertex positions\n p0t.xy += shear.xy * p0t.z;\n p1t.xy += shear.xy * p1t.z;\n p2t.xy += shear.xy * p2t.z;\n\n // compute edge function coefficients\n vec3 e = vec3(\n p1t.x * p2t.y - p1t.y * p2t.x,\n p2t.x * p0t.y - p2t.y * p0t.x,\n p0t.x * p1t.y - p0t.y * p1t.x\n );\n\n // check if intersection is inside triangle\n if (any(lessThan(e, vec3(0))) && any(greaterThan(e, vec3(0)))) {\n return ti;\n }\n\n float det = e.x + e.y + e.z;\n\n // not needed?\n // if (det == 0.) {\n // return ti;\n // }\n\n p0t.z *= shear.z;\n p1t.z *= shear.z;\n p2t.z *= shear.z;\n float tScaled = (e.x * p0t.z + e.y * p1t.z + e.z * p2t.z);\n\n // not needed?\n // if (sign(det) != sign(tScaled)) {\n // return ti;\n // }\n\n // check if closer intersection already exists\n if (abs(tScaled) > abs(r.tMax * det)) {\n return ti;\n }\n\n float invDet = 1. / det;\n ti.t = tScaled * invDet;\n ti.barycentric = e * invDet;\n\n return ti;\n}\n\nstruct Box {\n vec3 min;\n vec3 max;\n};\n\n// Branchless ray/box intersection\n// https://tavianator.com/fast-branchless-raybounding-box-intersections/\nfloat intersectBox(Ray r, Box b) {\n vec3 tBot = (b.min - r.o) * r.invD;\n vec3 tTop = (b.max - r.o) * r.invD;\n vec3 tNear = min(tBot, tTop);\n vec3 tFar = max(tBot, tTop);\n float t0 = max(tNear.x, max(tNear.y, tNear.z));\n float t1 = min(tFar.x, min(tFar.y, tFar.z));\n\n return (t0 > t1 || t0 > r.tMax) ? -1.0 : (t0 > 0.0 ? t0 : t1);\n}\n\nint maxDimension(vec3 v) {\n return v.x > v.y ? (v.x > v.z ? 0 : 2) : (v.y > v.z ? 1 : 2);\n}\n\n// Traverse BVH, find closest triangle intersection, and return surface information\nSurfaceInteraction intersectScene(inout Ray ray) {\n SurfaceInteraction si;\n\n int maxDim = maxDimension(abs(ray.d));\n\n // Permute space so that the z dimension is the one where the absolute value of the ray's direction is largest.\n // Then create a shear transformation that aligns ray direction with the +z axis\n vec3 shear;\n if (maxDim == 0) {\n shear = vec3(-ray.d.y, -ray.d.z, 1.0) * ray.invD.x;\n } else if (maxDim == 1) {\n shear = vec3(-ray.d.z, -ray.d.x, 1.0) * ray.invD.y;\n } else {\n shear = vec3(-ray.d.x, -ray.d.y, 1.0) * ray.invD.z;\n }\n\n int nodesToVisit[STACK_SIZE];\n int stack = 0;\n\n nodesToVisit[0] = 0;\n\n while(stack >= 0) {\n int i = nodesToVisit[stack--];\n\n vec4 r1 = fetchData(bvh, i, BVH_COLUMNS);\n vec4 r2 = fetchData(bvh, i + 1, BVH_COLUMNS);\n\n int splitAxisOrNumPrimitives = floatBitsToInt(r1.w);\n\n if (splitAxisOrNumPrimitives >= 0) {\n // Intersection is a bounding box. Test for box intersection and keep traversing BVH\n int splitAxis = splitAxisOrNumPrimitives;\n\n Box bbox = Box(r1.xyz, r2.xyz);\n\n if (intersectBox(ray, bbox) > 0.0) {\n // traverse near node to ray first, and far node to ray last\n if (ray.d[splitAxis] > 0.0) {\n nodesToVisit[++stack] = floatBitsToInt(r2.w);\n nodesToVisit[++stack] = i + 2;\n } else {\n nodesToVisit[++stack] = i + 2;\n nodesToVisit[++stack] = floatBitsToInt(r2.w);\n }\n }\n } else {\n ivec3 index = floatBitsToInt(r1.xyz);\n Triangle tri = Triangle(\n fetchData(positions, index.x, VERTEX_COLUMNS).xyz,\n fetchData(positions, index.y, VERTEX_COLUMNS).xyz,\n fetchData(positions, index.z, VERTEX_COLUMNS).xyz\n );\n TriangleIntersect hit = intersectTriangle(ray, tri, maxDim, shear);\n\n if (hit.t > 0.0) {\n ray.tMax = hit.t;\n int materialIndex = floatBitsToInt(r2.w);\n vec3 faceNormal = r2.xyz;\n surfaceInteractionFromIntersection(si, tri, hit.barycentric, index, faceNormal, materialIndex);\n }\n }\n }\n\n // Values must be clamped outside of intersection loop. Clamping inside the loop produces incorrect numbers on some devices.\n si.roughness = clamp(si.roughness, 0.03, 1.0);\n si.metalness = clamp(si.metalness, 0.0, 1.0);\n\n return si;\n}\n\nbool intersectSceneShadow(inout Ray ray) {\n int maxDim = maxDimension(abs(ray.d));\n\n // Permute space so that the z dimension is the one where the absolute value of the ray's direction is largest.\n // Then create a shear transformation that aligns ray direction with the +z axis\n vec3 shear;\n if (maxDim == 0) {\n shear = vec3(-ray.d.y, -ray.d.z, 1.0) * ray.invD.x;\n } else if (maxDim == 1) {\n shear = vec3(-ray.d.z, -ray.d.x, 1.0) * ray.invD.y;\n } else {\n shear = vec3(-ray.d.x, -ray.d.y, 1.0) * ray.invD.z;\n }\n\n int nodesToVisit[STACK_SIZE];\n int stack = 0;\n\n nodesToVisit[0] = 0;\n\n while(stack >= 0) {\n int i = nodesToVisit[stack--];\n\n vec4 r1 = fetchData(bvh, i, BVH_COLUMNS);\n vec4 r2 = fetchData(bvh, i + 1, BVH_COLUMNS);\n\n int splitAxisOrNumPrimitives = floatBitsToInt(r1.w);\n\n if (splitAxisOrNumPrimitives >= 0) {\n int splitAxis = splitAxisOrNumPrimitives;\n\n Box bbox = Box(r1.xyz, r2.xyz);\n\n if (intersectBox(ray, bbox) > 0.0) {\n if (ray.d[splitAxis] > 0.0) {\n nodesToVisit[++stack] = floatBitsToInt(r2.w);\n nodesToVisit[++stack] = i + 2;\n } else {\n nodesToVisit[++stack] = i + 2;\n nodesToVisit[++stack] = floatBitsToInt(r2.w);\n }\n }\n } else {\n ivec3 index = floatBitsToInt(r1.xyz);\n Triangle tri = Triangle(\n fetchData(positions, index.x, VERTEX_COLUMNS).xyz,\n fetchData(positions, index.y, VERTEX_COLUMNS).xyz,\n fetchData(positions, index.z, VERTEX_COLUMNS).xyz\n );\n\n if (intersectTriangle(ray, tri, maxDim, shear).t > 0.0) {\n return true;\n }\n }\n }\n\n return false;\n}\n"); } - // Random number generation as described by - // http://www.reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11/ function random (params) { - return "\n\n// higher quality but slower hashing function\nuint wangHash(uint x) {\n x = (x ^ 61u) ^ (x >> 16u);\n x *= 9u;\n x = x ^ (x >> 4u);\n x *= 0x27d4eb2du;\n x = x ^ (x >> 15u);\n return x;\n}\n\n// lower quality but faster hashing function\nuint xorshift(uint x) {\n x ^= x << 13u;\n x ^= x >> 17u;\n x ^= x << 5u;\n return x;\n}\n\n#define STRATA_DIMENSIONS ".concat(params.strataDimensions, "\n\nuniform float seed; // Random number [0, 1)\nuniform float strataStart[STRATA_DIMENSIONS];\nuniform float strataSize;\n\nconst highp float maxUint = 1.0 / 4294967295.0;\nhighp uint randState;\nint strataDimension;\n\n// init state with high quality hashing function to avoid patterns across the 2d image\nvoid initRandom() {\n randState = wangHash(floatBitsToUint(seed));\n randState *= wangHash(floatBitsToUint(vCoord.x));\n randState *= wangHash(floatBitsToUint(vCoord.y));\n randState = wangHash(randState);\n strataDimension = 0;\n}\n\nfloat random() {\n randState = xorshift(randState);\n float f = float(randState) * maxUint;\n\n // transform random number between [0, 1] to (0, 1)\n return EPS + (1.0 - 2.0 * EPS) * f;\n}\n\nvec2 randomVec2() {\n return vec2(random(), random());\n}\n\nfloat randomStrata() {\n return strataStart[strataDimension++] + strataSize * random();\n}\n\nvec2 randomStrataVec2() {\n return vec2(randomStrata(), randomStrata());\n}\n"); + return "\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;\nuniform float useStratifiedSampling;\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\nconst highp float maxUint = 1.0 / 4294967295.0;\n\nfloat pixelSeed;\nhighp uint randState;\n\n// simple integer hashing function\n// https://en.wikipedia.org/wiki/Xorshift\nuint xorshift(uint x) {\n x ^= x << 13u;\n x ^= x >> 17u;\n x ^= x << 5u;\n return x;\n}\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 // white noise used if stratified sampling is disabled\n // produces more balanced path tracing for 1 sample-per-pixel renders\n randState = xorshift(xorshift(floatBitsToUint(vCoord.x)) * xorshift(floatBitsToUint(vCoord.y)));\n}\n\nfloat randomSample() {\n randState = xorshift(randState);\n\n float stratifiedSample = stratifiedSamples[sampleIndex++];\n\n float random = mix(\n float(randState) * maxUint, // white noise\n fract((stratifiedSample + pixelSeed) * strataSize), // blue noise + stratified samples\n useStratifiedSampling\n );\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"; } // Sample the environment map using a cumulative distribution function as described in // http://www.pbr-book.org/3ed-2018/Light_Sources/Infinite_Area_Lights.html function envmap (params) { - return "\n\nuniform sampler2D envmap;\nuniform sampler2D envmapDistribution;\n\nfloat getEnvmapV(float u, out int vOffset, out float pdf) {\n ivec2 size = textureSize(envmap, 0);\n\n int left = 0;\n int right = size.y + 1; // cdf length is the length of the envmap + 1\n while (left < right) {\n int mid = (left + right) >> 1;\n float s = texelFetch(envmapDistribution, ivec2(0, mid), 0).x;\n if (s <= u) {\n left = mid + 1;\n } else {\n right = mid;\n }\n }\n vOffset = left - 1;\n\n vec2 s0 = texelFetch(envmapDistribution, ivec2(0, vOffset), 0).xy;\n vec2 s1 = texelFetch(envmapDistribution, ivec2(0, vOffset + 1), 0).xy;\n\n pdf = s0.y;\n\n return (float(vOffset) + (u - s0.x) / (s1.x - s0.x)) / float(size.y);\n}\n\nfloat getEnvmapU(float u, int vOffset, out float pdf) {\n ivec2 size = textureSize(envmap, 0);\n\n int left = 0;\n int right = size.x + 1; // cdf length is the length of the envmap + 1\n while (left < right) {\n int mid = (left + right) >> 1;\n float s = texelFetch(envmapDistribution, ivec2(1 + mid, vOffset), 0).x;\n if (s <= u) {\n left = mid + 1;\n } else {\n right = mid;\n }\n }\n int uOffset = left - 1;\n\n vec2 s0 = texelFetch(envmapDistribution, ivec2(1 + uOffset, vOffset), 0).xy;\n vec2 s1 = texelFetch(envmapDistribution, ivec2(1 + uOffset + 1, vOffset), 0).xy;\n\n pdf = s0.y;\n\n return (float(uOffset) + (u - s0.x) / (s1.x - s0.x)) / float(size.x);\n}\n\n// Perform two binary searches to find light direction.\nvec3 sampleEnvmap(vec2 random, out vec2 uv, out float pdf) {\n vec2 partialPdf;\n int vOffset;\n\n uv.y = getEnvmapV(random.x, vOffset, partialPdf.y);\n uv.x = getEnvmapU(random.y, vOffset, partialPdf.x);\n\n float phi = uv.x * TWOPI;\n float theta = uv.y * PI;\n float cosTheta = cos(theta);\n float sinTheta = sin(theta);\n float cosPhi = cos(phi);\n float sinPhi = sin(phi);\n\n vec3 dir = vec3(sinTheta * cosPhi, cosTheta, sinTheta * sinPhi);\n\n pdf = partialPdf.x * partialPdf.y * INVPI2 / (2.0 * sinTheta);\n\n return dir;\n}\n\nfloat envmapPdf(vec2 uv) {\n vec2 size = vec2(textureSize(envmap, 0));\n\n float sinTheta = sin(uv.y * PI);\n\n uv *= size;\n\n float partialX = texelFetch(envmapDistribution, ivec2(1.0 + uv.x, uv.y), 0).g;\n float partialY = texelFetch(envmapDistribution, ivec2(0, uv.y), 0).g;\n\n return partialX * partialY * INVPI2 / (2.0 * sinTheta);\n}\n\nvec3 sampleEnvmapFromDirection(vec3 d) {\n float theta = acos(d.y) * INVPI;\n float phi = mod(atan(d.z, d.x), TWOPI) * 0.5 * INVPI;\n\n return textureLinear(envmap, vec2(phi, theta)).rgb;\n}\n\n// debugging function\nvec3 sampleEnvmapDistributionFromDirection(vec3 d) {\n vec2 size = vec2(textureSize(envmap, 0));\n\n float theta = acos(d.y) * INVPI;\n float phi = mod(atan(d.z, d.x), TWOPI) * 0.5 * INVPI;\n\n float u = texelFetch(envmapDistribution, ivec2(1.0 + phi * size.x, theta * size.y), 0).g;\n float v = texelFetch(envmapDistribution, ivec2(0, theta * size.y), 0).g;\n\n return vec3(u * v);\n}\n\n"; + return "\n\nuniform sampler2D envmap;\nuniform sampler2D envmapDistribution;\n\nvec2 cartesianToEquirect(vec3 pointOnSphere) {\n float phi = mod(atan(-pointOnSphere.z, -pointOnSphere.x), TWOPI);\n float theta = acos(pointOnSphere.y);\n return vec2(phi * 0.5 * INVPI, theta * INVPI);\n}\n\nfloat getEnvmapV(float u, out int vOffset, out float pdf) {\n ivec2 size = textureSize(envmap, 0);\n\n int left = 0;\n int right = size.y + 1; // cdf length is the length of the envmap + 1\n while (left < right) {\n int mid = (left + right) >> 1;\n float s = texelFetch(envmapDistribution, ivec2(0, mid), 0).x;\n if (s <= u) {\n left = mid + 1;\n } else {\n right = mid;\n }\n }\n vOffset = left - 1;\n\n vec2 s0 = texelFetch(envmapDistribution, ivec2(0, vOffset), 0).xy;\n vec2 s1 = texelFetch(envmapDistribution, ivec2(0, vOffset + 1), 0).xy;\n\n pdf = s0.y;\n\n return (float(vOffset) + (u - s0.x) / (s1.x - s0.x)) / float(size.y);\n}\n\nfloat getEnvmapU(float u, int vOffset, out float pdf) {\n ivec2 size = textureSize(envmap, 0);\n\n int left = 0;\n int right = size.x + 1; // cdf length is the length of the envmap + 1\n while (left < right) {\n int mid = (left + right) >> 1;\n float s = texelFetch(envmapDistribution, ivec2(1 + mid, vOffset), 0).x;\n if (s <= u) {\n left = mid + 1;\n } else {\n right = mid;\n }\n }\n int uOffset = left - 1;\n\n vec2 s0 = texelFetch(envmapDistribution, ivec2(1 + uOffset, vOffset), 0).xy;\n vec2 s1 = texelFetch(envmapDistribution, ivec2(1 + uOffset + 1, vOffset), 0).xy;\n\n pdf = s0.y;\n\n return (float(uOffset) + (u - s0.x) / (s1.x - s0.x)) / float(size.x);\n}\n\n// Perform two binary searches to find light direction.\nvec3 sampleEnvmap(vec2 random, out vec2 uv, out float pdf) {\n vec2 partialPdf;\n int vOffset;\n\n uv.y = getEnvmapV(random.x, vOffset, partialPdf.y);\n uv.x = getEnvmapU(random.y, vOffset, partialPdf.x);\n\n float phi = uv.x * TWOPI;\n float theta = uv.y * PI;\n float cosTheta = cos(theta);\n float sinTheta = sin(theta);\n float cosPhi = cos(phi);\n float sinPhi = sin(phi);\n\n vec3 dir = vec3(-sinTheta * cosPhi, cosTheta, -sinTheta * sinPhi);\n\n pdf = partialPdf.x * partialPdf.y * INVPI2 / (2.0 * sinTheta);\n\n return dir;\n}\n\nfloat envmapPdf(vec2 uv) {\n vec2 size = vec2(textureSize(envmap, 0));\n\n float sinTheta = sin(uv.y * PI);\n\n uv *= size;\n\n float partialX = texelFetch(envmapDistribution, ivec2(1.0 + uv.x, uv.y), 0).g;\n float partialY = texelFetch(envmapDistribution, ivec2(0, uv.y), 0).g;\n\n return partialX * partialY * INVPI2 / (2.0 * sinTheta);\n}\n\nvec3 sampleEnvmapFromDirection(vec3 d) {\n vec2 uv = cartesianToEquirect(d);\n return textureLinear(envmap, uv).rgb;\n}\n\n// debugging function\nvec3 sampleEnvmapDistributionFromDirection(vec3 d) {\n vec2 size = vec2(textureSize(envmap, 0));\n\n vec2 uv = cartesianToEquirect(d);\n\n float u = texelFetch(envmapDistribution, ivec2(1.0 + uv.x * size.x, uv.y * size.y), 0).g;\n float v = texelFetch(envmapDistribution, ivec2(0, uv.y * size.y), 0).g;\n\n return vec3(u * v);\n}\n\n"; } function bsdf (params) { - return "\n\n// Computes the exact value of the Fresnel factor\n// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/\nfloat fresnel(float cosTheta, float eta, float invEta) {\n eta = cosTheta > 0.0 ? eta : invEta;\n cosTheta = abs(cosTheta);\n\n float gSquared = eta * eta + cosTheta * cosTheta - 1.0;\n\n if (gSquared < 0.0) {\n return 1.0;\n }\n\n float g = sqrt(gSquared);\n\n float a = (g - cosTheta) / (g + cosTheta);\n float b = (cosTheta * (g + cosTheta) - 1.0) / (cosTheta * (g - cosTheta) + 1.0);\n\n return 0.5 * a * a * (1.0 + b * b);\n}\n\nfloat fresnelSchlickWeight(float cosTheta) {\n float w = 1.0 - cosTheta;\n return (w * w) * (w * w) * w;\n}\n\n// Computes Schlick's approximation of the Fresnel factor\n// Assumes ray is moving from a less dense to a more dense medium\nfloat fresnelSchlick(float cosTheta, float r0) {\n return mix(fresnelSchlickWeight(cosTheta), 1.0, r0);\n}\n\n// Computes Schlick's approximation of Fresnel factor\n// Accounts for total internal reflection if ray is moving from a more dense to a less dense medium\nfloat fresnelSchlickTIR(float cosTheta, float r0, float ni) {\n\n // moving from a more dense to a less dense medium\n if (cosTheta < 0.0) {\n float inv_eta = ni;\n float SinT2 = inv_eta * inv_eta * (1.0f - cosTheta * cosTheta);\n if (SinT2 > 1.0) {\n return 1.0; // total internal reflection\n }\n cosTheta = sqrt(1.0f - SinT2);\n }\n\n return mix(fresnelSchlickWeight(cosTheta), 1.0, r0);\n}\n\nfloat trowbridgeReitzD(float cosTheta, float alpha2) {\n float e = cosTheta * cosTheta * (alpha2 - 1.0) + 1.0;\n return alpha2 / (PI * e * e);\n}\n\nfloat trowbridgeReitzLambda(float cosTheta, float alpha2) {\n float cos2Theta = cosTheta * cosTheta;\n float tan2Theta = (1.0 - cos2Theta) / cos2Theta;\n return 0.5 * (-1.0 + sqrt(1.0 + alpha2 * tan2Theta));\n}\n\n// An implementation of Disney's principled BRDF\n// https://disney-animation.s3.amazonaws.com/library/s2012_pbs_disney_brdf_notes_v2.pdf\nvec3 materialBrdf(SurfaceInteraction si, vec3 viewDir, vec3 lightDir, float cosThetaL, float diffuseWeight, out float pdf) {\n vec3 halfVector = normalize(viewDir + lightDir);\n\n cosThetaL = abs(cosThetaL);\n float cosThetaV = abs(dot(si.normal, viewDir));\n float cosThetaH = abs(dot(si.normal, halfVector));\n float cosThetaD = abs(dot(lightDir, halfVector));\n\n float alpha2 = (si.roughness * si.roughness) * (si.roughness * si.roughness);\n\n float F = fresnelSchlick(cosThetaD, mix(R0, 0.6, si.metalness));\n float D = trowbridgeReitzD(cosThetaH, alpha2);\n\n float roughnessRemapped = 0.5 + 0.5 * si.roughness;\n float alpha2Remapped = (roughnessRemapped * roughnessRemapped) * (roughnessRemapped * roughnessRemapped);\n\n float G = 1.0 / (1.0 + trowbridgeReitzLambda(cosThetaV, alpha2Remapped) + trowbridgeReitzLambda(cosThetaL, alpha2Remapped));\n\n float specular = F * D * G / (4.0 * cosThetaV * cosThetaL);\n float specularPdf = D * cosThetaH / (4.0 * cosThetaD);\n\n float f = -0.5 + 2.0 * cosThetaD * cosThetaD * si.roughness;\n float diffuse = diffuseWeight * INVPI * (1.0 + f * fresnelSchlickWeight(cosThetaL)) * (1.0 + f * fresnelSchlickWeight(cosThetaV));\n float diffusePdf = cosThetaL * INVPI;\n\n pdf = mix(0.5 * (specularPdf + diffusePdf), specularPdf, si.metalness);\n\n return mix(si.color * diffuse + mix(si.color, vec3(1.0), F) * specular, si.color * specular, si.metalness);\n}\n\n"; + return "\n\n// Computes the exact value of the Fresnel factor\n// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/\nfloat fresnel(float cosTheta, float eta, float invEta) {\n eta = cosTheta > 0.0 ? eta : invEta;\n cosTheta = abs(cosTheta);\n\n float gSquared = eta * eta + cosTheta * cosTheta - 1.0;\n\n if (gSquared < 0.0) {\n return 1.0;\n }\n\n float g = sqrt(gSquared);\n\n float a = (g - cosTheta) / (g + cosTheta);\n float b = (cosTheta * (g + cosTheta) - 1.0) / (cosTheta * (g - cosTheta) + 1.0);\n\n return 0.5 * a * a * (1.0 + b * b);\n}\n\nfloat fresnelSchlickWeight(float cosTheta) {\n float w = 1.0 - cosTheta;\n return (w * w) * (w * w) * w;\n}\n\n// Computes Schlick's approximation of the Fresnel factor\n// Assumes ray is moving from a less dense to a more dense medium\nfloat fresnelSchlick(float cosTheta, float r0) {\n return mix(fresnelSchlickWeight(cosTheta), 1.0, r0);\n}\n\n// Computes Schlick's approximation of Fresnel factor\n// Accounts for total internal reflection if ray is moving from a more dense to a less dense medium\nfloat fresnelSchlickTIR(float cosTheta, float r0, float ni) {\n\n // moving from a more dense to a less dense medium\n if (cosTheta < 0.0) {\n float inv_eta = ni;\n float SinT2 = inv_eta * inv_eta * (1.0f - cosTheta * cosTheta);\n if (SinT2 > 1.0) {\n return 1.0; // total internal reflection\n }\n cosTheta = sqrt(1.0f - SinT2);\n }\n\n return mix(fresnelSchlickWeight(cosTheta), 1.0, r0);\n}\n\nfloat trowbridgeReitzD(float cosTheta, float alpha2) {\n float e = cosTheta * cosTheta * (alpha2 - 1.0) + 1.0;\n return alpha2 / (PI * e * e);\n}\n\nfloat trowbridgeReitzLambda(float cosTheta, float alpha2) {\n float cos2Theta = cosTheta * cosTheta;\n float tan2Theta = (1.0 - cos2Theta) / cos2Theta;\n return 0.5 * (-1.0 + sqrt(1.0 + alpha2 * tan2Theta));\n}\n\n// An implementation of Disney's principled BRDF\n// https://disney-animation.s3.amazonaws.com/library/s2012_pbs_disney_brdf_notes_v2.pdf\nvec3 materialBrdf(SurfaceInteraction si, vec3 viewDir, vec3 lightDir, float cosThetaL, float diffuseWeight, out float pdf) {\n vec3 halfVector = normalize(viewDir + lightDir);\n\n cosThetaL = abs(cosThetaL);\n float cosThetaV = abs(dot(si.normal, viewDir));\n float cosThetaH = abs(dot(si.normal, halfVector));\n float cosThetaD = abs(dot(lightDir, halfVector));\n\n float alpha2 = (si.roughness * si.roughness) * (si.roughness * si.roughness);\n\n float F = fresnelSchlick(cosThetaD, mix(R0, 0.6, si.metalness));\n float D = trowbridgeReitzD(cosThetaH, alpha2);\n\n float roughnessRemapped = 0.5 + 0.5 * si.roughness;\n float alpha2Remapped = (roughnessRemapped * roughnessRemapped) * (roughnessRemapped * roughnessRemapped);\n\n float G = 1.0 / (1.0 + trowbridgeReitzLambda(cosThetaV, alpha2Remapped) + trowbridgeReitzLambda(cosThetaL, alpha2Remapped));\n\n float specular = F * D * G / (4.0 * cosThetaV * cosThetaL);\n float specularPdf = D * cosThetaH / (4.0 * cosThetaD);\n\n float f = -0.5 + 2.0 * cosThetaD * cosThetaD * si.roughness;\n float diffuse = diffuseWeight * INVPI * (1.0 + f * fresnelSchlickWeight(cosThetaL)) * (1.0 + f * fresnelSchlickWeight(cosThetaV));\n float diffusePdf = cosThetaL * INVPI;\n\n pdf = mix(0.5 * (specularPdf + diffusePdf), specularPdf, si.metalness);\n\n return mix(si.color * diffuse + specular, si.color * specular, si.metalness);\n}\n\n"; } function sample (params) { @@ -494,15 +563,15 @@ // 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 function sampleMaterial (params) { - return "\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 float phi = mod(atan(lightDir.z, lightDir.x), TWOPI);\n float theta = acos(lightDir.y);\n vec2 uv = vec2(0.5 * phi * INVPI, theta * INVPI);\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\nvec3 sampleMaterial(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 beta, inout bool abort) {\n mat3 basis = orthonormalBasis(si.normal);\n vec3 viewDir = -ray.d;\n\n vec2 diffuseOrSpecular = randomStrataVec2();\n\n vec3 lightDir = diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ?\n lightDirDiffuse(si.faceNormal, viewDir, basis, randomStrataVec2()) :\n lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomStrataVec2());\n\n bool lastBounce = bounce == BOUNCES;\n\n // Add path contribution\n vec3 li = beta * (\n importanceSampleLight(si, viewDir, lastBounce, randomStrataVec2()) +\n importanceSampleMaterial(si, viewDir, lastBounce, lightDir)\n );\n\n // Get new path direction\n\n lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ?\n lightDirDiffuse(si.faceNormal, viewDir, basis, randomStrataVec2()) :\n lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomStrataVec2());\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 beta *= abs(cosThetaL) * brdf / scatteringPdf;\n\n initRay(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 abort = orientation < 0.0;\n\n return li;\n}\n\n"; + return "\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\nvec3 sampleMaterial(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 beta, inout bool abort) {\n mat3 basis = orthonormalBasis(si.normal);\n vec3 viewDir = -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 vec3 li = beta * (\n importanceSampleLight(si, viewDir, lastBounce, randomSampleVec2()) +\n importanceSampleMaterial(si, viewDir, lastBounce, lightDir)\n );\n\n // Get new path direction\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 beta *= abs(cosThetaL) * brdf / scatteringPdf;\n\n initRay(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 abort = orientation < 0.0;\n\n return li;\n}\n\n"; } function sampleShadowCatcher (params) { - return "\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 float phi = mod(atan(lightDir.z, lightDir.x), TWOPI);\n float theta = acos(lightDir.y);\n vec2 uv = vec2(0.5 * phi * INVPI, theta * INVPI);\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\nvec3 sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 beta, inout float alpha, inout vec3 prevLi, inout bool abort) {\n mat3 basis = orthonormalBasis(si.normal);\n vec3 viewDir = -ray.d;\n vec3 color = sampleEnvmapFromDirection(-viewDir);\n\n vec3 lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomStrataVec2());\n\n float alphaBounce = 0.0;\n\n // Add path contribution\n vec3 li = beta * color * (\n importanceSampleLightShadowCatcher(si, viewDir, randomStrataVec2(), 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 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 prevLi *= alphaBounce;\n\n // Get new path direction\n\n lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomStrataVec2());\n\n float cosThetaL = dot(si.normal, lightDir);\n\n // lambertian brdf with terms cancelled\n beta *= color;\n\n initRay(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 abort = orientation < 0.0;\n\n // advance strata index by unused stratified samples\n const int usedStrata = 6;\n strataDimension += STRATA_PER_MATERIAL - usedStrata;\n\n return li;\n}\n\n#endif\n"; + return "\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\nvec3 sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 beta, inout float alpha, inout vec3 prevLi, inout bool abort) {\n mat3 basis = orthonormalBasis(si.normal);\n vec3 viewDir = -ray.d;\n vec3 color = sampleEnvmapFromDirection(-viewDir);\n\n vec3 lightDir = lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2());\n\n float alphaBounce = 0.0;\n\n // Add path contribution\n vec3 li = 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 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 prevLi *= alphaBounce;\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 beta *= color;\n\n initRay(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 abort = orientation < 0.0;\n\n // advance dimension index by unused stratified samples\n const int usedDimensions = 6;\n sampleIndex += DIMENSIONS_PER_MATERIAL - usedDimensions;\n\n return li;\n}\n\n#endif\n"; } function sampleGlass (params) { - return "\n\n#ifdef USE_GLASS\n\nvec3 sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 beta) {\n vec3 viewDir = -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 = randomStrata();\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 beta *= si.color;\n }\n\n initRay(ray, si.position + EPS * lightDir, lightDir);\n\n // advance strata index by unused stratified samples\n const int usedStrata = 1;\n strataDimension += STRATA_PER_MATERIAL - usedStrata;\n\n return bounce == BOUNCES ? beta * sampleEnvmapFromDirection(lightDir) : vec3(0.0);\n}\n\n#endif\n\n"; + return "\n\n#ifdef USE_GLASS\n\nvec3 sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Ray ray, inout vec3 beta) {\n vec3 viewDir = -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 beta *= si.color;\n }\n\n initRay(ray, si.position + EPS * lightDir, lightDir);\n\n // advance sample index by unused stratified samples\n const int usedDimensions = 1;\n sampleIndex += DIMENSIONS_PER_MATERIAL - usedDimensions;\n\n return bounce == BOUNCES ? beta * sampleEnvmapFromDirection(lightDir) : vec3(0.0);\n}\n\n#endif\n\n"; } function unrollLoop(indexName, start, limit, step, code) { @@ -515,9 +584,26 @@ return unrolled; } + function addDefines(params) { + var defines = ''; + + for (var _i = 0, _Object$entries = Object.entries(params); _i < _Object$entries.length; _i++) { + var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2), + name = _Object$entries$_i[0], + value = _Object$entries$_i[1]; + + // don't define falsy values such as false, 0, and ''. + // this adds support for #ifdef on falsy values + if (value) { + defines += "#define ".concat(name, " ").concat(value, "\n"); + } + } + + return defines; + } function fragString (params) { - return "#version 300 es\n\nprecision mediump float;\nprecision mediump int;\n\n#define PI 3.14159265359\n#define TWOPI 6.28318530718\n#define INVPI 0.31830988618\n#define INVPI2 0.10132118364\n#define EPS 0.0005\n#define INF 1.0e999\n#define RAY_MAX_DISTANCE 9999.0\n\n#define STANDARD 0\n#define THIN_GLASS 1\n#define THICK_GLASS 2\n#define SHADOW_CATCHER 3\n\n#define STRATA_PER_MATERIAL 8\n\nconst float IOR = 1.5;\nconst float INV_IOR = 1.0 / IOR;\n\nconst float IOR_THIN = 1.015;\nconst float INV_IOR_THIN = 1.0 / IOR_THIN;\n\nconst 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\nconst vec3 luminance = vec3(0.2126, 0.7152, 0.0722);\n\n#define BOUNCES ".concat(params.bounces, "\n").concat(params.useGlass ? '#define USE_GLASS' : '', "\n").concat(params.useShadowCatcher ? '#define USE_SHADOW_CATCHER' : '', "\n\nstruct Ray {\n vec3 o;\n vec3 d;\n vec3 invD;\n float tMax;\n};\n\nstruct 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\nstruct Camera {\n mat4 transform;\n float aspect;\n float fov;\n float focus;\n float aperture;\n};\n\nuniform Camera camera;\nuniform vec2 pixelSize; // 1 / screenResolution\n\nin vec2 vCoord;\n\nout vec4 fragColor;\n\nvoid 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\nivec2 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\nvec4 fetchData(sampler2D s, int i, int columnsLog2) {\n return texelFetch(s, unpackTexel(i, columnsLog2), 0);\n}\n\nivec4 fetchData(isampler2D s, int i, int columnsLog2) {\n return texelFetch(s, unpackTexel(i, columnsLog2), 0);\n}\n\n").concat(textureLinear(params), "\n").concat(intersect(params), "\n").concat(random(params), "\n").concat(envmap(params), "\n").concat(bsdf(params), "\n").concat(sample(params), "\n").concat(sampleMaterial(params), "\n").concat(sampleGlass(params), "\n").concat(sampleShadowCatcher(params), "\n\nstruct Path {\n Ray ray;\n float alpha;\n vec3 beta;\n bool specularBounce;\n bool abort;\n};\n\nvec3 bounce(inout Path path, int i) {\n vec3 li;\n\n if (path.abort) {\n return li;\n }\n\n SurfaceInteraction si = intersectScene(path.ray);\n\n if (!si.hit) {\n if (path.specularBounce) {\n li += path.beta * sampleEnvmapFromDirection(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 li += sampleGlassSpecular(si, i, path.ray, path.beta);\n path.specularBounce = true;\n }\n #endif\n #ifdef USE_SHADOW_CATCHER\n if (si.materialType == SHADOW_CATCHER) {\n li += sampleShadowCatcher(si, i, path.ray, path.beta, path.alpha, li, path.abort);\n path.specularBounce = false;\n }\n #endif\n if (si.materialType == STANDARD) {\n li += sampleMaterial(si, i, path.ray, path.beta, path.abort);\n path.specularBounce = false;\n }\n\n // Russian Roulette sampling\n if (i >= 2) {\n float q = 1.0 - dot(path.beta, luminance);\n if (randomStrata() < q) {\n path.abort = true;\n }\n path.beta /= 1.0 - q;\n }\n }\n\n return li;\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#\nvec4 integrator(inout Ray ray) {\n vec3 li;\n\n Path path;\n path.ray = ray;\n path.alpha = 1.0;\n path.beta = vec3(1.0);\n path.specularBounce = true;\n path.abort = false;\n\n // Manually unroll for loop.\n // Some hardware fails to interate over a GLSL loop, so we provide this workaround\n\n // for (int i = 1; i < params.bounces + 1, i += 1)\n // equivelant to\n ").concat(unrollLoop('i', 1, params.bounces + 1, 1, "\n li += bounce(path, i);\n "), "\n\n return vec4(li, path.alpha);\n}\n\nvoid main() {\n initRandom();\n\n vec2 vCoordAntiAlias = vCoord + pixelSize * (randomStrataVec2() - 0.5);\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(randomStrataVec2());\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 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 fragColor = 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 (strataDimension == STRATA_DIMENSIONS) {\n // fragColor = vec4(1, 1, 1, 1);\n // } else if (strataDimension > STRATA_DIMENSIONS) {\n // fragColor = vec4(1, 0, 0, 1);\n // }\n}\n"); + return "#version 300 es\n\nprecision mediump float;\nprecision mediump int;\n\n".concat(addDefines(params), "\n\n#define PI 3.14159265359\n#define TWOPI 6.28318530718\n#define INVPI 0.31830988618\n#define INVPI2 0.10132118364\n#define EPS 0.0005\n#define INF 1.0e999\n#define RAY_MAX_DISTANCE 9999.0\n\n#define STANDARD 0\n#define THIN_GLASS 1\n#define THICK_GLASS 2\n#define SHADOW_CATCHER 3\n\n#define DIMENSIONS_PER_MATERIAL 8\n\nconst float IOR = 1.5;\nconst float INV_IOR = 1.0 / IOR;\n\nconst float IOR_THIN = 1.015;\nconst float INV_IOR_THIN = 1.0 / IOR_THIN;\n\nconst 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\nconst vec3 luminance = vec3(0.2126, 0.7152, 0.0722);\n\nstruct Ray {\n vec3 o;\n vec3 d;\n vec3 invD;\n float tMax;\n};\n\nstruct 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\nstruct Camera {\n mat4 transform;\n float aspect;\n float fov;\n float focus;\n float aperture;\n};\n\nuniform Camera camera;\nuniform vec2 pixelSize; // 1 / screenResolution\n\nin vec2 vCoord;\n\nout vec4 fragColor;\n\nvoid 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\nivec2 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\nvec4 fetchData(sampler2D s, int i, int columnsLog2) {\n return texelFetch(s, unpackTexel(i, columnsLog2), 0);\n}\n\nivec4 fetchData(isampler2D s, int i, int columnsLog2) {\n return texelFetch(s, unpackTexel(i, columnsLog2), 0);\n}\n\n").concat(textureLinear(), "\n").concat(intersect(params), "\n").concat(random(), "\n").concat(envmap(), "\n").concat(bsdf(), "\n").concat(sample(), "\n").concat(sampleMaterial(), "\n").concat(sampleGlass(), "\n").concat(sampleShadowCatcher(), "\n\nstruct Path {\n Ray ray;\n vec3 li;\n float alpha;\n vec3 beta;\n bool specularBounce;\n bool abort;\n};\n\nvoid bounce(inout Path path, int i) {\n if (path.abort) {\n return;\n }\n\n SurfaceInteraction si = intersectScene(path.ray);\n\n if (!si.hit) {\n if (path.specularBounce) {\n path.li += path.beta * sampleEnvmapFromDirection(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 path.li += sampleGlassSpecular(si, i, path.ray, path.beta);\n path.specularBounce = true;\n }\n #endif\n #ifdef USE_SHADOW_CATCHER\n if (si.materialType == SHADOW_CATCHER) {\n path.li += sampleShadowCatcher(si, i, path.ray, path.beta, path.alpha, path.li, path.abort);\n path.specularBounce = false;\n }\n #endif\n if (si.materialType == STANDARD) {\n path.li += sampleMaterial(si, i, path.ray, path.beta, path.abort);\n path.specularBounce = false;\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#\nvec4 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 // Manually unroll for loop.\n // Some hardware fails to interate over a GLSL loop, so we provide this workaround\n\n // for (int i = 1; i < params.bounces + 1, i += 1)\n // equivelant to\n ").concat(unrollLoop('i', 1, params.BOUNCES + 1, 1, "\n bounce(path, i);\n "), "\n\n return vec4(path.li, path.alpha);\n}\n\nvoid main() {\n initRandom();\n\n vec2 vCoordAntiAlias = vCoord + pixelSize * (randomSampleVec2() - 0.5);\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 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 fragColor = 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"); } function addFlatGeometryIndices(geometry) { @@ -581,8 +667,8 @@ _iteratorError = err; } finally { try { - if (!_iteratorNormalCompletion && _iterator.return != null) { - _iterator.return(); + if (!_iteratorNormalCompletion && _iterator["return"] != null) { + _iterator["return"](); } } finally { if (_didIteratorError) { @@ -644,8 +730,8 @@ _iteratorError2 = err; } finally { try { - if (!_iteratorNormalCompletion2 && _iterator2.return != null) { - _iterator2.return(); + if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { + _iterator2["return"](); } } finally { if (_didIteratorError2) { @@ -669,7 +755,9 @@ var x = array[b]; array[b] = array[a]; array[a] = x; - } // https://en.cppreference.com/w/cpp/algorithm/partition + } // Reorders the elements in the range [first, last) in such a way that + // all elements for which the comparator c returns true + // precede the elements for which comparator c returns false. function partition(array, compare) { @@ -698,7 +786,9 @@ } return left; - } // https://en.cppreference.com/w/cpp/algorithm/nth_element + } // nth_element is a partial sorting algorithm that rearranges elements in [first, last) such that: + // The element pointed at by nth is changed to whatever element would occur in that position if [first, last) were sorted. + // All of the elements before this new nth element compare to true with elements after the nth element function nthElement(array, compare) { var left = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; @@ -1075,48 +1165,55 @@ return true; } - function generateEnvMapFromSceneComponents(background, directionalLights) { - var envImage; + var DEFAULT_MAP_RESOLUTION = { + width: 4096, + height: 2048 + }; // Tools for generating and modify env maps for lighting from scene component data - if (background && background.encoding === THREE.LinearEncoding) { - // background is an HDR image - var image = background.image; + function generateEnvMapFromSceneComponents(directionalLights, environmentLights) { + var envImage = initializeEnvMap(environmentLights); + directionalLights.forEach(function (light) { + envImage.data = addDirectionalLightToEnvMap(light, envImage); + }); + return envImage; + } + function initializeEnvMap(environmentLights) { + var envImage; // Initialize map from environment light if present + + if (environmentLights.length > 0) { + // TODO: support multiple environment lights (what if they have different resolutions?) + var environmentLight = environmentLights[0]; envImage = { - width: image.width, - height: image.height, - data: image.data + width: environmentLight.map.image.width, + height: environmentLight.map.image.height, + data: environmentLight.map.image.data }; envImage.data = rgbeToFloat(envImage.data); - directionalLights.forEach(function (light) { - envImage.data = addDirectionalLightToEnvMap(light, envImage); + envImage.data.forEach(function (datum, index, arr) { + arr[index] = datum * environmentLight.intensity; }); } else { - // background is a single color - var color = background; - - if (!(color instanceof THREE.Color)) { - if (color) { - // color is defined and set to something other than THREE.Color - console.warn('scene.background should be an HDR image or a THREE.Color'); - } - - color = new THREE.Color(0xffffff); - } - - envImage = { - width: 1, - height: 1, - data: new Float32Array(color.toArray()) - }; + // initialize blank map + envImage = generateBlankMap(DEFAULT_MAP_RESOLUTION.width, DEFAULT_MAP_RESOLUTION.height); } return envImage; } + function generateBlankMap(width, height) { + var texels = width * height; + var floatBuffer = new Float32Array(texels * 3); + floatBuffer.fill(0.0); + return { + width: width, + height: height, + data: floatBuffer + }; + } function addDirectionalLightToEnvMap(light, image) { var sphericalCoords = new THREE$1.Spherical(); var lightDirection = light.position.clone().sub(light.target.position); sphericalCoords.setFromVector3(lightDirection); - sphericalCoords.theta = Math.PI / 2 - sphericalCoords.theta; + sphericalCoords.theta = Math.PI * 3 / 2 - sphericalCoords.theta; sphericalCoords.makeSafe(); return addLightAtCoordinates(light, image, sphericalCoords); } // Perform modifications on env map to match input scene @@ -1125,7 +1222,6 @@ var floatBuffer = image.data; var width = image.width; var height = image.height; - var texels = floatBuffer.length / 3; var xTexels = floatBuffer.length / (3 * height); var yTexels = floatBuffer.length / (3 * width); // default softness for standard directional lights is 0.95 @@ -1153,9 +1249,9 @@ } function angleBetweenSphericals(originCoords, currentCoords) { - var originVector = new THREE.Vector3(); + var originVector = new THREE$1.Vector3(); originVector.setFromSpherical(originCoords); - var currentVector = new THREE.Vector3(); + var currentVector = new THREE$1.Vector3(); currentVector.setFromSpherical(currentCoords); return originVector.angleTo(currentVector); } @@ -1174,122 +1270,6 @@ return sphericalCoords; } - // Stratified Sampling - function makeStratifiedRandom(strataCount, dimensions) { - var samples = []; - var l = Math.pow(strataCount, dimensions); - - for (var i = 0; i < l; i++) { - samples[i] = i; - } - - var index = samples.length; - var randomNums = []; - - function reset() { - index = 0; - shuffle(samples); - } - - function next() { - if (index >= samples.length) { - reset(); - } - - var sample = samples[index++]; - - for (var _i = 0; _i < dimensions; _i++) { - randomNums[_i] = sample % strataCount / strataCount; - sample = Math.floor(sample / strataCount); - } - - return randomNums; - } - - return Object.freeze({ - reset: reset, - next: next, - strataCount: strataCount - }); - } - - // Stratified Sampling - function makeStratifiedRandomCombined(strataCount, listOfDimensions) { - var strataObjs = []; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = listOfDimensions[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var dim = _step.value; - strataObjs.push(makeStratifiedRandom(strataCount, dim)); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return != null) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - var randomNums = []; - - function reset() { - for (var _i = 0; _i < strataObjs.length; _i++) { - var strata = strataObjs[_i]; - strata.reset(); - } - } - - function next() { - var i = 0; - - for (var _i2 = 0; _i2 < strataObjs.length; _i2++) { - var strata = strataObjs[_i2]; - var nums = strata.next(); - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = nums[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var num = _step2.value; - randomNums[i++] = num; - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return != null) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - } - - return randomNums; - } - - return Object.freeze({ - next: next, - reset: reset, - strataCount: strataCount - }); - } - function texturesFromMaterials(materials, textureName, textures) { var indices = []; var _iteratorNormalCompletion = true; @@ -1326,8 +1306,8 @@ _iteratorError = err; } finally { try { - if (!_iteratorNormalCompletion && _iterator.return != null) { - _iterator.return(); + if (!_iteratorNormalCompletion && _iterator["return"] != null) { + _iterator["return"](); } } finally { if (_didIteratorError) { @@ -1360,8 +1340,8 @@ _iteratorError2 = err; } finally { try { - if (!_iteratorNormalCompletion2 && _iterator2.return != null) { - _iterator2.return(); + if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { + _iterator2["return"](); } } finally { if (_didIteratorError2) { @@ -1392,8 +1372,8 @@ _iteratorError3 = err; } finally { try { - if (!_iteratorNormalCompletion3 && _iterator3.return != null) { - _iterator3.return(); + if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) { + _iterator3["return"](); } } finally { if (_didIteratorError3) { @@ -1425,7 +1405,9 @@ _params$storage = params.storage, storage = _params$storage === void 0 ? null : _params$storage, _params$data = params.data, - data = _params$data === void 0 ? null : _params$data; + data = _params$data === void 0 ? null : _params$data, + _params$flipY = params.flipY, + flipY = _params$flipY === void 0 ? false : _params$flipY; width = width || data.width || 0; height = height || data.height || 0; var texture = gl.createTexture(); @@ -1452,7 +1434,7 @@ } channels = clamp(channels, 1, 4); - var format = [gl.R, gl.RG, gl.RGB, gl.RGBA][channels - 1]; + var format = [gl.RED, gl.RG, gl.RGB, gl.RGBA][channels - 1]; var isByteArray = storage === 'byte' || data instanceof Uint8Array || data instanceof HTMLImageElement || data instanceof HTMLCanvasElement || data instanceof ImageData; var isFloatArray = storage === 'float' || data instanceof Float32Array; var type; @@ -1476,12 +1458,16 @@ // otherwise use the max size of the array texture var layerWidth = dataArray[i].width || width; var layerHeight = dataArray[i].height || height; + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, Array.isArray(flipY) ? flipY[i] : flipY); gl.texSubImage3D(target, 0, 0, 0, i, layerWidth, layerHeight, 1, format, type, dataArray[i]); } } else { + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY); gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, data); - } + } // return state to default + + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); return Object.freeze({ target: target, texture: texture @@ -1586,40 +1572,90 @@ materialBuffer.bind(0); } - function textureDimensionsFromArray(count) { - var columnsLog = Math.round(Math.log2(Math.sqrt(count))); - var columns = Math.pow(2, columnsLog); - var rows = Math.ceil(count / columns); - return { - columnsLog: columnsLog, - columns: columns, - rows: rows, - size: rows * columns - }; + /* + 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. + */ + function makeStratifiedSampler(strataCount, dimensions) { + var strata = []; + var l = Math.pow(strataCount, dimensions); + + for (var i = 0; i < l; i++) { + strata[i] = i; + } + + var index = strata.length; + var sample = []; + + function restart() { + index = 0; + } + + function next() { + if (index >= strata.length) { + shuffle(strata); + restart(); + } + + var stratum = strata[index++]; + + for (var _i = 0; _i < dimensions; _i++) { + sample[_i] = stratum % strataCount + Math.random(); + stratum = Math.floor(stratum / strataCount); + } + + return sample; + } + + return Object.freeze({ + next: next, + restart: restart, + strataCount: strataCount + }); } - function maxImageSize(images) { - var maxSize = { - width: 0, - height: 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 many benefits of stratification while still allowing for small strata sizes. + */ + function makeStratifiedSamplerCombined(strataCount, listOfDimensions) { + var strataObjs = []; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { - for (var _iterator = images[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var image = _step.value; - maxSize.width = Math.max(maxSize.width, image.width); - maxSize.height = Math.max(maxSize.height, image.height); + for (var _iterator = listOfDimensions[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var dim = _step.value; + strataObjs.push(makeStratifiedSampler(strataCount, dim)); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { - if (!_iteratorNormalCompletion && _iterator.return != null) { - _iterator.return(); + if (!_iteratorNormalCompletion && _iterator["return"] != null) { + _iterator["return"](); } } finally { if (_didIteratorError) { @@ -1628,91 +1664,84 @@ } } - var relativeSizes = []; - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = images[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var _image = _step2.value; - relativeSizes.push(_image.width / maxSize.width); - relativeSizes.push(_image.height / maxSize.height); - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return != null) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - - return { - maxSize: maxSize, - relativeSizes: relativeSizes - }; - } // expand array to the given length + var combined = []; + function next() { + var i = 0; - function padArray(typedArray, length) { - var newArray = new typedArray.constructor(length); - newArray.set(typedArray); - return newArray; - } + for (var _i = 0, _strataObjs = strataObjs; _i < _strataObjs.length; _i++) { + var strata = _strataObjs[_i]; + var nums = strata.next(); + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; - function decomposeScene(scene) { - var meshes = []; - var directionalLights = []; - scene.traverse(function (child) { - if (child instanceof THREE$1.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$1.MeshStandardMaterial)) { - console.log(child, 'must use MeshStandardMaterial in order to be rendered.'); - } else { - meshes.push(child); + try { + for (var _iterator2 = nums[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var num = _step2.value; + combined[i++] = num; + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { + _iterator2["return"](); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } } } - if (child instanceof THREE$1.DirectionalLight) { - directionalLights.push(child); + return combined; + } + + function restart() { + for (var _i2 = 0, _strataObjs2 = strataObjs; _i2 < _strataObjs2.length; _i2++) { + var strata = _strataObjs2[_i2]; + strata.restart(); } + } + + return Object.freeze({ + next: next, + restart: restart, + strataCount: strataCount }); - return { - meshes: meshes, - directionalLights: directionalLights - }; } - function makeRayTracingShader(gl, optionalExtensions, fullscreenQuad, textureAllocator, scene) { - var OES_texture_float_linear = optionalExtensions.OES_texture_float_linear; // Number of ray bounces per sample - - var bounces = 3; // 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 - - var strataDimensions = []; - strataDimensions.push(2, 2); // anti-aliasing, depth-of-field + function makeRayTracingShader(_ref) { + var gl = _ref.gl, + optionalExtensions = _ref.optionalExtensions, + fullscreenQuad = _ref.fullscreenQuad, + textureAllocator = _ref.textureAllocator, + scene = _ref.scene, + bounces = _ref.bounces; + bounces = clamp(bounces, 1, 6); + var OES_texture_float_linear = optionalExtensions.OES_texture_float_linear; + var samplingDimensions = []; + samplingDimensions.push(2, 2); // anti aliasing, depth of field for (var 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) { - strataDimensions.push(1); // russian roulette sampling + // russian roulette sampling + // this step is skipped on the first bounce + samplingDimensions.push(1); } } function initScene() { var _decomposeScene = decomposeScene(scene), meshes = _decomposeScene.meshes, - directionalLights = _decomposeScene.directionalLights; + directionalLights = _decomposeScene.directionalLights, + environmentLights = _decomposeScene.environmentLights; if (meshes.length === 0) { throw 'RayTracingRenderer: Scene contains no renderable meshes.'; @@ -1743,19 +1772,19 @@ }); var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragString({ OES_texture_float_linear: OES_texture_float_linear, - bvhColumnsLog: bvhDim.columnsLog, - indexColumnsLog: indexDim.columnsLog, - vertexColumnsLog: vertexDim.columnsLog, - maxBvhDepth: flattenedBvh.maxDepth, - numTris: numTris, - numMaterials: materials.length, - numDiffuseMaps: maps.map.textures.length, - numNormalMaps: maps.normalMap.textures.length, - numPbrMaps: pbrMap.textures.length, - bounces: bounces, - useGlass: useGlass, - useShadowCatcher: useShadowCatcher, - strataDimensions: strataDimensions.reduce(function (a, b) { + BVH_COLUMNS: bvhDim.columnsLog, + INDEX_COLUMNS: indexDim.columnsLog, + VERTEX_COLUMNS: vertexDim.columnsLog, + STACK_SIZE: flattenedBvh.maxDepth, + NUM_TRIS: numTris, + NUM_MATERIALS: materials.length, + NUM_DIFFUSE_MAPS: maps.map.textures.length, + NUM_NORMAL_MAPS: maps.normalMap.textures.length, + NUM_PBR_MAPS: pbrMap.textures.length, + BOUNCES: bounces, + USE_GLASS: useGlass, + USE_SHADOW_CATCHER: useShadowCatcher, + SAMPLING_DIMENSIONS: samplingDimensions.reduce(function (a, b) { return a + b; }) })); @@ -1789,6 +1818,9 @@ var images = maps.map.textures.map(function (t) { return t.image; }); + var flipY = maps.map.textures.map(function (t) { + return t.flipY; + }); var _maxImageSize = maxImageSize(images), maxSize = _maxImageSize.maxSize, @@ -1800,7 +1832,8 @@ height: maxSize.height, channels: 3, gammaCorrection: true, - data: images + data: images, + flipY: flipY })); bufferData.diffuseMapSize = relativeSizes; bufferData.diffuseMapIndex = maps.map.indices; @@ -1811,6 +1844,10 @@ return t.image; }); + var _flipY = maps.normalMap.textures.map(function (t) { + return t.flipY; + }); + var _maxImageSize2 = maxImageSize(_images), _maxSize = _maxImageSize2.maxSize, _relativeSizes = _maxImageSize2.relativeSizes; // create GL Array Texture from individual textures @@ -1820,7 +1857,8 @@ width: _maxSize.width, height: _maxSize.height, channels: 3, - data: _images + data: _images, + flipY: _flipY })); bufferData.normalMapSize = _relativeSizes; bufferData.normalMapIndex = maps.normalMap.indices; @@ -1831,6 +1869,10 @@ return t.image; }); + var _flipY2 = pbrMap.textures.map(function (t) { + return t.flipY; + }); + var _maxImageSize3 = maxImageSize(_images2), _maxSize2 = _maxImageSize3.maxSize, _relativeSizes2 = _maxImageSize3.relativeSizes; // create GL Array Texture from individual textures @@ -1840,7 +1882,8 @@ width: _maxSize2.width, height: _maxSize2.height, channels: 3, - data: _images2 + data: _images2, + flipY: _flipY2 })); bufferData.pbrMapSize = _relativeSizes2; bufferData.roughnessMapIndex = pbrMap.indices.roughnessMap; @@ -1876,8 +1919,7 @@ width: bvhDim.columns, height: bvhDim.rows })); - var background = scene.background; - var envImage = generateEnvMapFromSceneComponents(background, directionalLights); + var envImage = generateEnvMapFromSceneComponents(directionalLights, environmentLights); textureAllocator.bind(uniforms.envmap, makeTexture(gl, { data: envImage.data, minFilter: OES_texture_float_linear ? gl.LINEAR : gl.NEAREST, @@ -1903,11 +1945,19 @@ program = _initScene.program, uniforms = _initScene.uniforms; - var 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) { @@ -1919,15 +1969,32 @@ gl.uniform1f(uniforms['camera.aperture'], camera.aperture || 0); } - function setStrataCount(count) { - random = makeStratifiedRandomCombined(count, strataDimensions); + var samples; + + function nextSeed() { + gl.useProgram(program); + gl.uniform1fv(uniforms['stratifiedSamples[0]'], samples.next()); } - function updateSeed() { + 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 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() { @@ -1935,26 +2002,155 @@ fullscreenQuad.draw(); } + samples = makeStratifiedSamplerCombined(1, samplingDimensions); return Object.freeze({ - setSize: setSize, + draw: draw, + nextSeed: nextSeed, setCamera: setCamera, + setNoise: setNoise, + setSize: setSize, setStrataCount: setStrataCount, - updateSeed: updateSeed, - draw: draw + useStratifiedSampling: useStratifiedSampling + }); + } + + function textureDimensionsFromArray(count) { + var columnsLog = Math.round(Math.log2(Math.sqrt(count))); + var columns = Math.pow(2, columnsLog); + var rows = Math.ceil(count / columns); + return { + columnsLog: columnsLog, + columns: columns, + rows: rows, + size: rows * columns + }; + } + + function maxImageSize(images) { + var maxSize = { + width: 0, + height: 0 + }; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = images[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var image = _step.value; + maxSize.width = Math.max(maxSize.width, image.width); + maxSize.height = Math.max(maxSize.height, image.height); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator["return"] != null) { + _iterator["return"](); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + var relativeSizes = []; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = images[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _image = _step2.value; + relativeSizes.push(_image.width / maxSize.width); + relativeSizes.push(_image.height / maxSize.height); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { + _iterator2["return"](); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + return { + maxSize: maxSize, + relativeSizes: relativeSizes + }; + } // expand array to the given length + + + function padArray(typedArray, length) { + var newArray = new typedArray.constructor(length); + newArray.set(typedArray); + return newArray; + } + + function isHDRTexture(texture) { + return texture.map && texture.map.image && (texture.map.encoding === THREE$1.RGBEEncoding || texture.map.encoding === THREE$1.LinearEncoding); + } + + function decomposeScene(scene) { + var meshes = []; + var directionalLights = []; + var environmentLights = []; + scene.traverse(function (child) { + if (child instanceof THREE$1.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$1.MeshStandardMaterial)) { + console.warn(child, 'must use MeshStandardMaterial in order to be rendered.'); + } else { + meshes.push(child); + } + } + + if (child instanceof THREE$1.DirectionalLight) { + directionalLights.push(child); + } + + if (child instanceof THREE$1.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: meshes, + directionalLights: directionalLights, + environmentLights: environmentLights + }; } function fragString$1 (params) { - return "#version 300 es\n\nprecision mediump float;\nprecision mediump int;\n\nin vec2 vCoord;\n\nout vec4 fragColor;\n\nuniform sampler2D image;\n\n".concat(textureLinear(params), "\n\n// Tonemapping functions from THREE.js\n\nvec3 linear(vec3 color) {\n return color;\n}\n// https://www.cs.utah.edu/~reinhard/cdrom/\nvec3 reinhard(vec3 color) {\n return clamp(color / (vec3(1.0 ) + color), vec3(0.0), vec3(1.0));\n}\n// http://filmicworlds.com/blog/filmic-tonemapping-operators/\n#define uncharted2Helper(x) max(((x * (0.15 * x + 0.10 * 0.50) + 0.20 * 0.02) / (x * (0.15 * x + 0.50) + 0.20 * 0.30)) - 0.02 / 0.30, vec3(0.0))\nconst vec3 uncharted2WhitePoint = 1.0 / uncharted2Helper(vec3(").concat(params.whitePoint, "));\nvec3 uncharted2( vec3 color ) {\n // John Hable's filmic operator from Uncharted 2 video game\n return clamp(uncharted2Helper(color) * uncharted2WhitePoint, vec3(0.0), vec3(1.0));\n}\n// http://filmicworlds.com/blog/filmic-tonemapping-operators/\nvec3 cineon( vec3 color ) {\n // optimized filmic operator by Jim Hejl and Richard Burgess-Dawson\n color = max(vec3( 0.0 ), color - 0.004);\n return pow((color * (6.2 * color + 0.5)) / (color * (6.2 * color + 1.7) + 0.06), vec3(2.2));\n}\n// https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/\nvec3 acesFilmic( vec3 color ) {\n return clamp((color * (2.51 * color + 0.03)) / (color * (2.43 * color + 0.59) + 0.14), vec3(0.0), vec3(1.0));\n}\n\nvoid main() {\n vec4 tex = textureLinear(image, vCoord);\n\n vec3 light = tex.rgb / tex.a;\n // alpha channel stores the number of samples progressively rendered\n // divide the sum of light by alpha to obtain average contribution of light\n\n // in addition, alpha contains a scale factor for the shadow catcher material\n // dividing by alpha normalizes the brightness of the shadow catcher to match the background envmap.\n\n light *= ").concat(params.exposure, "; // exposure\n\n light = ").concat(params.toneMapping, "(light); // tone mapping\n\n light = pow(light, vec3(1.0 / 2.2)); // gamma correction\n\n fragColor = vec4(light, 1.0);\n}\n\n"); + return "#version 300 es\n\nprecision mediump float;\nprecision mediump int;\n\nin vec2 vCoord;\n\nout vec4 fragColor;\n\nuniform sampler2D image;\n\n".concat(textureLinear(), "\n\n// Tonemapping functions from THREE.js\n\nvec3 linear(vec3 color) {\n return color;\n}\n// https://www.cs.utah.edu/~reinhard/cdrom/\nvec3 reinhard(vec3 color) {\n return clamp(color / (vec3(1.0 ) + color), vec3(0.0), vec3(1.0));\n}\n// http://filmicworlds.com/blog/filmic-tonemapping-operators/\n#define uncharted2Helper(x) max(((x * (0.15 * x + 0.10 * 0.50) + 0.20 * 0.02) / (x * (0.15 * x + 0.50) + 0.20 * 0.30)) - 0.02 / 0.30, vec3(0.0))\nconst vec3 uncharted2WhitePoint = 1.0 / uncharted2Helper(vec3(").concat(params.whitePoint, "));\nvec3 uncharted2( vec3 color ) {\n // John Hable's filmic operator from Uncharted 2 video game\n return clamp(uncharted2Helper(color) * uncharted2WhitePoint, vec3(0.0), vec3(1.0));\n}\n// http://filmicworlds.com/blog/filmic-tonemapping-operators/\nvec3 cineon( vec3 color ) {\n // optimized filmic operator by Jim Hejl and Richard Burgess-Dawson\n color = max(vec3( 0.0 ), color - 0.004);\n return pow((color * (6.2 * color + 0.5)) / (color * (6.2 * color + 1.7) + 0.06), vec3(2.2));\n}\n// https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/\nvec3 acesFilmic( vec3 color ) {\n return clamp((color * (2.51 * color + 0.03)) / (color * (2.43 * color + 0.59) + 0.14), vec3(0.0), vec3(1.0));\n}\n\nvoid main() {\n vec4 tex = textureLinear(image, vCoord);\n\n vec3 light = tex.rgb / tex.a;\n // alpha channel stores the number of samples progressively rendered\n // divide the sum of light by alpha to obtain average contribution of light\n\n // in addition, alpha contains a scale factor for the shadow catcher material\n // dividing by alpha normalizes the brightness of the shadow catcher to match the background envmap.\n\n light *= ").concat(params.exposure, "; // exposure\n\n light = ").concat(params.toneMapping, "(light); // tone mapping\n\n light = pow(light, vec3(1.0 / 2.2)); // gamma correction\n\n fragColor = vec4(light, 1.0);\n}\n\n"); } var _toneMapFunctions; var toneMapFunctions = (_toneMapFunctions = {}, _defineProperty(_toneMapFunctions, THREE$1.LinearToneMapping, 'linear'), _defineProperty(_toneMapFunctions, THREE$1.ReinhardToneMapping, 'reinhard'), _defineProperty(_toneMapFunctions, THREE$1.Uncharted2ToneMapping, 'uncharted2'), _defineProperty(_toneMapFunctions, THREE$1.CineonToneMapping, 'cineon'), _defineProperty(_toneMapFunctions, THREE$1.ACESFilmicToneMapping, 'acesFilmic'), _toneMapFunctions); - function makeToneMapShader(gl, optionalExtensions, fullscreenQuad, textureAllocator, toneMapParams) { + function makeToneMapShader(_ref) { + var gl = _ref.gl, + optionalExtensions = _ref.optionalExtensions, + fullscreenQuad = _ref.fullscreenQuad, + textureAllocator = _ref.textureAllocator, + toneMappingParams = _ref.toneMappingParams; var OES_texture_float_linear = optionalExtensions.OES_texture_float_linear; - var toneMapping = toneMapParams.toneMapping, - whitePoint = toneMapParams.whitePoint, - exposure = toneMapParams.exposure; + var toneMapping = toneMappingParams.toneMapping, + whitePoint = toneMappingParams.whitePoint, + exposure = toneMappingParams.exposure; var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragString$1({ OES_texture_float_linear: OES_texture_float_linear, toneMapping: toneMapFunctions[toneMapping] || 'linear', @@ -1966,8 +2162,8 @@ var uniforms = getUniforms(gl, program); var bindFramebuffer = textureAllocator.reserveSlot(); - function draw(_ref) { - var texture = _ref.texture; + function draw(_ref2) { + var texture = _ref2.texture; gl.useProgram(program); bindFramebuffer(uniforms.image, texture); fullscreenQuad.draw(); @@ -2206,11 +2402,40 @@ }); } - function makeSceneSampler(gl, optionalExtensions, scene, toneMappingParams) { + var noiseBase64 = 'data:image/png;base64,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'; + + function makeRenderingPipeline(_ref) { + var gl = _ref.gl, + optionalExtensions = _ref.optionalExtensions, + scene = _ref.scene, + toneMappingParams = _ref.toneMappingParams, + bounces = _ref.bounces; + var ready = false; var fullscreenQuad = makeFullscreenQuad(gl); var textureAllocator = makeTextureAllocator(gl); - var rayTracingShader = makeRayTracingShader(gl, optionalExtensions, fullscreenQuad, textureAllocator, scene); - var toneMapShader = makeToneMapShader(gl, optionalExtensions, fullscreenQuad, textureAllocator, toneMappingParams); + var rayTracingShader = makeRayTracingShader({ + gl: gl, + optionalExtensions: optionalExtensions, + fullscreenQuad: fullscreenQuad, + textureAllocator: textureAllocator, + scene: scene, + bounces: bounces + }); + var toneMapShader = makeToneMapShader({ + gl: gl, + optionalExtensions: optionalExtensions, + fullscreenQuad: fullscreenQuad, + textureAllocator: textureAllocator, + toneMappingParams: toneMappingParams + }); + var noiseImage = new Image(); + noiseImage.src = noiseBase64; + + noiseImage.onload = function () { + rayTracingShader.setNoise(noiseImage); + ready = true; + }; + var useLinearFiltering = optionalExtensions.OES_texture_float_linear; var hdrBuffer = makeRenderTargetFloat(gl); // full resolution buffer representing the rendered scene with HDR lighting @@ -2218,11 +2443,12 @@ // used to sample only a portion of the scene to the HDR Buffer to prevent the GPU from locking up from excessive computation var tileRender = makeTileRender(gl); - var lastCamera = new LensCamera(); // how many samples to render with simple noise before switching to stratified noise + var lastCamera = new LensCamera(); // how many samples to render with uniform noise before switching to stratified noise - var numSimpleSamples = 4; // how many partitions of stratified noise should be created + var numUniformSamples = 6; // how many partitions of stratified noise should be created + // higher number results in faster convergence over time, but with lower quality initial samples - var strataSize = 6; + var strataCount = 6; var sampleCount = 0; var sampleRenderedCallback = function sampleRenderedCallback() {}; @@ -2233,13 +2459,12 @@ hdrBuffer.unbind(); sampleCount = 0; tileRender.reset(); - rayTracingShader.setStrataCount(1); - rayTracingShader.updateSeed(); } function initFirstSample(camera) { lastCamera.copy(camera); rayTracingShader.setCamera(camera); + rayTracingShader.useStratifiedSampling(false); clear(); } @@ -2300,8 +2525,21 @@ }); } + 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(); @@ -2316,11 +2554,7 @@ if (isFirstTile) { sampleCount++; - rayTracingShader.updateSeed(); - - if (sampleCount === numSimpleSamples) { - rayTracingShader.setStrataCount(strataSize); - } + updateSeed(); } renderTile(x, y, tileWidth, tileHeight); @@ -2333,7 +2567,9 @@ } function drawOffscreenTile(camera) { - if (!camerasEqual(camera, lastCamera)) { + if (!ready) { + return; + } else if (!camerasEqual(camera, lastCamera)) { initFirstSample(camera); } @@ -2347,11 +2583,7 @@ if (isFirstTile) { sampleCount++; - rayTracingShader.updateSeed(); - - if (sampleCount === numSimpleSamples) { - rayTracingShader.setStrataCount(strataSize); - } + updateSeed(); } renderTile(x, y, tileWidth, tileHeight); @@ -2362,16 +2594,14 @@ } 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(); } @@ -2423,20 +2653,21 @@ loadExtensions(gl, glRequiredExtensions); var optionalExtensions = loadExtensions(gl, glOptionalExtensions); // private properties - var sceneSampler = null; + var pipeline = null; var size = new THREE$1.Vector2(); var renderTime = 22; var pixelRatio = 1; - var lastFocus = false; var module = { + bounces: 3, domElement: canvas, + maxHardwareUsage: false, needsUpdate: true, onSampleRendered: null, renderWhenOffFocus: true, renderToScreen: true, + toneMapping: THREE$1.LinearToneMapping, toneMappingExposure: 1, - toneMappingWhitePoint: 1, - toneMapping: THREE$1.LinearToneMapping + toneMappingWhitePoint: 1 }; function initScene(scene) { @@ -2446,9 +2677,16 @@ whitePoint: module.toneMappingWhitePoint, toneMapping: module.toneMapping }; - sceneSampler = makeSceneSampler(gl, optionalExtensions, scene, toneMappingParams); + var bounces = module.bounces; + pipeline = makeRenderingPipeline({ + gl: gl, + optionalExtensions: optionalExtensions, + scene: scene, + toneMappingParams: toneMappingParams, + bounces: bounces + }); - sceneSampler.onSampleRendered = function () { + pipeline.onSampleRendered = function () { if (module.onSampleRendered) { module.onSampleRendered.apply(module, arguments); } @@ -2460,8 +2698,8 @@ } function restartTimer() { - if (sceneSampler) { - sceneSampler.restartTimer(); + if (pipeline) { + pipeline.restartTimer(); } } @@ -2476,8 +2714,8 @@ canvas.style.height = "".concat(size.height, "px"); } - if (sceneSampler) { - sceneSampler.setSize(size.width * pixelRatio, size.height * pixelRatio); + if (pipeline) { + pipeline.setSize(size.width * pixelRatio, size.height * pixelRatio); } }; @@ -2505,8 +2743,8 @@ module.setRenderTime = function (time) { renderTime = time; - if (sceneSampler) { - sceneSampler.setRenderTime(time); + if (pipeline) { + pipeline.setRenderTime(time); } }; @@ -2515,17 +2753,19 @@ }; module.getTotalSamplesRendered = function () { - if (sceneSampler) { - return sceneSampler.getTotalSamplesRendered(); + if (pipeline) { + return pipeline.getTotalSamplesRendered(); } }; module.sendToScreen = function () { - if (sceneSampler) { - sceneSampler.hdrBufferToScreen(); + if (pipeline) { + pipeline.hdrBufferToScreen(); } }; + var lastFocus = false; + module.render = function (scene, camera) { if (!module.renderWhenOffFocus) { var hasFocus = document.hasFocus(); @@ -2546,11 +2786,16 @@ camera.updateMatrixWorld(); if (module.renderToScreen) { - sceneSampler.drawTile(camera); + if (module.maxHardwareUsage) { + // render new sample for the entire screen + pipeline.drawFull(camera); + } else { + // render new sample for a tiled subset of the screen + pipeline.drawTile(camera); + } } else { - sceneSampler.drawOffscreenTile(camera); - } // sceneSampler.drawFull(camera); - + pipeline.drawOffscreenTile(camera); + } }; // Assume module.render is called using requestAnimationFrame. // This means that when the user is on a different browser tab, module.render won't be called. // Since the timer should not measure time when module.render is inactive, @@ -2561,7 +2806,7 @@ module.dispose = function () { document.removeEventListener('visibilitychange', restartTimer); - sceneSampler = false; + pipeline = false; }; return module; @@ -2592,18 +2837,20 @@ if (THREE) { THREE.LensCamera = LensCamera; THREE.SoftDirectionalLight = SoftDirectionalLight; + THREE.EnvironmentLight = EnvironmentLight; THREE.RayTracingMaterial = RayTracingMaterial; THREE.RayTracingRenderer = RayTracingRenderer; THREE.ThickMaterial = ThickMaterial; THREE.ThinMaterial = ThinMaterial; } - exports.constants = constants; + exports.EnvironmentLight = EnvironmentLight; exports.LensCamera = LensCamera; - exports.SoftDirectionalLight = SoftDirectionalLight; exports.RayTracingMaterial = RayTracingMaterial; exports.RayTracingRenderer = RayTracingRenderer; + exports.SoftDirectionalLight = SoftDirectionalLight; + exports.constants = constants; Object.defineProperty(exports, '__esModule', { value: true }); -}))); +})); diff --git a/build/RayTracingRenderer.js b/build/RayTracingRenderer.js index 614adf7..77755c2 100644 --- a/build/RayTracingRenderer.js +++ b/build/RayTracingRenderer.js @@ -1,8 +1,8 @@ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) : typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) : - (factory((global.RayTracingRenderer = {}),global.THREE)); -}(this, (function (exports,THREE$1) { 'use strict'; + (global = global || self, factory(global.RayTracingRenderer = {}, global.THREE)); +}(this, function (exports, THREE$1) { 'use strict'; const ThinMaterial = 1; const ThickMaterial = 2; @@ -38,6 +38,18 @@ } } + class EnvironmentLight extends THREE$1.Light { + constructor(map, ...args) { + super(...args); + this.map = map; + } + + copy(source) { + super.copy(source); + this.map = source.map; + } + } + class RayTracingMaterial extends THREE$1.MeshStandardMaterial { constructor(...args) { super(...args); @@ -249,8 +261,6 @@ void main() { function textureLinear(params) { return ` - ${params.OES_texture_float_linear ? '#define OES_texture_float_linear' : ''} - vec4 textureLinear(sampler2D map, vec2 uv) { #ifdef OES_texture_float_linear return texture(map, uv); @@ -276,16 +286,6 @@ void main() { function intersect(params) { return ` -#define BVH_COLUMNS ${params.bvhColumnsLog} -#define INDEX_COLUMNS ${params.indexColumnsLog} -#define VERTEX_COLUMNS ${params.vertexColumnsLog} -#define STACK_SIZE ${params.maxBvhDepth} -#define NUM_TRIS ${params.numTris} -#define NUM_MATERIALS ${params.numMaterials} -${params.numDiffuseMaps > 0 ? `#define NUM_DIFFUSE_MAPS ${params.numDiffuseMaps}` : ''} -${params.numNormalMaps > 0 ? `#define NUM_NORMAL_MAPS ${params.numNormalMaps}` : ''} -${params.numPbrMaps > 0 ? `#define NUM_PBR_MAPS ${params.numPbrMaps}` : ''} - uniform highp isampler2D indices; uniform sampler2D positions; uniform sampler2D normals; @@ -301,7 +301,7 @@ uniform Materials { #endif #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS) - vec4 diffuseNormalMapSize[${Math.max(params.numDiffuseMaps, params.numNormalMaps)}]; + vec4 diffuseNormalMapSize[${Math.max(params.NUM_DIFFUSE_MAPS, params.NUM_NORMAL_MAPS)}]; #endif #if defined(NUM_PBR_MAPS) @@ -648,23 +648,29 @@ bool intersectSceneShadow(inout Ray ray) { `; } - // Random number generation as described by - // http://www.reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11/ - function random(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; -// lower quality but faster hashing function +const highp float maxUint = 1.0 / 4294967295.0; + +float pixelSeed; +highp uint randState; + +// simple integer hashing function +// https://en.wikipedia.org/wiki/Xorshift uint xorshift(uint x) { x ^= x << 13u; x ^= x >> 17u; @@ -672,43 +678,34 @@ uint xorshift(uint x) { return x; } -#define STRATA_DIMENSIONS ${params.strataDimensions} - -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()); } `; } @@ -722,6 +719,12 @@ vec2 randomStrataVec2() { uniform sampler2D envmap; uniform sampler2D envmapDistribution; +vec2 cartesianToEquirect(vec3 pointOnSphere) { + float phi = mod(atan(-pointOnSphere.z, -pointOnSphere.x), TWOPI); + float theta = acos(pointOnSphere.y); + return vec2(phi * 0.5 * INVPI, theta * INVPI); +} + float getEnvmapV(float u, out int vOffset, out float pdf) { ivec2 size = textureSize(envmap, 0); @@ -785,7 +788,7 @@ vec3 sampleEnvmap(vec2 random, out vec2 uv, out float pdf) { float cosPhi = cos(phi); float sinPhi = sin(phi); - vec3 dir = vec3(sinTheta * cosPhi, cosTheta, sinTheta * sinPhi); + vec3 dir = vec3(-sinTheta * cosPhi, cosTheta, -sinTheta * sinPhi); pdf = partialPdf.x * partialPdf.y * INVPI2 / (2.0 * sinTheta); @@ -806,21 +809,18 @@ float envmapPdf(vec2 uv) { } vec3 sampleEnvmapFromDirection(vec3 d) { - float theta = acos(d.y) * INVPI; - float phi = mod(atan(d.z, d.x), TWOPI) * 0.5 * INVPI; - - return textureLinear(envmap, vec2(phi, theta)).rgb; + vec2 uv = cartesianToEquirect(d); + return textureLinear(envmap, uv).rgb; } // debugging function vec3 sampleEnvmapDistributionFromDirection(vec3 d) { vec2 size = vec2(textureSize(envmap, 0)); - float theta = acos(d.y) * INVPI; - float phi = mod(atan(d.z, d.x), TWOPI) * 0.5 * INVPI; + vec2 uv = cartesianToEquirect(d); - float u = texelFetch(envmapDistribution, ivec2(1.0 + phi * size.x, theta * size.y), 0).g; - float v = texelFetch(envmapDistribution, ivec2(0, theta * size.y), 0).g; + float u = texelFetch(envmapDistribution, ivec2(1.0 + uv.x * size.x, uv.y * size.y), 0).g; + float v = texelFetch(envmapDistribution, ivec2(0, uv.y * size.y), 0).g; return vec3(u * v); } @@ -919,7 +919,7 @@ vec3 materialBrdf(SurfaceInteraction si, vec3 viewDir, vec3 lightDir, float cosT pdf = mix(0.5 * (specularPdf + diffusePdf), specularPdf, si.metalness); - return mix(si.color * diffuse + mix(si.color, vec3(1.0), F) * specular, si.color * specular, si.metalness); + return mix(si.color * diffuse + specular, si.color * specular, si.metalness); } `; @@ -1048,9 +1048,7 @@ vec3 importanceSampleMaterial(SurfaceInteraction si, vec3 viewDir, bool lastBoun } } - float phi = mod(atan(lightDir.z, lightDir.x), TWOPI); - float theta = acos(lightDir.y); - vec2 uv = vec2(0.5 * phi * INVPI, theta * INVPI); + vec2 uv = cartesianToEquirect(lightDir); float lightPdf = envmapPdf(uv); @@ -1070,25 +1068,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); @@ -1171,9 +1169,7 @@ float importanceSampleMaterialShadowCatcher(SurfaceInteraction si, vec3 viewDir, occluded = 0.0; } - float phi = mod(atan(lightDir.z, lightDir.x), TWOPI); - float theta = acos(lightDir.y); - vec2 uv = vec2(0.5 * phi * INVPI, theta * INVPI); + vec2 uv = cartesianToEquirect(lightDir); float lightPdf = envmapPdf(uv); @@ -1198,13 +1194,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) ); @@ -1220,7 +1216,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); @@ -1234,9 +1230,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; } @@ -1260,7 +1256,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); @@ -1273,9 +1269,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); } @@ -1296,12 +1292,28 @@ vec3 sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Ray ray, inout return unrolled; } + function addDefines(params) { + let defines = ''; + + for (let [name, value] of Object.entries(params)) { + // don't define falsy values such as false, 0, and ''. + // this adds support for #ifdef on falsy values + if (value) { + defines += `#define ${name} ${value}\n`; + } + } + + return defines; + } + function fragString(params) { return `#version 300 es precision mediump float; precision mediump int; +${addDefines(params)} + #define PI 3.14159265359 #define TWOPI 6.28318530718 #define INVPI 0.31830988618 @@ -1315,7 +1327,7 @@ precision mediump int; #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; @@ -1328,10 +1340,6 @@ const float R0 = (1.0 - IOR) * (1.0 - IOR) / ((1.0 + IOR) * (1.0 + IOR)); // https://www.w3.org/WAI/GL/wiki/Relative_luminance const vec3 luminance = vec3(0.2126, 0.7152, 0.0722); -#define BOUNCES ${params.bounces} -${params.useGlass ? '#define USE_GLASS' : ''} -${params.useShadowCatcher ? '#define USE_SHADOW_CATCHER' : ''} - struct Ray { vec3 o; vec3 d; @@ -1388,77 +1396,73 @@ ivec4 fetchData(isampler2D s, int i, int columnsLog2) { return texelFetch(s, unpackTexel(i, columnsLog2), 0); } -${textureLinear(params)} +${textureLinear()} ${intersect(params)} -${random(params)} -${envmap(params)} -${bsdf(params)} -${sample(params)} -${sampleMaterial(params)} -${sampleGlass(params)} -${sampleShadowCatcher(params)} +${random()} +${envmap()} +${bsdf()} +${sample()} +${sampleMaterial()} +${sampleGlass()} +${sampleShadowCatcher()} struct Path { Ray ray; + vec3 li; float alpha; vec3 beta; bool specularBounce; bool abort; }; -vec3 bounce(inout Path path, int i) { - vec3 li; - +void bounce(inout Path path, int i) { if (path.abort) { - return li; + return; } SurfaceInteraction si = intersectScene(path.ray); if (!si.hit) { if (path.specularBounce) { - li += path.beta * sampleEnvmapFromDirection(path.ray.d); + path.li += path.beta * sampleEnvmapFromDirection(path.ray.d); } path.abort = true; } else { #ifdef USE_GLASS if (si.materialType == THIN_GLASS || si.materialType == THICK_GLASS) { - li += sampleGlassSpecular(si, i, path.ray, path.beta); + path.li += sampleGlassSpecular(si, i, path.ray, path.beta); path.specularBounce = true; } #endif #ifdef USE_SHADOW_CATCHER if (si.materialType == SHADOW_CATCHER) { - li += sampleShadowCatcher(si, i, path.ray, path.beta, path.alpha, li, path.abort); + path.li += sampleShadowCatcher(si, i, path.ray, path.beta, path.alpha, path.li, path.abort); path.specularBounce = false; } #endif if (si.materialType == STANDARD) { - li += sampleMaterial(si, i, path.ray, path.beta, path.abort); + path.li += sampleMaterial(si, i, path.ray, path.beta, path.abort); path.specularBounce = false; } // 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; } } - - return li; } // Path tracing integrator as described in // http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Path_Tracing.html# vec4 integrator(inout Ray ray) { - vec3 li; - Path path; path.ray = ray; + path.li = vec3(0); path.alpha = 1.0; path.beta = vec3(1.0); path.specularBounce = true; @@ -1469,23 +1473,23 @@ vec4 integrator(inout Ray ray) { // for (int i = 1; i < params.bounces + 1, i += 1) // equivelant to - ${unrollLoop('i', 1, params.bounces + 1, 1, ` - li += bounce(path, i); + ${unrollLoop('i', 1, params.BOUNCES + 1, 1, ` + bounce(path, i); `)} - return vec4(li, path.alpha); + return vec4(path.li, path.alpha); } 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); @@ -1518,9 +1522,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); // } } @@ -1636,7 +1640,9 @@ void main() { array[a] = x; } - // https://en.cppreference.com/w/cpp/algorithm/partition + // Reorders the elements in the range [first, last) in such a way that + // all elements for which the comparator c returns true + // precede the elements for which comparator c returns false. function partition(array, compare, left = 0, right = array.length) { while (left !== right) { while (compare(array[left])) { @@ -1659,7 +1665,9 @@ void main() { return left; } - // https://en.cppreference.com/w/cpp/algorithm/nth_element + // nth_element is a partial sorting algorithm that rearranges elements in [first, last) such that: + // The element pointed at by nth is changed to whatever element would occur in that position if [first, last) were sorted. + // All of the elements before this new nth element compare to true with elements after the nth element function nthElement(array, compare, left = 0, right = array.length, k = Math.floor((left + right) / 2)) { for (let i = left; i <= k; i++) { let minIndex = i; @@ -2039,46 +2047,58 @@ void main() { return true; } + const DEFAULT_MAP_RESOLUTION = { + width: 4096, + height: 2048, + }; + // Tools for generating and modify env maps for lighting from scene component data - function generateEnvMapFromSceneComponents(background, directionalLights){ + function generateEnvMapFromSceneComponents(directionalLights, environmentLights) { + let envImage = initializeEnvMap(environmentLights); + directionalLights.forEach( light => { envImage.data = addDirectionalLightToEnvMap(light, envImage); }); + + return envImage; + } + + function initializeEnvMap(environmentLights) { let envImage; - if(background && background.encoding === THREE.LinearEncoding) { - // background is an HDR image - const image = background.image; + // Initialize map from environment light if present + if (environmentLights.length > 0) { + // TODO: support multiple environment lights (what if they have different resolutions?) + const environmentLight = environmentLights[0]; envImage = { - width: image.width, - height: image.height, - data: image.data + width: environmentLight.map.image.width, + height: environmentLight.map.image.height, + data: environmentLight.map.image.data, }; envImage.data = rgbeToFloat(envImage.data); - directionalLights.forEach( light => { envImage.data = addDirectionalLightToEnvMap(light, envImage); }); - } else { - // background is a single color - let color = background; + envImage.data.forEach((datum, index, arr) => { + arr[index] = datum * environmentLight.intensity; + }); + } else { // initialize blank map + envImage = generateBlankMap(DEFAULT_MAP_RESOLUTION.width, DEFAULT_MAP_RESOLUTION.height); + } - if (!(color instanceof THREE.Color)) { - if (color) { - // color is defined and set to something other than THREE.Color - console.warn('scene.background should be an HDR image or a THREE.Color'); - } + return envImage; + } - color = new THREE.Color(0xffffff); - } + function generateBlankMap(width, height) { + const texels = width * height; + const floatBuffer = new Float32Array(texels * 3); + floatBuffer.fill(0.0); - envImage = { - width: 1, - height: 1, - data: new Float32Array(color.toArray()) - }; - } - return envImage; + return { + width: width, + height: height, + data: floatBuffer, + }; } function addDirectionalLightToEnvMap(light, image) { const sphericalCoords = new THREE$1.Spherical(); const lightDirection = light.position.clone().sub(light.target.position); sphericalCoords.setFromVector3(lightDirection); - sphericalCoords.theta = (Math.PI / 2) - sphericalCoords.theta; + sphericalCoords.theta = (Math.PI * 3 / 2) - sphericalCoords.theta; sphericalCoords.makeSafe(); return addLightAtCoordinates(light, image, sphericalCoords); } @@ -2089,7 +2109,6 @@ void main() { const width = image.width; const height = image.height; - const texels = floatBuffer.length / 3; const xTexels = (floatBuffer.length / (3 * height)); const yTexels = (floatBuffer.length / (3 * width)); // default softness for standard directional lights is 0.95 @@ -2116,9 +2135,9 @@ void main() { } function angleBetweenSphericals(originCoords, currentCoords) { - const originVector = new THREE.Vector3(); + const originVector = new THREE$1.Vector3(); originVector.setFromSpherical(originCoords); - const currentVector = new THREE.Vector3(); + const currentVector = new THREE$1.Vector3(); currentVector.setFromSpherical(currentCoords); return originVector.angleTo(currentVector); } @@ -2137,82 +2156,6 @@ void main() { return sphericalCoords; } - // Stratified Sampling - - 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 - }); - } - - // Stratified Sampling - - 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 - }); - } - function texturesFromMaterials(materials, textureName, textures) { const indices = []; @@ -2279,7 +2222,8 @@ void main() { height = null, channels = null, storage = null, - data = null + data = null, + flipY = false } = params; width = width || data.width || 0; @@ -2314,7 +2258,7 @@ void main() { channels = clamp(channels, 1, 4); const format = [ - gl.R, + gl.RED, gl.RG, gl.RGB, gl.RGBA @@ -2360,12 +2304,19 @@ void main() { // otherwise use the max size of the array texture const layerWidth = dataArray[i].width || width; const layerHeight = dataArray[i].height || height; + + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, Array.isArray(flipY) ? flipY[i] : flipY); + gl.texSubImage3D(target, 0, 0, 0, i, layerWidth, layerHeight, 1, format, type, dataArray[i]); } } else { + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY); gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, data); } + // return state to default + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + return Object.freeze({ target, texture @@ -2436,91 +2387,138 @@ void main() { materialBuffer.bind(0); } - 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, - }; - } + /* + Stratified Sampling + http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html - function maxImageSize(images) { - const maxSize = { - width: 0, - height: 0 - }; + 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. - for (const image of images) { - maxSize.width = Math.max(maxSize.width, image.width); - maxSize.height = Math.max(maxSize.height, image.height); + 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. + */ + + function makeStratifiedSampler(strataCount, dimensions) { + const strata = []; + const l = strataCount ** dimensions; + for (let i = 0; i < l; i++) { + strata[i] = i; } - const relativeSizes = []; - for (const image of images) { - relativeSizes.push(image.width / maxSize.width); - relativeSizes.push(image.height / maxSize.height); + let index = strata.length; + + const sample = []; + + function restart() { + index = 0; } - return { maxSize, relativeSizes }; - } + function next() { + if (index >= strata.length) { + shuffle(strata); + restart(); + } + let stratum = strata[index++]; - // expand array to the given length - function padArray(typedArray, length) { - const newArray = new typedArray.constructor(length); - newArray.set(typedArray); - return newArray; + 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 + }); } - function decomposeScene(scene) { - const meshes = []; - const directionalLights = []; + /* + Stratified Sampling + http://www.pbr-book.org/3ed-2018/Sampling_and_Reconstruction/Stratified_Sampling.html - scene.traverse(child => { - if (child instanceof THREE$1.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$1.MeshStandardMaterial)) { - console.log(child, 'must use MeshStandardMaterial in order to be rendered.'); - } else { - meshes.push(child); + 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. + */ + + 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; } } - if (child instanceof THREE$1.DirectionalLight) { - directionalLights.push(child); + + return combined; + } + + function restart() { + for (const strata of strataObjs) { + strata.restart(); } - }); + } - return { - meshes, directionalLights - }; + return Object.freeze({ + next, + restart, + strataCount + }); } - function makeRayTracingShader(gl, optionalExtensions, fullscreenQuad, textureAllocator, scene) { + //Important TODO: Refactor this file to get rid of duplicate and confusing code - const { OES_texture_float_linear } = optionalExtensions; + function makeRayTracingShader({ + gl, + optionalExtensions, + fullscreenQuad, + textureAllocator, + scene, + bounces // number of global illumination bounces + }) { + + bounces = clamp(bounces, 1, 6); - // Number of ray bounces per sample - const bounces = 3; + 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) { - strataDimensions.push(1); // russian roulette sampling + // russian roulette sampling + // this step is skipped on the first bounce + samplingDimensions.push(1); } } function initScene() { - const { meshes, directionalLights } = decomposeScene(scene); + const { meshes, directionalLights, environmentLights } = decomposeScene(scene); if (meshes.length === 0) { throw 'RayTracingRenderer: Scene contains no renderable meshes.'; } @@ -2547,19 +2545,19 @@ void main() { const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragString({ OES_texture_float_linear, - bvhColumnsLog: bvhDim.columnsLog, - indexColumnsLog: indexDim.columnsLog, - vertexColumnsLog: vertexDim.columnsLog, - maxBvhDepth: flattenedBvh.maxDepth, - numTris: numTris, - numMaterials: materials.length, - numDiffuseMaps: maps.map.textures.length, - numNormalMaps: maps.normalMap.textures.length, - numPbrMaps: pbrMap.textures.length, - bounces, - useGlass, - useShadowCatcher, - strataDimensions: strataDimensions.reduce((a, b) => a + b) + BVH_COLUMNS: bvhDim.columnsLog, + INDEX_COLUMNS: indexDim.columnsLog, + VERTEX_COLUMNS: vertexDim.columnsLog, + STACK_SIZE: flattenedBvh.maxDepth, + NUM_TRIS: numTris, + NUM_MATERIALS: materials.length, + NUM_DIFFUSE_MAPS: maps.map.textures.length, + NUM_NORMAL_MAPS: maps.normalMap.textures.length, + NUM_PBR_MAPS: pbrMap.textures.length, + BOUNCES: bounces, + USE_GLASS: useGlass, + USE_SHADOW_CATCHER: useShadowCatcher, + SAMPLING_DIMENSIONS: samplingDimensions.reduce((a, b) => a + b) })); const program = createProgram(gl, fullscreenQuad.vertexShader, fragmentShader); @@ -2585,6 +2583,7 @@ void main() { if (maps.map.textures.length > 0) { const images = maps.map.textures.map(t => t.image); + const flipY = maps.map.textures.map(t => t.flipY); const { maxSize, relativeSizes } = maxImageSize(images); // create GL Array Texture from individual textures textureAllocator.bind(uniforms.diffuseMap, makeTexture(gl, { @@ -2592,7 +2591,8 @@ void main() { height: maxSize.height, channels: 3, gammaCorrection: true, - data: images + data: images, + flipY })); bufferData.diffuseMapSize = relativeSizes; bufferData.diffuseMapIndex = maps.map.indices; @@ -2600,13 +2600,15 @@ void main() { if (maps.normalMap.textures.length > 0) { const images = maps.normalMap.textures.map(t => t.image); + const flipY = maps.normalMap.textures.map(t => t.flipY); const { maxSize, relativeSizes } = maxImageSize(images); // create GL Array Texture from individual textures textureAllocator.bind(uniforms.normalMap, makeTexture(gl, { width: maxSize.width, height: maxSize.height, channels: 3, - data: images + data: images, + flipY })); bufferData.normalMapSize = relativeSizes; bufferData.normalMapIndex = maps.normalMap.indices; @@ -2614,13 +2616,15 @@ void main() { if (pbrMap.textures.length > 0) { const images = pbrMap.textures.map(t => t.image); + const flipY = pbrMap.textures.map(t => t.flipY); const { maxSize, relativeSizes } = maxImageSize(images); // create GL Array Texture from individual textures textureAllocator.bind(uniforms.pbrMap, makeTexture(gl, { width: maxSize.width, height: maxSize.height, channels: 3, - data: images + data: images, + flipY })); bufferData.pbrMapSize = relativeSizes; bufferData.roughnessMapIndex = pbrMap.indices.roughnessMap; @@ -2661,8 +2665,7 @@ void main() { height: bvhDim.rows, })); - const background = scene.background; - const envImage = generateEnvMapFromSceneComponents(background, directionalLights); + const envImage = generateEnvMapFromSceneComponents(directionalLights, environmentLights); textureAllocator.bind(uniforms.envmap, makeTexture(gl, { data: envImage.data, @@ -2689,13 +2692,21 @@ void main() { 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); @@ -2705,15 +2716,32 @@ void main() { 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 updateSeed() { + 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 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() { @@ -2721,15 +2749,99 @@ void main() { 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$1.RGBEEncoding || texture.map.encoding === THREE$1.LinearEncoding); + } + + function decomposeScene(scene) { + const meshes = []; + const directionalLights = []; + const environmentLights = []; + scene.traverse(child => { + if (child instanceof THREE$1.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$1.MeshStandardMaterial)) { + console.warn(child, 'must use MeshStandardMaterial in order to be rendered.'); + } else { + meshes.push(child); + } + } + if (child instanceof THREE$1.DirectionalLight) { + directionalLights.push(child); + } + if (child instanceof THREE$1.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 + }; + } + function fragString$1(params) { return `#version 300 es @@ -2742,7 +2854,7 @@ out vec4 fragColor; uniform sampler2D image; -${textureLinear(params)} +${textureLinear()} // Tonemapping functions from THREE.js @@ -2801,10 +2913,16 @@ void main() { [THREE$1.ACESFilmicToneMapping]: 'acesFilmic' }; - function makeToneMapShader(gl, optionalExtensions, fullscreenQuad, textureAllocator, toneMapParams) { + function makeToneMapShader({ + gl, + optionalExtensions, + fullscreenQuad, + textureAllocator, + toneMappingParams + }) { const { OES_texture_float_linear } = optionalExtensions; - const { toneMapping, whitePoint, exposure } = toneMapParams; + const { toneMapping, whitePoint, exposure } = toneMappingParams; const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragString$1({ OES_texture_float_linear, @@ -3068,11 +3186,31 @@ void main() { }); } - function makeSceneSampler(gl, optionalExtensions, scene, toneMappingParams) { + var noiseBase64 = 'data:image/png;base64,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'; + + // Important TODO: Refactor this file to get rid of duplicate and confusing code + + function makeRenderingPipeline({ + gl, + optionalExtensions, + scene, + toneMappingParams, + 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); - const toneMapShader = makeToneMapShader(gl, optionalExtensions, fullscreenQuad, textureAllocator, toneMappingParams); + 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; @@ -3084,11 +3222,12 @@ void main() { 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; @@ -3101,13 +3240,12 @@ void main() { sampleCount = 0; tileRender.reset(); - rayTracingShader.setStrataCount(1); - rayTracingShader.updateSeed(); } function initFirstSample(camera) { lastCamera.copy(camera); rayTracingShader.setCamera(camera); + rayTracingShader.useStratifiedSampling(false); clear(); } @@ -3171,8 +3309,21 @@ void main() { }); } + 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(); @@ -3181,11 +3332,7 @@ void main() { if (isFirstTile) { sampleCount++; - rayTracingShader.updateSeed(); - - if (sampleCount === numSimpleSamples) { - rayTracingShader.setStrataCount(strataSize); - } + updateSeed(); } renderTile(x, y, tileWidth, tileHeight); @@ -3198,7 +3345,9 @@ void main() { } function drawOffscreenTile(camera) { - if (!camerasEqual(camera, lastCamera)) { + if (!ready) { + return; + } else if (!camerasEqual(camera, lastCamera)) { initFirstSample(camera); } @@ -3206,12 +3355,7 @@ void main() { if (isFirstTile) { sampleCount++; - rayTracingShader.updateSeed(); - - - if (sampleCount === numSimpleSamples) { - rayTracingShader.setStrataCount(strataSize); - } + updateSeed(); } renderTile(x, y, tileWidth, tileHeight); @@ -3222,17 +3366,15 @@ void main() { } 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(); } @@ -3287,21 +3429,22 @@ void main() { const optionalExtensions = loadExtensions(gl, glOptionalExtensions); // private properties - let sceneSampler = null; + let pipeline = null; const size = new THREE$1.Vector2(); let renderTime = 22; let pixelRatio = 1; - let lastFocus = false; const module = { + bounces: 3, domElement: canvas, + maxHardwareUsage: false, needsUpdate: true, onSampleRendered: null, renderWhenOffFocus: true, renderToScreen: true, + toneMapping: THREE$1.LinearToneMapping, toneMappingExposure: 1, toneMappingWhitePoint: 1, - toneMapping: THREE$1.LinearToneMapping, }; function initScene(scene) { @@ -3313,9 +3456,11 @@ void main() { toneMapping: module.toneMapping }; - sceneSampler = makeSceneSampler(gl, optionalExtensions, scene, toneMappingParams); + const bounces = module.bounces; - sceneSampler.onSampleRendered = (...args) => { + pipeline = makeRenderingPipeline({gl, optionalExtensions, scene, toneMappingParams, bounces}); + + pipeline.onSampleRendered = (...args) => { if (module.onSampleRendered) { module.onSampleRendered(...args); } @@ -3327,8 +3472,8 @@ void main() { } function restartTimer() { - if (sceneSampler) { - sceneSampler.restartTimer(); + if (pipeline) { + pipeline.restartTimer(); } } @@ -3342,8 +3487,8 @@ void main() { 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); } }; @@ -3367,8 +3512,8 @@ void main() { module.setRenderTime = (time) => { renderTime = time; - if (sceneSampler) { - sceneSampler.setRenderTime(time); + if (pipeline) { + pipeline.setRenderTime(time); } }; @@ -3377,17 +3522,18 @@ void main() { }; module.getTotalSamplesRendered = () => { - if (sceneSampler) { - return sceneSampler.getTotalSamplesRendered(); + if (pipeline) { + return pipeline.getTotalSamplesRendered(); } }; module.sendToScreen = () => { - if (sceneSampler) { - sceneSampler.hdrBufferToScreen(); + if (pipeline) { + pipeline.hdrBufferToScreen(); } }; + let lastFocus = false; module.render = (scene, camera) => { if (!module.renderWhenOffFocus) { const hasFocus = document.hasFocus(); @@ -3407,12 +3553,17 @@ void main() { camera.updateMatrixWorld(); if (module.renderToScreen) { - sceneSampler.drawTile(camera); + if(module.maxHardwareUsage) { + // render new sample for the entire screen + pipeline.drawFull(camera); + } else { + // render new sample for a tiled subset of the screen + pipeline.drawTile(camera); + } + } else { - sceneSampler.drawOffscreenTile(camera); + pipeline.drawOffscreenTile(camera); } - - // sceneSampler.drawFull(camera); }; // Assume module.render is called using requestAnimationFrame. @@ -3423,7 +3574,7 @@ void main() { module.dispose = () => { document.removeEventListener('visibilitychange', restartTimer); - sceneSampler = false; + pipeline = false; }; return module; @@ -3453,18 +3604,20 @@ void main() { if (THREE) { THREE.LensCamera = LensCamera; THREE.SoftDirectionalLight = SoftDirectionalLight; + THREE.EnvironmentLight = EnvironmentLight; THREE.RayTracingMaterial = RayTracingMaterial; THREE.RayTracingRenderer = RayTracingRenderer; THREE.ThickMaterial = ThickMaterial; THREE.ThinMaterial = ThinMaterial; } - exports.constants = constants; + exports.EnvironmentLight = EnvironmentLight; exports.LensCamera = LensCamera; - exports.SoftDirectionalLight = SoftDirectionalLight; exports.RayTracingMaterial = RayTracingMaterial; exports.RayTracingRenderer = RayTracingRenderer; + exports.SoftDirectionalLight = SoftDirectionalLight; + exports.constants = constants; Object.defineProperty(exports, '__esModule', { value: true }); -}))); +})); diff --git a/package.json b/package.json index fb54324..24b6a8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "three.js-ray-tracing-renderer", - "version": "1.0.0", + "version": "0.1.1", "description": "", "main": "src/RayTracingRenderer.js", "scripts": {