diff --git a/index.html b/index.html
index 5eb96c9..d631e5a 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,7 @@
+
diff --git a/library/three/RayTracingRenderer.js b/library/three/RayTracingRenderer.js
new file mode 100644
index 0000000..cb994a9
--- /dev/null
+++ b/library/three/RayTracingRenderer.js
@@ -0,0 +1,4661 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) :
+ typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) :
+ (global = global || self, factory(global.RayTracingRenderer = {}, global.THREE));
+}(this, function (exports, THREE$1) { 'use strict';
+
+ const ThinMaterial = 1;
+ const ThickMaterial = 2;
+ const ShadowCatcherMaterial = 3;
+
+ var constants = /*#__PURE__*/Object.freeze({
+ ThinMaterial: ThinMaterial,
+ ThickMaterial: ThickMaterial,
+ ShadowCatcherMaterial: ShadowCatcherMaterial
+ });
+
+ class LensCamera extends THREE$1.PerspectiveCamera {
+ constructor(...args) {
+ super(...args);
+ this.aperture = 0.01;
+ }
+
+ copy(source, recursive) {
+ super.copy(source, recursive);
+ this.aperture = source.aperture;
+ }
+ }
+
+ class SoftDirectionalLight extends THREE$1.DirectionalLight {
+ constructor(color, intensity, softness = 0) {
+ super(color, intensity);
+ this.softness = softness;
+ }
+
+ copy(source) {
+ super.copy(source);
+ this.softness = source.softness;
+ }
+ }
+
+ class EnvironmentLight extends THREE$1.Light {
+ constructor(map, ...args) {
+ super(...args);
+ this.map = map;
+ this.isEnvironmentLight = true;
+ }
+
+ copy(source) {
+ super.copy(source);
+ this.map = source.map;
+ }
+ }
+
+ class RayTracingMaterial extends THREE$1.MeshStandardMaterial {
+ constructor(...args) {
+ super(...args);
+ this.solid = false;
+ this.shadowCatcher = false;
+ }
+
+ copy(source) {
+ super.copy(source);
+ this.solid = source.solid;
+ this.shadowCatcher = source.shadowCatcher;
+ }
+ }
+
+ function loadExtensions(gl, extensions) {
+ const supported = {};
+ for (const name of extensions) {
+ supported[name] = gl.getExtension(name);
+ }
+ return supported;
+ }
+
+ function compileShader(gl, type, source) {
+ const shader = gl.createShader(type);
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+ const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
+
+ if (success) {
+ return shader;
+ }
+
+ const output = source.split('\n').map((x, i) => `${i + 1}: ${x}`).join('\n');
+ console.log(output);
+
+ throw gl.getShaderInfoLog(shader);
+ }
+
+ function createProgram(gl, vertexShader, fragmentShader, transformVaryings, transformBufferMode) {
+ const program = gl.createProgram();
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);
+
+ if (transformVaryings) {
+ gl.transformFeedbackVaryings(program, transformVaryings, transformBufferMode);
+ }
+
+ gl.linkProgram(program);
+
+ gl.detachShader(program, vertexShader);
+ gl.detachShader(program, fragmentShader);
+
+ const success = gl.getProgramParameter(program, gl.LINK_STATUS);
+
+ if (success) {
+ return program;
+ }
+
+ throw gl.getProgramInfoLog(program);
+ }
+
+ function getUniforms(gl, program) {
+ const uniforms = {};
+
+ const count = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
+ for (let i = 0; i < count; i++) {
+ const { name, type } = gl.getActiveUniform(program, i);
+ const location = gl.getUniformLocation(program, name);
+ if (location) {
+ uniforms[name] = {
+ type, location
+ };
+ }
+ }
+
+ return uniforms;
+ }
+
+ function getAttributes(gl, program) {
+ const attributes = {};
+
+ const count = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
+ for (let i = 0; i < count; i++) {
+ const { name } = gl.getActiveAttrib(program, i);
+ if (name) {
+ attributes[name] = gl.getAttribLocation(program, name);
+ }
+ }
+
+ return attributes;
+ }
+
+ function decomposeScene(scene) {
+ const meshes = [];
+ const directionalLights = [];
+ const ambientLights = [];
+ const environmentLights = [];
+
+ scene.traverse(child => {
+ if (child.isMesh) {
+ if (!child.geometry || !child.geometry.getAttribute('position')) {
+ console.warn(child, 'must have a geometry property with a position attribute');
+ }
+ else if (!(child.material.isMeshStandardMaterial)) {
+ console.warn(child, 'must use MeshStandardMaterial in order to be rendered.');
+ } else {
+ meshes.push(child);
+ }
+ }
+ if (child.isDirectionalLight) {
+ directionalLights.push(child);
+ }
+ if (child.isAmbientLight) {
+ ambientLights.push(child);
+ }
+ if (child.isEnvironmentLight) {
+ if (environmentLights.length > 1) {
+ console.warn(environmentLights, 'only one environment light can be used per scene');
+ }
+ // Valid lights have HDR texture map in RGBEEncoding
+ if (isHDRTexture(child)) {
+ environmentLights.push(child);
+ } else {
+ console.warn(child, 'environment light does not use color value or map with THREE.RGBEEncoding');
+ }
+ }
+ });
+
+ const background = scene.background;
+
+ return {
+ background, meshes, directionalLights, ambientLights, environmentLights
+ };
+ }
+
+ function isHDRTexture(texture) {
+ return texture.map
+ && texture.map.image
+ && (texture.map.encoding === THREE$1.RGBEEncoding || texture.map.encoding === THREE$1.LinearEncoding);
+ }
+
+ function makeFramebuffer(gl, { color, depth }) {
+
+ const framebuffer = gl.createFramebuffer();
+
+ function bind() {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
+ }
+
+ function unbind() {
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+ }
+
+ function init() {
+ bind();
+
+ const drawBuffers = [];
+
+ for (let location in color) {
+ location = Number(location);
+
+ if (location === undefined) {
+ console.error('invalid location');
+ }
+
+ const tex = color[location];
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + location, tex.target, tex.texture, 0);
+ drawBuffers.push(gl.COLOR_ATTACHMENT0 + location);
+ }
+
+ gl.drawBuffers(drawBuffers);
+
+ if (depth) {
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, depth.target, depth.texture);
+ }
+
+ unbind();
+ }
+
+ init();
+
+ return {
+ color,
+ bind,
+ unbind
+ };
+ }
+
+ var vertex = {
+ source: `
+ layout(location = 0) in vec2 a_position;
+
+ out vec2 vCoord;
+
+ void main() {
+ vCoord = a_position;
+ gl_Position = vec4(2. * a_position - 1., 0, 1);
+ }
+`
+ };
+
+ let typeMap;
+
+ function makeUniformSetter(gl, program) {
+ const uniformInfo = getUniforms(gl, program);
+ const uniforms = {};
+ const needsUpload = [];
+
+ for (let name in uniformInfo) {
+ const { type, location } = uniformInfo[name];
+
+ const uniform = {
+ type,
+ location,
+ v0: 0,
+ v1: 0,
+ v2: 0,
+ v3: 0
+ };
+
+ uniforms[name] = uniform;
+ }
+
+ function setUniform(name, v0, v1, v2, v3) {
+ // v0 - v4 are the values to be passed to the uniform
+ // v0 can either be a number or an array, and v1-v3 are optional
+ const uni = uniforms[name];
+
+ if (!uni) {
+ // if (!failedUnis.has(name)) {
+ // console.warn(`Uniform "${name}" does not exist in shader`);
+ // failedUnis.add(name);
+ // }
+
+ return;
+ }
+
+ uni.v0 = v0;
+ uni.v1 = v1;
+ uni.v2 = v2;
+ uni.v3 = v3;
+ needsUpload.push(uni);
+ }
+
+ typeMap = typeMap || initTypeMap(gl);
+
+ function upload() {
+ while (needsUpload.length > 0) {
+
+ const { type, location, v0, v1, v2, v3 } = needsUpload.pop();
+ const glMethod = typeMap[type];
+
+ if (v0.length) {
+ if (glMethod.matrix) {
+ const array = v0;
+ const transpose = v1 || false;
+ gl[glMethod.matrix](location, transpose, array);
+ } else {
+ gl[glMethod.array](location, v0);
+ }
+ } else {
+ gl[glMethod.values](location, v0, v1, v2, v3);
+ }
+ }
+ }
+
+ return {
+ setUniform,
+ upload,
+ };
+ }
+
+ function initTypeMap(gl) {
+ return {
+ [gl.FLOAT]: glName(1, 'f'),
+ [gl.FLOAT_VEC2]: glName(2, 'f'),
+ [gl.FLOAT_VEC3]: glName(3, 'f'),
+ [gl.FLOAT_VEC4]: glName(4, 'f'),
+ [gl.INT]: glName(1, 'i'),
+ [gl.INT_VEC2]: glName(2, 'i'),
+ [gl.INT_VEC3]: glName(3, 'i'),
+ [gl.INT_VEC4]: glName(4, 'i'),
+ [gl.SAMPLER_2D]: glName(1, 'i'),
+ [gl.SAMPLER_2D_ARRAY]: glName(1, 'i'),
+ [gl.FLOAT_MAT2]: glNameMatrix(2, 2),
+ [gl.FLOAT_MAT3]: glNameMatrix(3, 3),
+ [gl.FLOAT_MAT4]: glNameMatrix(4, 4)
+ };
+ }
+
+ function glName(numComponents, type) {
+ return {
+ values: `uniform${numComponents}${type}`,
+ array: `uniform${numComponents}${type}v`
+ };
+ }
+
+ function glNameMatrix(rows, columns) {
+ return {
+ matrix: rows === columns ?
+ `uniformMatrix${rows}fv` :
+ `uniformMatrix${rows}x${columns}fv`
+ };
+ }
+
+ function makeRenderPass(gl, params) {
+ const {
+ defines,
+ fragment,
+ vertex,
+ } = params;
+
+ const vertexCompiled = vertex instanceof WebGLShader ? vertex : makeVertexShader(gl, params);
+
+ const fragmentCompiled = fragment instanceof WebGLShader ? fragment : makeFragmentShader(gl, params);
+
+ const program = createProgram(gl, vertexCompiled, fragmentCompiled);
+
+ return {
+ ...makeRenderPassFromProgram(gl, program),
+ outputLocs: fragment.outputs ? getOutputLocations(fragment.outputs) : {}
+ };
+ }
+
+ function makeVertexShader(gl, { defines, vertex }) {
+ return makeShaderStage(gl, gl.VERTEX_SHADER, vertex, defines);
+ }
+
+ function makeFragmentShader(gl, { defines, fragment }) {
+ return makeShaderStage(gl, gl.FRAGMENT_SHADER, fragment, defines);
+ }
+
+ function makeRenderPassFromProgram(gl, program) {
+
+ const uniformSetter = makeUniformSetter(gl, program);
+
+ const textures = {};
+
+ let nextTexUnit = 1;
+
+ function setTexture(name, texture) {
+ if (!texture) {
+ return;
+ }
+
+ if (!textures[name]) {
+ const unit = nextTexUnit++;
+
+ uniformSetter.setUniform(name, unit);
+
+ textures[name] = {
+ unit,
+ tex: texture
+ };
+ } else {
+ textures[name].tex = texture;
+ }
+ }
+
+ function bindTextures() {
+ for (let name in textures) {
+ const { tex, unit } = textures[name];
+ gl.activeTexture(gl.TEXTURE0 + unit);
+ gl.bindTexture(tex.target, tex.texture);
+ }
+ }
+
+ function useProgram(autoBindTextures = true) {
+ gl.useProgram(program);
+ uniformSetter.upload();
+ if (autoBindTextures) {
+ bindTextures();
+ }
+ }
+
+ return {
+ attribLocs: getAttributes(gl, program),
+ bindTextures,
+ program,
+ setTexture,
+ setUniform: uniformSetter.setUniform,
+ textures,
+ useProgram,
+ };
+ }
+
+ function makeShaderStage(gl, type, shader, defines) {
+ let str = '#version 300 es\nprecision mediump float;\nprecision mediump int;\n';
+
+ if (defines) {
+ str += addDefines(defines);
+ }
+
+ if (type === gl.FRAGMENT_SHADER && shader.outputs) {
+ str += addOutputs(shader.outputs);
+ }
+
+ if (shader.includes) {
+ str += addIncludes(shader.includes, defines);
+ }
+
+ if (typeof shader.source === 'function') {
+ str += shader.source(defines);
+ } else {
+ str += shader.source;
+ }
+
+ return compileShader(gl, type, str);
+ }
+
+ function addDefines(defines) {
+ let str = '';
+
+ for (const name in defines) {
+ const value = defines[name];
+
+ // don't define falsy values such as false, 0, and ''.
+ // this adds support for #ifdef on falsy values
+ if (value) {
+ str += `#define ${name} ${value}\n`;
+ }
+ }
+
+ return str;
+ }
+
+ function addOutputs(outputs) {
+ let str = '';
+
+ const locations = getOutputLocations(outputs);
+
+ for (let name in locations) {
+ const location = locations[name];
+ str += `layout(location = ${location}) out vec4 out_${name};\n`;
+ }
+
+ return str;
+ }
+
+ function addIncludes(includes, defines) {
+ let str = '';
+
+ for (let include of includes) {
+ if (typeof include === 'function') {
+ str += include(defines);
+ } else {
+ str += include;
+ }
+ }
+
+ return str;
+ }
+
+ function getOutputLocations(outputs) {
+ let locations = {};
+
+ for (let i = 0; i < outputs.length; i++) {
+ locations[outputs[i]] = i;
+ }
+
+ return locations;
+ }
+
+ function makeFullscreenQuad(gl) {
+ const vao = gl.createVertexArray();
+
+ gl.bindVertexArray(vao);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]), gl.STATIC_DRAW);
+
+ // vertex shader should set layout(location = 0) on position attribute
+ const posLoc = 0;
+
+ gl.enableVertexAttribArray(posLoc);
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
+
+ gl.bindVertexArray(null);
+
+ const vertexShader = makeVertexShader(gl, { vertex });
+
+ function draw() {
+ gl.bindVertexArray(vao);
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
+ }
+
+ return {
+ draw,
+ vertexShader
+ };
+ }
+
+ var vertex$1 = {
+
+ source: `
+ in vec3 aPosition;
+ in vec3 aNormal;
+ in vec2 aUv;
+ in ivec2 aMaterialMeshIndex;
+
+ uniform mat4 projView;
+
+ out vec3 vPosition;
+ out vec3 vNormal;
+ out vec2 vUv;
+ flat out ivec2 vMaterialMeshIndex;
+
+ void main() {
+ vPosition = aPosition;
+ vNormal = aNormal;
+ vUv = aUv;
+ vMaterialMeshIndex = aMaterialMeshIndex;
+ gl_Position = projView * vec4(aPosition, 1);
+ }
+`
+ };
+
+ var constants$1 = `
+ #define PI 3.14159265359
+ #define TWOPI 6.28318530718
+ #define INVPI 0.31830988618
+ #define INVPI2 0.10132118364
+ #define EPS 0.0005
+ #define INF 1.0e999
+
+ #define ROUGHNESS_MIN 0.03
+`;
+
+ var materialBuffer = `
+
+uniform Materials {
+ vec4 colorAndMaterialType[NUM_MATERIALS];
+ vec4 roughnessMetalnessNormalScale[NUM_MATERIALS];
+
+ #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS) || defined(NUM_PBR_MAPS)
+ ivec4 diffuseNormalRoughnessMetalnessMapIndex[NUM_MATERIALS];
+ #endif
+
+ #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS)
+ vec4 diffuseNormalMapSize[NUM_DIFFUSE_NORMAL_MAPS];
+ #endif
+
+ #if defined(NUM_PBR_MAPS)
+ vec2 pbrMapSize[NUM_PBR_MAPS];
+ #endif
+} materials;
+
+#ifdef NUM_DIFFUSE_MAPS
+ uniform mediump sampler2DArray diffuseMap;
+#endif
+
+#ifdef NUM_NORMAL_MAPS
+ uniform mediump sampler2DArray normalMap;
+#endif
+
+#ifdef NUM_PBR_MAPS
+ uniform mediump sampler2DArray pbrMap;
+#endif
+
+float getMatType(int materialIndex) {
+ return materials.colorAndMaterialType[materialIndex].w;
+}
+
+vec3 getMatColor(int materialIndex, vec2 uv) {
+ vec3 color = materials.colorAndMaterialType[materialIndex].rgb;
+
+ #ifdef NUM_DIFFUSE_MAPS
+ int diffuseMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].x;
+ if (diffuseMapIndex >= 0) {
+ color *= texture(diffuseMap, vec3(uv * materials.diffuseNormalMapSize[diffuseMapIndex].xy, diffuseMapIndex)).rgb;
+ }
+ #endif
+
+ return color;
+}
+
+float getMatRoughness(int materialIndex, vec2 uv) {
+ float roughness = materials.roughnessMetalnessNormalScale[materialIndex].x;
+
+ #ifdef NUM_PBR_MAPS
+ int roughnessMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].z;
+ if (roughnessMapIndex >= 0) {
+ roughness *= texture(pbrMap, vec3(uv * materials.pbrMapSize[roughnessMapIndex].xy, roughnessMapIndex)).g;
+ }
+ #endif
+
+ return roughness;
+}
+
+float getMatMetalness(int materialIndex, vec2 uv) {
+ float metalness = materials.roughnessMetalnessNormalScale[materialIndex].y;
+
+ #ifdef NUM_PBR_MAPS
+ int metalnessMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].w;
+ if (metalnessMapIndex >= 0) {
+ metalness *= texture(pbrMap, vec3(uv * materials.pbrMapSize[metalnessMapIndex].xy, metalnessMapIndex)).b;
+ }
+ #endif
+
+ return metalness;
+}
+
+#ifdef NUM_NORMAL_MAPS
+vec3 getMatNormal(int materialIndex, vec2 uv, vec3 normal, vec3 dp1, vec3 dp2, vec2 duv1, vec2 duv2) {
+ int normalMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].y;
+ if (normalMapIndex >= 0) {
+ // http://www.thetenthplanet.de/archives/1180
+ // Compute co-tangent and co-bitangent vectors
+ vec3 dp2perp = cross(dp2, normal);
+ vec3 dp1perp = cross(normal, dp1);
+ vec3 dpdu = dp2perp * duv1.x + dp1perp * duv2.x;
+ vec3 dpdv = dp2perp * duv1.y + dp1perp * duv2.y;
+ float invmax = inversesqrt(max(dot(dpdu, dpdu), dot(dpdv, dpdv)));
+ dpdu *= invmax;
+ dpdv *= invmax;
+
+ vec3 n = 2.0 * texture(normalMap, vec3(uv * materials.diffuseNormalMapSize[normalMapIndex].zw, normalMapIndex)).rgb - 1.0;
+ n.xy *= materials.roughnessMetalnessNormalScale[materialIndex].zw;
+
+ mat3 tbn = mat3(dpdu, dpdv, normal);
+
+ return normalize(tbn * n);
+ } else {
+ return normal;
+ }
+}
+#endif
+`;
+
+ var fragment = {
+
+ outputs: ['position', 'normal', 'faceNormal', 'color', 'matProps'],
+ includes: [
+ constants$1,
+ materialBuffer,
+ ],
+ source: `
+ in vec3 vPosition;
+ in vec3 vNormal;
+ in vec2 vUv;
+ flat in ivec2 vMaterialMeshIndex;
+
+ vec3 faceNormals(vec3 pos) {
+ vec3 fdx = dFdx(pos);
+ vec3 fdy = dFdy(pos);
+ return cross(fdx, fdy);
+ }
+
+ void main() {
+ int materialIndex = vMaterialMeshIndex.x;
+ int meshIndex = vMaterialMeshIndex.y;
+
+ vec2 uv = fract(vUv);
+
+ vec3 color = getMatColor(materialIndex, uv);
+ float roughness = getMatRoughness(materialIndex, uv);
+ float metalness = getMatMetalness(materialIndex, uv);
+ float materialType = getMatType(materialIndex);
+
+ roughness = clamp(roughness, ROUGHNESS_MIN, 1.0);
+ metalness = clamp(metalness, 0.0, 1.0);
+
+ vec3 normal = normalize(vNormal);
+ vec3 faceNormal = normalize(faceNormals(vPosition));
+ normal *= sign(dot(normal, faceNormal));
+
+ #ifdef NUM_NORMAL_MAPS
+ vec3 dp1 = dFdx(vPosition);
+ vec3 dp2 = dFdy(vPosition);
+ vec2 duv1 = dFdx(vUv);
+ vec2 duv2 = dFdy(vUv);
+ normal = getMatNormal(materialIndex, uv, normal, dp1, dp2, duv1, duv2);
+ #endif
+
+ out_position = vec4(vPosition, float(meshIndex) + EPS);
+ out_normal = vec4(normal, materialType);
+ out_faceNormal = vec4(faceNormal, 0);
+ out_color = vec4(color, 0);
+ out_matProps = vec4(roughness, metalness, 0, 0);
+ }
+`
+
+ };
+
+ function makeGBufferPass(gl, { materialBuffer, mergedMesh }) {
+ const renderPass = makeRenderPass(gl, {
+ defines: materialBuffer.defines,
+ vertex: vertex$1,
+ fragment
+ });
+
+ renderPass.setTexture('diffuseMap', materialBuffer.textures.diffuseMap);
+ renderPass.setTexture('normalMap', materialBuffer.textures.normalMap);
+ renderPass.setTexture('pbrMap', materialBuffer.textures.pbrMap);
+
+ const geometry = mergedMesh.geometry;
+
+ const elementCount = geometry.getIndex().count;
+
+ const vao = gl.createVertexArray();
+
+ gl.bindVertexArray(vao);
+ uploadAttributes(gl, renderPass, geometry);
+ gl.bindVertexArray(null);
+
+ let jitterX = 0;
+ let jitterY = 0;
+ function setJitter(x, y) {
+ jitterX = x;
+ jitterY = y;
+ }
+
+ let currentCamera;
+ function setCamera(camera) {
+ currentCamera = camera;
+ }
+
+ function calcCamera() {
+ projView.copy(currentCamera.projectionMatrix);
+
+ projView.elements[8] += 2 * jitterX;
+ projView.elements[9] += 2 * jitterY;
+
+ projView.multiply(currentCamera.matrixWorldInverse);
+ renderPass.setUniform('projView', projView.elements);
+ }
+
+ let projView = new THREE$1.Matrix4();
+
+ function draw() {
+ calcCamera();
+ gl.bindVertexArray(vao);
+ renderPass.useProgram();
+ gl.enable(gl.DEPTH_TEST);
+ gl.drawElements(gl.TRIANGLES, elementCount, gl.UNSIGNED_INT, 0);
+ gl.disable(gl.DEPTH_TEST);
+ }
+
+ return {
+ draw,
+ outputLocs: renderPass.outputLocs,
+ setCamera,
+ setJitter
+ };
+ }
+
+ function uploadAttributes(gl, renderPass, geometry) {
+ setAttribute(gl, renderPass.attribLocs.aPosition, geometry.getAttribute('position'));
+ setAttribute(gl, renderPass.attribLocs.aNormal, geometry.getAttribute('normal'));
+ setAttribute(gl, renderPass.attribLocs.aUv, geometry.getAttribute('uv'));
+ setAttribute(gl, renderPass.attribLocs.aMaterialMeshIndex, geometry.getAttribute('materialMeshIndex'));
+
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.getIndex().array, gl.STATIC_DRAW);
+ }
+
+ function setAttribute(gl, location, bufferAttribute) {
+ if (location === undefined) {
+ return;
+ }
+
+ const { itemSize, array } = bufferAttribute;
+
+ gl.enableVertexAttribArray(location);
+ gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
+ gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
+
+ if (array instanceof Float32Array) {
+ gl.vertexAttribPointer(location, itemSize, gl.FLOAT, false, 0, 0);
+ } else if (array instanceof Int32Array) {
+ gl.vertexAttribIPointer(location, itemSize, gl.INT, 0, 0);
+ } else {
+ throw 'Unsupported buffer type';
+ }
+ }
+
+ function makeUniformBuffer(gl, program, blockName) {
+ const blockIndex = gl.getUniformBlockIndex(program, blockName);
+ const blockSize = gl.getActiveUniformBlockParameter(program, blockIndex, gl.UNIFORM_BLOCK_DATA_SIZE);
+
+ const uniforms = getUniformBlockInfo(gl, program, blockIndex);
+
+ const buffer = gl.createBuffer();
+ gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
+ gl.bufferData(gl.UNIFORM_BUFFER, blockSize, gl.STATIC_DRAW);
+
+ const data = new DataView(new ArrayBuffer(blockSize));
+
+ function set(name, value) {
+ if (!uniforms[name]) {
+ // console.warn('No uniform property with name ', name);
+ return;
+ }
+
+ const { type, size, offset, stride } = uniforms[name];
+
+ switch(type) {
+ case gl.FLOAT:
+ setData(data, 'setFloat32', size, offset, stride, 1, value);
+ break;
+ case gl.FLOAT_VEC2:
+ setData(data, 'setFloat32', size, offset, stride, 2, value);
+ break;
+ case gl.FLOAT_VEC3:
+ setData(data, 'setFloat32', size, offset, stride, 3, value);
+ break;
+ case gl.FLOAT_VEC4:
+ setData(data, 'setFloat32', size, offset, stride, 4, value);
+ break;
+ case gl.INT:
+ setData(data, 'setInt32', size, offset, stride, 1, value);
+ break;
+ case gl.INT_VEC2:
+ setData(data, 'setInt32', size, offset, stride, 2, value);
+ break;
+ case gl.INT_VEC3:
+ setData(data, 'setInt32', size, offset, stride, 3, value);
+ break;
+ case gl.INT_VEC4:
+ setData(data, 'setInt32', size, offset, stride, 4, value);
+ break;
+ case gl.BOOL:
+ setData(data, 'setUint32', size, offset, stride, 1, value);
+ break;
+ default:
+ console.warn('UniformBuffer: Unsupported type');
+ }
+ }
+
+ function bind(index) {
+ gl.bindBuffer(gl.UNIFORM_BUFFER, buffer);
+ gl.bufferSubData(gl.UNIFORM_BUFFER, 0, data);
+ gl.bindBufferBase(gl.UNIFORM_BUFFER, index, buffer);
+ }
+
+ return {
+ set,
+ bind
+ };
+ }
+
+ function getUniformBlockInfo(gl, program, blockIndex) {
+ const indices = gl.getActiveUniformBlockParameter(program, blockIndex, gl.UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES);
+ const offset = gl.getActiveUniforms(program, indices, gl.UNIFORM_OFFSET);
+ const stride = gl.getActiveUniforms(program, indices, gl.UNIFORM_ARRAY_STRIDE);
+
+ const uniforms = {};
+ for (let i = 0; i < indices.length; i++) {
+ const { name, type, size } = gl.getActiveUniform(program, indices[i]);
+ uniforms[name] = {
+ type,
+ size,
+ offset: offset[i],
+ stride: stride[i]
+ };
+ }
+
+ return uniforms;
+ }
+
+ function setData(dataView, setter, size, offset, stride, components, value) {
+ const l = Math.min(value.length / components, size);
+ for (let i = 0; i < l; i++) {
+ for (let k = 0; k < components; k++) {
+ dataView[setter](offset + i * stride + k * 4, value[components * i + k], true);
+ }
+ }
+ }
+
+ function clamp(x, min, max) {
+ return Math.min(Math.max(x, min), max);
+ }
+
+ function shuffle(arr) {
+ for (let i = arr.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ const x = arr[i];
+ arr[i] = arr[j];
+ arr[j] = x;
+ }
+ return arr;
+ }
+
+ function numberArraysEqual(a, b, eps = 1e-4) {
+ for (let i = 0; i < a.length; i++) {
+ if (Math.abs(a[i] - b[i]) > eps) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ function makeTexture(gl, params) {
+ let {
+ width = null,
+ height = null,
+
+ // A single HTMLImageElement, ImageData, or TypedArray,
+ // Or an array of any of these objects. In this case an Array Texture will be created
+ data = null,
+
+ // If greater than 1, create an Array Texture of this length
+ length = 1,
+
+ // Number of channels, [1-4]. If left blank, the the function will decide the number of channels automatically from the data
+ channels = null,
+
+ // Either 'byte' or 'float'
+ // If left empty, the function will decide the format automatically from the data
+ storage = null,
+
+ // Reverse the texture across the y-axis.
+ flipY = false,
+
+ // sampling properties
+ gammaCorrection = false,
+ wrapS = gl.CLAMP_TO_EDGE,
+ wrapT = gl.CLAMP_TO_EDGE,
+ minFilter = gl.NEAREST,
+ magFilter = gl.NEAREST,
+ } = params;
+
+ width = width || data.width || 0;
+ height = height || data.height || 0;
+
+ const texture = gl.createTexture();
+
+ let target;
+ let dataArray;
+
+ // if data is a JS array but not a TypedArray, assume data is an array of images and create a GL Array Texture
+ if (Array.isArray(data)) {
+ dataArray = data;
+ data = dataArray[0];
+ }
+
+ target = dataArray || length > 1 ? gl.TEXTURE_2D_ARRAY : gl.TEXTURE_2D;
+
+ gl.activeTexture(gl.TEXTURE0);
+ gl.bindTexture(target, texture);
+
+ gl.texParameteri(target, gl.TEXTURE_WRAP_S, wrapS);
+ gl.texParameteri(target, gl.TEXTURE_WRAP_T, wrapT);
+ gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, minFilter);
+ gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, magFilter);
+
+ if (!channels) {
+ if (data && data.length) {
+ channels = data.length / (width * height); // infer number of channels from data size
+ } else {
+ channels = 4;
+ }
+ }
+
+ channels = clamp(channels, 1, 4);
+
+ const { type, format, internalFormat } = getTextureFormat(gl, channels, storage, data, gammaCorrection);
+
+ if (dataArray) {
+ gl.texStorage3D(target, 1, internalFormat, width, height, dataArray.length);
+ for (let i = 0; i < dataArray.length; i++) {
+ // if layer is an HTMLImageElement, use the .width and .height properties of each layer
+ // 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 if (length > 1) {
+ // create empty array texture
+ gl.texStorage3D(target, 1, internalFormat, width, height, length);
+ } else {
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipY);
+ gl.texStorage2D(target, 1, internalFormat, width, height);
+ if (data) {
+ gl.texSubImage2D(target, 0, 0, 0, width, height, format, type, data);
+ }
+ }
+
+ // return state to default
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
+
+ return {
+ target,
+ texture
+ };
+ }
+
+ function makeDepthTarget(gl, width, height) {
+ const texture = gl.createRenderbuffer();
+ const target = gl.RENDERBUFFER;
+
+ gl.bindRenderbuffer(target, texture);
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, width, height);
+ gl.bindRenderbuffer(target, null);
+
+ return {
+ target,
+ texture
+ };
+ }
+
+ function getTextureFormat(gl, channels, storage, data, gammaCorrection) {
+ let type;
+ let internalFormat;
+
+ const isByteArray =
+ data instanceof Uint8Array ||
+ data instanceof HTMLImageElement ||
+ data instanceof HTMLCanvasElement ||
+ data instanceof ImageData;
+
+ const isFloatArray = data instanceof Float32Array;
+
+ if (storage === 'byte' || (!storage && isByteArray)) {
+ internalFormat = {
+ 1: gl.R8,
+ 2: gl.RG8,
+ 3: gammaCorrection ? gl.SRGB8 : gl.RGB8,
+ 4: gammaCorrection ? gl.SRGB8_ALPHA8 : gl.RGBA8
+ }[channels];
+
+ type = gl.UNSIGNED_BYTE;
+ } else if (storage === 'float' || (!storage && isFloatArray)) {
+ internalFormat = {
+ 1: gl.R32F,
+ 2: gl.RG32F,
+ 3: gl.RGB32F,
+ 4: gl.RGBA32F
+ }[channels];
+
+ type = gl.FLOAT;
+ } else if (storage === 'halfFloat') {
+ internalFormat = {
+ 1: gl.R16F,
+ 2: gl.RG16F,
+ 3: gl.RGB16F,
+ 4: gl.RGBA16F
+ }[channels];
+
+ type = gl.FLOAT;
+ } else if (storage === 'snorm') {
+ internalFormat = {
+ 1: gl.R8_SNORM,
+ 2: gl.RG8_SNORM,
+ 3: gl.RGB8_SNORM,
+ 4: gl.RGBA8_SNORM,
+ }[channels];
+
+ type = gl.UNSIGNED_BYTE;
+ }
+
+ const format = {
+ 1: gl.RED,
+ 2: gl.RG,
+ 3: gl.RGB,
+ 4: gl.RGBA
+ }[channels];
+
+ return {
+ format,
+ internalFormat,
+ type
+ };
+ }
+
+ // retrieve textures used by meshes, grouping textures from meshes shared by *the same* mesh property
+ function getTexturesFromMaterials(meshes, textureNames) {
+ const textureMap = {};
+
+ for (const name of textureNames) {
+ const textures = [];
+ textureMap[name] = {
+ indices: texturesFromMaterials(meshes, name, textures),
+ textures
+ };
+ }
+
+ return textureMap;
+ }
+
+ // retrieve textures used by meshes, grouping textures from meshes shared *across all* mesh properties
+ function mergeTexturesFromMaterials(meshes, textureNames) {
+ const textureMap = {
+ textures: [],
+ indices: {}
+ };
+
+ for (const name of textureNames) {
+ textureMap.indices[name] = texturesFromMaterials(meshes, name, textureMap.textures);
+ }
+
+ return textureMap;
+ }
+
+ function texturesFromMaterials(materials, textureName, textures) {
+ const indices = [];
+
+ for (const material of materials) {
+ if (!material[textureName]) {
+ indices.push(-1);
+ } else {
+ let index = textures.length;
+ for (let i = 0; i < textures.length; i++) {
+ if (textures[i] === material[textureName]) {
+ // Reuse existing duplicate texture.
+ index = i;
+ break;
+ }
+ }
+ if (index === textures.length) {
+ // New texture. Add texture to list.
+ textures.push(material[textureName]);
+ }
+ indices.push(index);
+ }
+ }
+
+ return indices;
+ }
+
+ function makeMaterialBuffer(gl, materials) {
+ const maps = getTexturesFromMaterials(materials, ['map', 'normalMap']);
+ const pbrMap = mergeTexturesFromMaterials(materials, ['roughnessMap', 'metalnessMap']);
+
+ const textures = {};
+
+ const bufferData = {};
+
+ bufferData.color = materials.map(m => m.color);
+ bufferData.roughness = materials.map(m => m.roughness);
+ bufferData.metalness = materials.map(m => m.metalness);
+ bufferData.normalScale = materials.map(m => m.normalScale);
+
+ bufferData.type = materials.map(m => {
+ if (m.shadowCatcher) {
+ return ShadowCatcherMaterial;
+ }
+ if (m.transparent) {
+ return m.solid ? ThickMaterial : ThinMaterial;
+ }
+ });
+
+ if (maps.map.textures.length > 0) {
+ const { relativeSizes, texture } = makeTextureArray(gl, maps.map.textures, true);
+ textures.diffuseMap = texture;
+ bufferData.diffuseMapSize = relativeSizes;
+ bufferData.diffuseMapIndex = maps.map.indices;
+ }
+
+ if (maps.normalMap.textures.length > 0) {
+ const { relativeSizes, texture } = makeTextureArray(gl, maps.normalMap.textures, false);
+ textures.normalMap = texture;
+ bufferData.normalMapSize = relativeSizes;
+ bufferData.normalMapIndex = maps.normalMap.indices;
+ }
+
+ if (pbrMap.textures.length > 0) {
+ const { relativeSizes, texture } = makeTextureArray(gl, pbrMap.textures, false);
+ textures.pbrMap = texture;
+ bufferData.pbrMapSize = relativeSizes;
+ bufferData.roughnessMapIndex = pbrMap.indices.roughnessMap;
+ bufferData.metalnessMapIndex = pbrMap.indices.metalnessMap;
+ }
+
+ const defines = {
+ NUM_MATERIALS: materials.length,
+ NUM_DIFFUSE_MAPS: maps.map.textures.length,
+ NUM_NORMAL_MAPS: maps.normalMap.textures.length,
+ NUM_DIFFUSE_NORMAL_MAPS: Math.max(maps.map.textures.length, maps.normalMap.textures.length),
+ NUM_PBR_MAPS: pbrMap.textures.length,
+ };
+
+ // create temporary shader program including the Material uniform buffer
+ // used to query the compiled structure of the uniform buffer
+ const renderPass = makeRenderPass(gl, {
+ vertex: {
+ source: `void main() {}`
+ },
+ fragment: {
+ includes: [ materialBuffer ],
+ source: `void main() {}`
+ },
+ defines
+ });
+
+ uploadToUniformBuffer(gl, renderPass.program, bufferData);
+
+ return { defines, textures };
+ }
+
+ function makeTextureArray(gl, textures, gammaCorrection = false) {
+ const images = textures.map(t => t.image);
+ const flipY = textures.map(t => t.flipY);
+ const { maxSize, relativeSizes } = maxImageSize(images);
+
+ // create GL Array Texture from individual textures
+ const texture = makeTexture(gl, {
+ width: maxSize.width,
+ height: maxSize.height,
+ gammaCorrection,
+ data: images,
+ flipY,
+ channels: 3,
+ minFilter: gl.LINEAR,
+ magFilter: gl.LINEAR,
+ });
+
+ return {
+ texture,
+ relativeSizes
+ };
+ }
+
+ 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 };
+ }
+
+
+ // Upload arrays to uniform buffer objects
+ // Packs different arrays into vec4's to take advantage of GLSL's std140 memory layout
+
+ function uploadToUniformBuffer(gl, program, bufferData) {
+ const materialBuffer = makeUniformBuffer(gl, program, 'Materials');
+
+ materialBuffer.set('Materials.colorAndMaterialType[0]', interleave(
+ { data: [].concat(...bufferData.color.map(d => d.toArray())), channels: 3 },
+ { data: bufferData.type, channels: 1}
+ ));
+
+ materialBuffer.set('Materials.roughnessMetalnessNormalScale[0]', interleave(
+ { data: bufferData.roughness, channels: 1 },
+ { data: bufferData.metalness, channels: 1 },
+ { data: [].concat(...bufferData.normalScale.map(d => d.toArray())), channels: 2 }
+ ));
+
+ materialBuffer.set('Materials.diffuseNormalRoughnessMetalnessMapIndex[0]', interleave(
+ { data: bufferData.diffuseMapIndex, channels: 1 },
+ { data: bufferData.normalMapIndex, channels: 1 },
+ { data: bufferData.roughnessMapIndex, channels: 1 },
+ { data: bufferData.metalnessMapIndex, channels: 1 }
+ ));
+
+ materialBuffer.set('Materials.diffuseNormalMapSize[0]', interleave(
+ { data: bufferData.diffuseMapSize, channels: 2 },
+ { data: bufferData.normalMapSize, channels: 2 }
+ ));
+
+ materialBuffer.set('Materials.pbrMapSize[0]', bufferData.pbrMapSize);
+
+ materialBuffer.bind(0);
+ }
+
+ function interleave(...arrays) {
+ let maxLength = 0;
+ for (let i = 0; i < arrays.length; i++) {
+ const a = arrays[i];
+ const l = a.data ? a.data.length / a.channels : 0;
+ maxLength = Math.max(maxLength, l);
+ }
+
+ const interleaved = [];
+ for (let i = 0; i < maxLength; i++) {
+ for (let j = 0; j < arrays.length; j++) {
+ const { data = [], channels } = arrays[j];
+ for (let c = 0; c < channels; c++) {
+ interleaved.push(data[i * channels + c]);
+ }
+ }
+ }
+
+ return interleaved;
+ }
+
+ function mergeMeshesToGeometry(meshes) {
+
+ let vertexCount = 0;
+ let indexCount = 0;
+
+ const geometryAndMaterialIndex = [];
+ const materialIndexMap = new Map();
+
+ for (const mesh of meshes) {
+ const geometry = cloneBufferGeometry(mesh.geometry, ['position', 'normal', 'uv']);
+
+ const index = geometry.getIndex();
+ if (!index) {
+ addFlatGeometryIndices(geometry);
+ }
+
+ geometry.applyMatrix(mesh.matrixWorld);
+
+ if (!geometry.getAttribute('normal')) {
+ geometry.computeVertexNormals();
+ } else {
+ geometry.normalizeNormals();
+ }
+
+ vertexCount += geometry.getAttribute('position').count;
+ indexCount += geometry.getIndex().count;
+
+ const material = mesh.material;
+ let materialIndex = materialIndexMap.get(material);
+ if (materialIndex === undefined) {
+ materialIndex = materialIndexMap.size;
+ materialIndexMap.set(material, materialIndex);
+ }
+
+ geometryAndMaterialIndex.push({
+ geometry,
+ materialIndex
+ });
+ }
+
+ const geometry = mergeGeometry(geometryAndMaterialIndex, vertexCount, indexCount);
+
+ return {
+ geometry,
+ materials: Array.from(materialIndexMap.keys())
+ };
+ }
+
+ function mergeGeometry(geometryAndMaterialIndex, vertexCount, indexCount) {
+ const positionAttrib = new THREE$1.BufferAttribute(new Float32Array(3 * vertexCount), 3, false);
+ const normalAttrib = new THREE$1.BufferAttribute(new Float32Array(3 * vertexCount), 3, false);
+ const uvAttrib = new THREE$1.BufferAttribute(new Float32Array(2 * vertexCount), 2, false);
+ const materialMeshIndexAttrib = new THREE$1.BufferAttribute(new Int32Array(2 * vertexCount), 2, false);
+ const indexAttrib = new THREE$1.BufferAttribute(new Uint32Array(indexCount), 1, false);
+
+ const mergedGeometry = new THREE$1.BufferGeometry();
+ mergedGeometry.addAttribute('position', positionAttrib);
+ mergedGeometry.addAttribute('normal', normalAttrib);
+ mergedGeometry.addAttribute('uv', uvAttrib);
+ mergedGeometry.addAttribute('materialMeshIndex', materialMeshIndexAttrib);
+ mergedGeometry.setIndex(indexAttrib);
+
+ let currentVertex = 0;
+ let currentIndex = 0;
+ let currentMesh = 1;
+
+ for (const { geometry, materialIndex } of geometryAndMaterialIndex) {
+ const vertexCount = geometry.getAttribute('position').count;
+ mergedGeometry.merge(geometry, currentVertex);
+
+ const meshIndex = geometry.getIndex();
+ for (let i = 0; i < meshIndex.count; i++) {
+ indexAttrib.setX(currentIndex + i, currentVertex + meshIndex.getX(i));
+ }
+
+ for (let i = 0; i < vertexCount; i++) {
+ materialMeshIndexAttrib.setXY(currentVertex + i, materialIndex, currentMesh);
+ }
+
+ currentVertex += vertexCount;
+ currentIndex += meshIndex.count;
+ currentMesh++;
+ }
+
+ return mergedGeometry;
+ }
+
+ // Similar to buffergeometry.clone(), except we only copy
+ // specific attributes instead of everything
+ function cloneBufferGeometry(bufferGeometry, attributes) {
+ const newGeometry = new THREE$1.BufferGeometry();
+
+ for (const name of attributes) {
+ const attrib = bufferGeometry.getAttribute(name);
+ if (attrib) {
+ newGeometry.addAttribute(name, attrib.clone());
+ }
+ }
+
+ const index = bufferGeometry.getIndex();
+ if (index) {
+ newGeometry.setIndex(index);
+ }
+
+ return newGeometry;
+ }
+
+ function addFlatGeometryIndices(geometry) {
+ const position = geometry.getAttribute('position');
+
+ if (!position) {
+ console.warn('No position attribute');
+ return;
+ }
+
+ const index = new Uint32Array(position.count);
+
+ for (let i = 0; i < index.length; i++) {
+ index[i] = i;
+ }
+
+ geometry.setIndex(new THREE$1.BufferAttribute(index, 1, false));
+
+ return geometry;
+ }
+
+ // 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])) {
+ left++;
+ if (left === right) {
+ return left;
+ }
+ }
+ do {
+ right--;
+ if (left === right) {
+ return left;
+ }
+ } while (!compare(array[right]));
+
+ swap(array, left, right);
+ left++;
+ }
+
+ return left;
+ }
+
+ // 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;
+ let minValue = array[i];
+ for (let j = i + 1; j < right; j++) {
+ if (!compare(minValue, array[j])) {
+ minIndex = j;
+ minValue = array[j];
+ swap(array, i, minIndex);
+ }
+ }
+ }
+ }
+
+ function swap(array, a, b) {
+ const x = array[b];
+ array[b] = array[a];
+ array[a] = x;
+ }
+
+ // Create a bounding volume hierarchy of scene geometry
+
+ const size = new THREE$1.Vector3();
+
+ function bvhAccel(geometry) {
+ const primitiveInfo = makePrimitiveInfo(geometry);
+ const node = recursiveBuild(primitiveInfo, 0, primitiveInfo.length);
+
+ return node;
+ }
+
+ function flattenBvh(bvh) {
+ const flat = [];
+ const isBounds = [];
+
+ const splitAxisMap = {
+ x: 0,
+ y: 1,
+ z: 2
+ };
+
+ let maxDepth = 1;
+ const traverse = (node, depth = 1) => {
+
+ maxDepth = Math.max(depth, maxDepth);
+
+ if (node.primitives) {
+ for (let i = 0; i < node.primitives.length; i++) {
+ const p = node.primitives[i];
+ flat.push(
+ p.indices[0], p.indices[1], p.indices[2], node.primitives.length,
+ p.faceNormal.x, p.faceNormal.y, p.faceNormal.z, p.materialIndex
+ );
+ isBounds.push(false);
+ }
+ } else {
+ const bounds = node.bounds;
+
+ flat.push(
+ bounds.min.x, bounds.min.y, bounds.min.z, splitAxisMap[node.splitAxis],
+ bounds.max.x, bounds.max.y, bounds.max.z, null // pointer to second shild
+ );
+
+ const i = flat.length - 1;
+ isBounds.push(true);
+
+ traverse(node.child0, depth + 1);
+ flat[i] = flat.length / 4; // pointer to second child
+ traverse(node.child1, depth + 1);
+ }
+ };
+
+ traverse(bvh);
+
+ const buffer = new ArrayBuffer(4 * flat.length);
+ const floatView = new Float32Array(buffer);
+ const intView = new Int32Array(buffer);
+
+ for (let i = 0; i < isBounds.length; i++) {
+ let k = 8 * i;
+
+ if (isBounds[i]) {
+ floatView[k] = flat[k];
+ floatView[k + 1] = flat[k + 1];
+ floatView[k + 2] = flat[k + 2];
+ intView[k + 3] = flat[k + 3];
+ } else {
+ intView[k] = flat[k];
+ intView[k + 1] = flat[k + 1];
+ intView[k + 2] = flat[k + 2];
+ intView[k + 3] = -flat[k + 3]; // negative signals to shader that this node is a triangle
+ }
+
+ floatView[k + 4] = flat[k + 4];
+ floatView[k + 5] = flat[k + 5];
+ floatView[k + 6] = flat[k + 6];
+ intView[k + 7] = flat[k + 7];
+ }
+
+ return {
+ maxDepth,
+ count: flat.length / 4,
+ buffer: floatView
+ };
+ }
+
+ function makePrimitiveInfo(geometry) {
+ const primitiveInfo = [];
+ const indices = geometry.getIndex().array;
+ const position = geometry.getAttribute('position');
+ const materialMeshIndex = geometry.getAttribute('materialMeshIndex');
+
+ const v0 = new THREE$1.Vector3();
+ const v1 = new THREE$1.Vector3();
+ const v2 = new THREE$1.Vector3();
+ const e0 = new THREE$1.Vector3();
+ const e1 = new THREE$1.Vector3();
+
+ for (let i = 0; i < indices.length; i += 3) {
+ const i0 = indices[i];
+ const i1 = indices[i + 1];
+ const i2 = indices[i + 2];
+
+ const bounds = new THREE$1.Box3();
+
+ v0.fromBufferAttribute(position, i0);
+ v1.fromBufferAttribute(position, i1);
+ v2.fromBufferAttribute(position, i2);
+ e0.subVectors(v2, v0);
+ e1.subVectors(v1, v0);
+
+ bounds.expandByPoint(v0);
+ bounds.expandByPoint(v1);
+ bounds.expandByPoint(v2);
+
+ const info = {
+ bounds: bounds,
+ center: bounds.getCenter(new THREE$1.Vector3()),
+ indices: [i0, i1, i2],
+ faceNormal: new THREE$1.Vector3().crossVectors(e1, e0).normalize(),
+ materialIndex: materialMeshIndex.getX(i0)
+ };
+
+ primitiveInfo.push(info);
+ }
+
+ return primitiveInfo;
+ }
+
+ function recursiveBuild(primitiveInfo, start, end) {
+ const bounds = new THREE$1.Box3();
+ for (let i = start; i < end; i++) {
+ bounds.union(primitiveInfo[i].bounds);
+ }
+
+ const nPrimitives = end - start;
+
+ if (nPrimitives === 1) {
+ return makeLeafNode(primitiveInfo.slice(start, end), bounds);
+ } else {
+ const centroidBounds = new THREE$1.Box3();
+ for (let i = start; i < end; i++) {
+ centroidBounds.expandByPoint(primitiveInfo[i].center);
+ }
+ const dim = maximumExtent(centroidBounds);
+
+ let mid = Math.floor((start + end) / 2);
+
+ // middle split method
+ // const dimMid = (centroidBounds.max[dim] + centroidBounds.min[dim]) / 2;
+ // mid = partition(primitiveInfo, p => p.center[dim] < dimMid, start, end);
+
+ // if (mid === start || mid === end) {
+ // mid = Math.floor((start + end) / 2);
+ // nthElement(primitiveInfo, (a, b) => a.center[dim] < b.center[dim], start, end, mid);
+ // }
+
+ // surface area heuristic method
+ if (nPrimitives <= 4) {
+ nthElement(primitiveInfo, (a, b) => a.center[dim] < b.center[dim], start, end, mid);
+ } else {
+ const buckets = [];
+ for (let i = 0; i < 12; i++) {
+ buckets.push({
+ bounds: new THREE$1.Box3(),
+ count: 0,
+ });
+ }
+
+ for (let i = start; i < end; i++) {
+ let b = Math.floor(buckets.length * boxOffset(centroidBounds, dim, primitiveInfo[i].center));
+ if (b === buckets.length) {
+ b = buckets.length - 1;
+ }
+ buckets[b].count++;
+ buckets[b].bounds.union(primitiveInfo[i].bounds);
+ }
+
+ const cost = [];
+
+ for (let i = 0; i < buckets.length - 1; i++) {
+ const b0 = new THREE$1.Box3();
+ const b1 = new THREE$1.Box3();
+ let count0 = 0;
+ let count1 = 0;
+ for (let j = 0; j <= i; j++) {
+ b0.union(buckets[j].bounds);
+ count0 += buckets[j].count;
+ }
+ for (let j = i + 1; j < buckets.length; j++) {
+ b1.union(buckets[j].bounds);
+ count1 += buckets[j].count;
+ }
+ cost.push(0.1 + (count0 * surfaceArea(b0) + count1 * surfaceArea(b1)) / surfaceArea(bounds));
+ }
+
+ let minCost = cost[0];
+ let minCostSplitBucket = 0;
+ for (let i = 1; i < cost.length; i++) {
+ if (cost[i] < minCost) {
+ minCost = cost[i];
+ minCostSplitBucket = i;
+ }
+ }
+
+ mid = partition(primitiveInfo, p => {
+ let b = Math.floor(buckets.length * boxOffset(centroidBounds, dim, p.center));
+ if (b === buckets.length) {
+ b = buckets.length - 1;
+ }
+ return b <= minCostSplitBucket;
+ }, start, end);
+ }
+
+ return makeInteriorNode(
+ dim,
+ recursiveBuild(primitiveInfo, start, mid),
+ recursiveBuild(primitiveInfo, mid, end),
+ );
+ }
+ }
+
+ function makeLeafNode(primitives, bounds) {
+ return {
+ primitives,
+ bounds
+ };
+ }
+
+ function makeInteriorNode(splitAxis, child0, child1) {
+ return {
+ child0,
+ child1,
+ bounds: new THREE$1.Box3().union(child0.bounds).union(child1.bounds),
+ splitAxis,
+ };
+ }
+
+ function maximumExtent(box3) {
+ box3.getSize(size);
+ if (size.x > size.z) {
+ return size.x > size.y ? 'x' : 'y';
+ } else {
+ return size.z > size.y ? 'z' : 'y';
+ }
+ }
+
+ function boxOffset(box3, dim, v) {
+ let offset = v[dim] - box3.min[dim];
+
+ if (box3.max[dim] > box3.min[dim]){
+ offset /= box3.max[dim] - box3.min[dim];
+ }
+
+ return offset;
+ }
+
+ function surfaceArea(box3) {
+ box3.getSize(size);
+ return 2 * (size.x * size.z + size.x * size.y + size.z * size.y);
+ }
+
+ // Convert image data from the RGBE format to a 32-bit floating point format
+ // See https://www.cg.tuwien.ac.at/research/theses/matkovic/node84.html for a description of the RGBE format
+ // Optional multiplier argument for performance optimization
+ function rgbeToFloat(buffer, intensity = 1) {
+ const texels = buffer.length / 4;
+ const floatBuffer = new Float32Array(texels * 3);
+
+ const expTable = [];
+ for (let i = 0; i < 255; i++) {
+ expTable[i] = intensity * Math.pow(2, i - 128) / 255;
+ }
+
+ for (let i = 0; i < texels; i++) {
+
+ const r = buffer[4 * i];
+ const g = buffer[4 * i + 1];
+ const b = buffer[4 * i + 2];
+ const a = buffer[4 * i + 3];
+ const e = expTable[a];
+
+ floatBuffer[3 * i] = r * e;
+ floatBuffer[3 * i + 1] = g * e;
+ floatBuffer[3 * i + 2] = b * e;
+ }
+
+ return floatBuffer;
+ }
+
+ // Convert image data from the RGBE format to a 32-bit floating point format
+
+ const DEFAULT_MAP_RESOLUTION = {
+ width: 2048,
+ height: 1024,
+ };
+
+ // Tools for generating and modify env maps for lighting from scene component data
+
+ function generateBackgroundMapFromSceneBackground(background) {
+ let backgroundImage;
+
+ if (background.isColor) {
+ backgroundImage = generateSolidMap(1, 1, background);
+ } else if (background.encoding === THREE$1.RGBEEncoding) {
+ backgroundImage = {
+ width: background.image.width,
+ height: background.image.height,
+ data: background.image.data,
+ };
+ backgroundImage.data = rgbeToFloat(backgroundImage.data);
+ }
+ return backgroundImage;
+ }
+
+ function generateEnvMapFromSceneComponents(directionalLights, ambientLights, environmentLights) {
+ let envImage = initializeEnvMap(environmentLights);
+ ambientLights.forEach( light => { addAmbientLightToEnvMap(light, envImage); });
+ directionalLights.forEach( light => { envImage.data = addDirectionalLightToEnvMap(light, envImage); });
+
+ return envImage;
+ }
+
+ function initializeEnvMap(environmentLights) {
+ let envImage;
+
+ // 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: environmentLight.map.image.width,
+ height: environmentLight.map.image.height,
+ data: environmentLight.map.image.data,
+ };
+ envImage.data = rgbeToFloat(envImage.data, environmentLight.intensity);
+ } else {
+ // initialize blank map
+ envImage = generateSolidMap(DEFAULT_MAP_RESOLUTION.width, DEFAULT_MAP_RESOLUTION.height);
+ }
+
+ return envImage;
+ }
+
+ function generateSolidMap(width, height, color, intensity) {
+ const texels = width * height;
+ const floatBuffer = new Float32Array(texels * 3);
+ if (color && color.isColor) {
+ setBufferToColor(floatBuffer, color, intensity);
+ }
+ return {
+ width: width,
+ height: height,
+ data: floatBuffer,
+ };
+ }
+
+ function setBufferToColor(buffer, color, intensity = 1) {
+ buffer.forEach(function(part, index) {
+ const component = index % 3;
+ if (component === 0) {
+ buffer[index] = color.r * intensity;
+ }
+ else if (component === 1) {
+ buffer[index] = color.g * intensity;
+ }
+ else if (component === 2) {
+ buffer[index] = color.b * intensity;
+ }
+ });
+ return buffer;
+ }
+
+ function addAmbientLightToEnvMap(light, image) {
+ const color = light.color;
+ image.data.forEach(function(part, index) {
+ const component = index % 3;
+ if (component === 0) {
+ image.data[index] += color.r * light.intensity;
+ }
+ else if (component === 1) {
+ image.data[index] += color.g * light.intensity;
+ }
+ else if (component === 2) {
+ image.data[index] += color.b * light.intensity;
+ }
+ });
+ }
+
+ 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 * 3 / 2) - sphericalCoords.theta;
+ sphericalCoords.makeSafe();
+
+ return addLightAtCoordinates(light, image, sphericalCoords);
+ }
+
+ // Perform modifications on env map to match input scene
+ function addLightAtCoordinates(light, image, originCoords) {
+ const floatBuffer = image.data;
+ const width = image.width;
+ const height = image.height;
+ const xTexels = floatBuffer.length / (3 * height);
+ const yTexels = floatBuffer.length / (3 * width);
+
+ // default softness for standard directional lights is 0.01, i.e. a hard shadow
+ const softness = light.softness || 0.01;
+
+ // angle from center of light at which no more contributions are projected
+ const threshold = findThreshold(softness);
+
+ // if too few texels are rejected by the threshold then the time to evaluate it is no longer worth it
+ const useThreshold = threshold < Math.PI / 5;
+
+ // functional trick to keep the conditional check out of the main loop
+ const intensityFromAngleFunction = useThreshold ? getIntensityFromAngleDifferentialThresholded : getIntensityFromAngleDifferential;
+
+ let begunAddingContributions = false;
+ let currentCoords = new THREE$1.Spherical();
+
+ // Iterates over each row from top to bottom
+ for (let i = 0; i < xTexels; i++) {
+
+ let encounteredInThisRow = false;
+
+ // Iterates over each texel in row
+ for (let j = 0; j < yTexels; j++) {
+ const bufferIndex = j * width + i;
+ currentCoords = equirectangularToSpherical(i, j, width, height, currentCoords);
+ const falloff = intensityFromAngleFunction(originCoords, currentCoords, softness, threshold);
+
+ if(falloff > 0) {
+ encounteredInThisRow = true;
+ begunAddingContributions = true;
+ }
+
+ const intensity = light.intensity * falloff;
+
+ floatBuffer[bufferIndex * 3] += intensity * light.color.r;
+ floatBuffer[bufferIndex * 3 + 1] += intensity * light.color.g;
+ floatBuffer[bufferIndex * 3 + 2] += intensity * light.color.b;
+ }
+
+ // First row to not add a contribution since adding began
+ // This means the entire light has been added and we can exit early
+ if(!encounteredInThisRow && begunAddingContributions) {
+ return floatBuffer;
+ }
+ }
+
+ return floatBuffer;
+ }
+
+ function findThreshold(softness) {
+ const step = Math.PI / 128;
+ const maxSteps = (2.0 * Math.PI) / step;
+
+ for (let i = 0; i < maxSteps; i++) {
+ const angle = i * step;
+ const falloff = getFalloffAtAngle(angle, softness);
+ if (falloff <= 0.0001) {
+ return angle;
+ }
+ }
+ }
+
+ function getIntensityFromAngleDifferentialThresholded(originCoords, currentCoords, softness, threshold) {
+ const deltaPhi = getAngleDelta(originCoords.phi, currentCoords.phi);
+ const deltaTheta = getAngleDelta(originCoords.theta, currentCoords.theta);
+
+ if(deltaTheta > threshold && deltaPhi > threshold) {
+ return 0;
+ }
+
+ const angle = angleBetweenSphericals(originCoords, currentCoords);
+ return getFalloffAtAngle(angle, softness);
+ }
+
+ function getIntensityFromAngleDifferential(originCoords, currentCoords, softness) {
+ const angle = angleBetweenSphericals(originCoords, currentCoords);
+ return getFalloffAtAngle(angle, softness);
+ }
+
+ function getAngleDelta(angleA, angleB) {
+ const diff = Math.abs(angleA - angleB) % (2 * Math.PI);
+ return diff > Math.PI ? (2 * Math.PI - diff) : diff;
+ }
+
+ const angleBetweenSphericals = function() {
+ const originVector = new THREE$1.Vector3();
+ const currentVector = new THREE$1.Vector3();
+
+ return (originCoords, currentCoords) => {
+ originVector.setFromSpherical(originCoords);
+ currentVector.setFromSpherical(currentCoords);
+ return originVector.angleTo(currentVector);
+ };
+ }();
+
+ // TODO: possibly clean this up and optimize it
+ //
+ // This function was arrived at through experimentation, it provides good
+ // looking results with percieved softness that scale relatively linearly with
+ // the softness value in the 0 - 1 range
+ //
+ // For now it doesn't incur too much of a performance penalty because for most of our use cases (lights without too much softness)
+ // the threshold cutoff in getIntensityFromAngleDifferential stops us from running it too many times
+ function getFalloffAtAngle(angle, softness) {
+ const softnessCoefficient = Math.pow(2, 14.5 * Math.max(0.001, 1.0 - clamp(softness, 0.0, 1.0)));
+ const falloff = Math.pow(softnessCoefficient, 1.1) * Math.pow(8, -softnessCoefficient * Math.pow(angle, 1.8));
+ return falloff;
+ }
+
+ function equirectangularToSpherical(x, y, width, height, target) {
+ target.phi = (Math.PI * y) / height;
+ target.theta = (2.0 * Math.PI * x) / width;
+ return target;
+ }
+
+ // Create a piecewise 2D cumulative distribution function of light intensity from an envmap
+ // http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations.html#Piecewise-Constant2DDistributions
+
+ function envmapDistribution(image) {
+ const data = image.data;
+
+ const cdfImage = {
+ width: image.width + 2,
+ height: image.height + 1
+ };
+
+ const cdf = makeTextureArray$1(cdfImage.width, cdfImage.height, 2);
+
+ for (let y = 0; y < image.height; y++) {
+ const sinTheta = Math.sin(Math.PI * (y + 0.5) / image.height);
+ for (let x = 0; x < image.width; x++) {
+ const i = 3 * (y * image.width + x);
+ let r = data[i];
+ let g = data[i + 1];
+ let b = data[i + 2];
+ let luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ luminance *= sinTheta;
+ cdf.set(x + 2, y, 0, cdf.get(x + 1, y, 0) + luminance / image.width);
+ cdf.set(x + 1, y, 1, luminance);
+ }
+
+ const rowIntegral = cdf.get(cdfImage.width - 1, y, 0);
+
+ for (let x = 1; x < cdf.width; x++) {
+ cdf.set(x, y, 0, cdf.get(x, y, 0) / rowIntegral);
+ cdf.set(x, y, 1, cdf.get(x, y, 1) / rowIntegral);
+ }
+
+ cdf.set(0, y + 1, 0, cdf.get(0, y, 0) + rowIntegral / image.height);
+ cdf.set(0, y, 1, rowIntegral);
+ }
+
+ const integral = cdf.get(0, cdf.height - 1, 0);
+
+ for (let y = 0; y < cdf.height; y++) {
+ cdf.set(0, y, 0, cdf.get(0, y, 0) / integral);
+ cdf.set(0, y, 1, cdf.get(0, y, 1) / integral);
+ }
+ cdfImage.data = cdf.array;
+
+ return cdfImage;
+ }
+
+
+ function makeTextureArray$1(width, height, channels) {
+ const array = new Float32Array(channels * width * height);
+
+ return {
+ set(x, y, channel, val) {
+ array[channels * (y * width + x) + channel] = val;
+ },
+ get(x, y, channel) {
+ return array[channels * (y * width + x) + channel];
+ },
+ width,
+ height,
+ channels,
+ array
+ };
+ }
+
+ function unrollLoop(indexName, start, limit, step, code) {
+ let unrolled = `int ${indexName};\n`;
+
+ for (let i = start; (step > 0 && i < limit) || (step < 0 && i > limit); i += step) {
+ unrolled += `${indexName} = ${i};\n`;
+ unrolled += code;
+ }
+
+ return unrolled;
+ }
+
+ var rayTraceCore = `
+ #define STANDARD 0
+ #define THIN_GLASS 1
+ #define THICK_GLASS 2
+ #define SHADOW_CATCHER 3
+
+ const float IOR = 1.5;
+ const float INV_IOR = 1.0 / IOR;
+
+ const float IOR_THIN = 1.015;
+ const float INV_IOR_THIN = 1.0 / IOR_THIN;
+
+ 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 RAY_MAX_DISTANCE 9999.0
+
+ struct Ray {
+ vec3 o;
+ vec3 d;
+ vec3 invD;
+ float tMax;
+ };
+
+ struct SurfaceInteraction {
+ bool hit;
+ vec3 position;
+ vec3 normal; // smoothed normal from the three triangle vertices
+ vec3 faceNormal; // normal of the triangle
+ vec3 color;
+ float roughness;
+ float metalness;
+ int materialType;
+ };
+
+ struct Camera {
+ mat4 transform;
+ float aspect;
+ float fov;
+ float focus;
+ float aperture;
+ };
+
+ void initRay(inout Ray ray, vec3 origin, vec3 direction) {
+ ray.o = origin;
+ ray.d = direction;
+ ray.invD = 1.0 / ray.d;
+ ray.tMax = RAY_MAX_DISTANCE;
+ }
+
+ // given the index from a 1D array, retrieve corresponding position from packed 2D texture
+ ivec2 unpackTexel(int i, int columnsLog2) {
+ ivec2 u;
+ u.y = i >> columnsLog2; // equivalent to (i / 2^columnsLog2)
+ u.x = i - (u.y << columnsLog2); // equivalent to (i % 2^columnsLog2)
+ return u;
+ }
+
+ vec4 fetchData(sampler2D s, int i, int columnsLog2) {
+ return texelFetch(s, unpackTexel(i, columnsLog2), 0);
+ }
+
+ ivec4 fetchData(isampler2D s, int i, int columnsLog2) {
+ return texelFetch(s, unpackTexel(i, columnsLog2), 0);
+ }
+
+ struct Path {
+ Ray ray;
+ vec3 li;
+ float alpha;
+ vec3 beta;
+ bool specularBounce;
+ bool abort;
+ float misWeight;
+ };
+
+ uniform Camera camera;
+ uniform vec2 pixelSize; // 1 / screenResolution
+ uniform vec2 jitter;
+
+ in vec2 vCoord;
+`;
+
+ // Manually performs linear filtering if the extension OES_texture_float_linear is not supported
+
+ var textureLinear = `
+vec4 textureLinear(sampler2D map, vec2 uv) {
+ #ifdef OES_texture_float_linear
+ return texture(map, uv);
+ #else
+ vec2 size = vec2(textureSize(map, 0));
+ vec2 texelSize = 1.0 / size;
+
+ uv = uv * size - 0.5;
+ vec2 f = fract(uv);
+ uv = floor(uv) + 0.5;
+
+ vec4 s1 = texture(map, (uv + vec2(0, 0)) * texelSize);
+ vec4 s2 = texture(map, (uv + vec2(1, 0)) * texelSize);
+ vec4 s3 = texture(map, (uv + vec2(0, 1)) * texelSize);
+ vec4 s4 = texture(map, (uv + vec2(1, 1)) * texelSize);
+
+ return mix(mix(s1, s2, f.x), mix(s3, s4, f.x), f.y);
+ #endif
+}
+`;
+
+ var intersect = `
+
+uniform sampler2D positions;
+uniform sampler2D normals;
+uniform sampler2D uvs;
+uniform sampler2D bvh;
+
+struct Triangle {
+ vec3 p0;
+ vec3 p1;
+ vec3 p2;
+};
+
+void surfaceInteractionFromBVH(inout SurfaceInteraction si, Triangle tri, vec3 barycentric, ivec3 index, vec3 faceNormal, int materialIndex) {
+ si.hit = true;
+ si.faceNormal = faceNormal;
+ si.position = barycentric.x * tri.p0 + barycentric.y * tri.p1 + barycentric.z * tri.p2;
+ ivec2 i0 = unpackTexel(index.x, VERTEX_COLUMNS);
+ ivec2 i1 = unpackTexel(index.y, VERTEX_COLUMNS);
+ ivec2 i2 = unpackTexel(index.z, VERTEX_COLUMNS);
+
+ vec3 n0 = texelFetch(normals, i0, 0).xyz;
+ vec3 n1 = texelFetch(normals, i1, 0).xyz;
+ vec3 n2 = texelFetch(normals, i2, 0).xyz;
+ vec3 normal = normalize(barycentric.x * n0 + barycentric.y * n1 + barycentric.z * n2);
+
+ #if defined(NUM_DIFFUSE_MAPS) || defined(NUM_NORMAL_MAPS) || defined(NUM_PBR_MAPS)
+ vec2 uv0 = texelFetch(uvs, i0, 0).xy;
+ vec2 uv1 = texelFetch(uvs, i1, 0).xy;
+ vec2 uv2 = texelFetch(uvs, i2, 0).xy;
+ vec2 uv = fract(barycentric.x * uv0 + barycentric.y * uv1 + barycentric.z * uv2);
+ #else
+ vec2 uv = vec2(0.0);
+ #endif
+
+ si.materialType = int(getMatType(materialIndex));
+ si.color = getMatColor(materialIndex, uv);
+ si.roughness = getMatRoughness(materialIndex, uv);
+ si.metalness = getMatMetalness(materialIndex, uv);
+
+ #ifdef NUM_NORMAL_MAPS
+ vec3 dp1 = tri.p0 - tri.p2;
+ vec3 dp2 = tri.p1 - tri.p2;
+ vec2 duv1 = uv0 - uv2;
+ vec2 duv2 = uv1 - uv2;
+ si.normal = getMatNormal(materialIndex, uv, normal, dp1, dp2, duv1, duv2);
+ #else
+ si.normal = normal;
+ #endif
+}
+
+struct TriangleIntersect {
+ float t;
+ vec3 barycentric;
+};
+
+// Triangle-ray intersection
+// Faster than the classic Möller–Trumbore intersection algorithm
+// http://www.pbr-book.org/3ed-2018/Shapes/Triangle_Meshes.html#TriangleIntersection
+TriangleIntersect intersectTriangle(Ray r, Triangle tri, int maxDim, vec3 shear) {
+ TriangleIntersect ti;
+ vec3 d = r.d;
+
+ // translate vertices based on ray origin
+ vec3 p0t = tri.p0 - r.o;
+ vec3 p1t = tri.p1 - r.o;
+ vec3 p2t = tri.p2 - r.o;
+
+ // permute components of triangle vertices
+ if (maxDim == 0) {
+ p0t = p0t.yzx;
+ p1t = p1t.yzx;
+ p2t = p2t.yzx;
+ } else if (maxDim == 1) {
+ p0t = p0t.zxy;
+ p1t = p1t.zxy;
+ p2t = p2t.zxy;
+ }
+
+ // apply shear transformation to translated vertex positions
+ p0t.xy += shear.xy * p0t.z;
+ p1t.xy += shear.xy * p1t.z;
+ p2t.xy += shear.xy * p2t.z;
+
+ // compute edge function coefficients
+ vec3 e = vec3(
+ p1t.x * p2t.y - p1t.y * p2t.x,
+ p2t.x * p0t.y - p2t.y * p0t.x,
+ p0t.x * p1t.y - p0t.y * p1t.x
+ );
+
+ // check if intersection is inside triangle
+ if (any(lessThan(e, vec3(0))) && any(greaterThan(e, vec3(0)))) {
+ return ti;
+ }
+
+ float det = e.x + e.y + e.z;
+
+ // not needed?
+ // if (det == 0.) {
+ // return ti;
+ // }
+
+ p0t.z *= shear.z;
+ p1t.z *= shear.z;
+ p2t.z *= shear.z;
+ float tScaled = (e.x * p0t.z + e.y * p1t.z + e.z * p2t.z);
+
+ // not needed?
+ // if (sign(det) != sign(tScaled)) {
+ // return ti;
+ // }
+
+ // check if closer intersection already exists
+ if (abs(tScaled) > abs(r.tMax * det)) {
+ return ti;
+ }
+
+ float invDet = 1. / det;
+ ti.t = tScaled * invDet;
+ ti.barycentric = e * invDet;
+
+ return ti;
+}
+
+struct Box {
+ vec3 min;
+ vec3 max;
+};
+
+// Branchless ray/box intersection
+// https://tavianator.com/fast-branchless-raybounding-box-intersections/
+float intersectBox(Ray r, Box b) {
+ vec3 tBot = (b.min - r.o) * r.invD;
+ vec3 tTop = (b.max - r.o) * r.invD;
+ vec3 tNear = min(tBot, tTop);
+ vec3 tFar = max(tBot, tTop);
+ float t0 = max(tNear.x, max(tNear.y, tNear.z));
+ float t1 = min(tFar.x, min(tFar.y, tFar.z));
+
+ return (t0 > t1 || t0 > r.tMax) ? -1.0 : (t0 > 0.0 ? t0 : t1);
+}
+
+int maxDimension(vec3 v) {
+ return v.x > v.y ? (v.x > v.z ? 0 : 2) : (v.y > v.z ? 1 : 2);
+}
+
+// Traverse BVH, find closest triangle intersection, and return surface information
+void intersectScene(inout Ray ray, inout SurfaceInteraction si) {
+ si.hit = false;
+
+ int maxDim = maxDimension(abs(ray.d));
+
+ // Permute space so that the z dimension is the one where the absolute value of the ray's direction is largest.
+ // Then create a shear transformation that aligns ray direction with the +z axis
+ vec3 shear;
+ if (maxDim == 0) {
+ shear = vec3(-ray.d.y, -ray.d.z, 1.0) * ray.invD.x;
+ } else if (maxDim == 1) {
+ shear = vec3(-ray.d.z, -ray.d.x, 1.0) * ray.invD.y;
+ } else {
+ shear = vec3(-ray.d.x, -ray.d.y, 1.0) * ray.invD.z;
+ }
+
+ int nodesToVisit[STACK_SIZE];
+ int stack = 0;
+
+ nodesToVisit[0] = 0;
+
+ while(stack >= 0) {
+ int i = nodesToVisit[stack--];
+
+ vec4 r1 = fetchData(bvh, i, BVH_COLUMNS);
+ vec4 r2 = fetchData(bvh, i + 1, BVH_COLUMNS);
+
+ int splitAxisOrNumPrimitives = floatBitsToInt(r1.w);
+
+ if (splitAxisOrNumPrimitives >= 0) {
+ // Intersection is a bounding box. Test for box intersection and keep traversing BVH
+ int splitAxis = splitAxisOrNumPrimitives;
+
+ Box bbox = Box(r1.xyz, r2.xyz);
+
+ if (intersectBox(ray, bbox) > 0.0) {
+ // traverse near node to ray first, and far node to ray last
+ if (ray.d[splitAxis] > 0.0) {
+ nodesToVisit[++stack] = floatBitsToInt(r2.w);
+ nodesToVisit[++stack] = i + 2;
+ } else {
+ nodesToVisit[++stack] = i + 2;
+ nodesToVisit[++stack] = floatBitsToInt(r2.w);
+ }
+ }
+ } else {
+ ivec3 index = floatBitsToInt(r1.xyz);
+ Triangle tri = Triangle(
+ fetchData(positions, index.x, VERTEX_COLUMNS).xyz,
+ fetchData(positions, index.y, VERTEX_COLUMNS).xyz,
+ fetchData(positions, index.z, VERTEX_COLUMNS).xyz
+ );
+ TriangleIntersect hit = intersectTriangle(ray, tri, maxDim, shear);
+
+ if (hit.t > 0.0) {
+ ray.tMax = hit.t;
+ int materialIndex = floatBitsToInt(r2.w);
+ vec3 faceNormal = r2.xyz;
+ surfaceInteractionFromBVH(si, tri, hit.barycentric, index, faceNormal, materialIndex);
+ }
+ }
+ }
+
+ // Values must be clamped outside of intersection loop. Clamping inside the loop produces incorrect numbers on some devices.
+ si.roughness = clamp(si.roughness, ROUGHNESS_MIN, 1.0);
+ si.metalness = clamp(si.metalness, 0.0, 1.0);
+}
+
+bool intersectSceneShadow(inout Ray ray) {
+ int maxDim = maxDimension(abs(ray.d));
+
+ // Permute space so that the z dimension is the one where the absolute value of the ray's direction is largest.
+ // Then create a shear transformation that aligns ray direction with the +z axis
+ vec3 shear;
+ if (maxDim == 0) {
+ shear = vec3(-ray.d.y, -ray.d.z, 1.0) * ray.invD.x;
+ } else if (maxDim == 1) {
+ shear = vec3(-ray.d.z, -ray.d.x, 1.0) * ray.invD.y;
+ } else {
+ shear = vec3(-ray.d.x, -ray.d.y, 1.0) * ray.invD.z;
+ }
+
+ int nodesToVisit[STACK_SIZE];
+ int stack = 0;
+
+ nodesToVisit[0] = 0;
+
+ while(stack >= 0) {
+ int i = nodesToVisit[stack--];
+
+ vec4 r1 = fetchData(bvh, i, BVH_COLUMNS);
+ vec4 r2 = fetchData(bvh, i + 1, BVH_COLUMNS);
+
+ int splitAxisOrNumPrimitives = floatBitsToInt(r1.w);
+
+ if (splitAxisOrNumPrimitives >= 0) {
+ int splitAxis = splitAxisOrNumPrimitives;
+
+ Box bbox = Box(r1.xyz, r2.xyz);
+
+ if (intersectBox(ray, bbox) > 0.0) {
+ if (ray.d[splitAxis] > 0.0) {
+ nodesToVisit[++stack] = floatBitsToInt(r2.w);
+ nodesToVisit[++stack] = i + 2;
+ } else {
+ nodesToVisit[++stack] = i + 2;
+ nodesToVisit[++stack] = floatBitsToInt(r2.w);
+ }
+ }
+ } else {
+ ivec3 index = floatBitsToInt(r1.xyz);
+ Triangle tri = Triangle(
+ fetchData(positions, index.x, VERTEX_COLUMNS).xyz,
+ fetchData(positions, index.y, VERTEX_COLUMNS).xyz,
+ fetchData(positions, index.z, VERTEX_COLUMNS).xyz
+ );
+
+ if (intersectTriangle(ray, tri, maxDim, shear).t > 0.0) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+`;
+
+ var surfaceInteractionDirect = `
+
+ uniform sampler2D gPosition;
+ uniform sampler2D gNormal;
+ uniform sampler2D gFaceNormal;
+ uniform sampler2D gColor;
+ uniform sampler2D gMatProps;
+
+ void surfaceInteractionDirect(vec2 coord, inout SurfaceInteraction si) {
+ vec4 positionAndMeshIndex = texture(gPosition, coord);
+
+ si.position = positionAndMeshIndex.xyz;
+
+ float meshIndex = positionAndMeshIndex.w;
+
+ vec4 normalMaterialType = texture(gNormal, coord);
+
+ si.normal = normalize(normalMaterialType.xyz);
+ si.materialType = int(normalMaterialType.w);
+
+ si.faceNormal = normalize(texture(gFaceNormal, coord).xyz);
+
+ si.color = texture(gColor, coord).rgb;
+
+ vec4 matProps = texture(gMatProps, coord);
+ si.roughness = matProps.x;
+ si.metalness = matProps.y;
+
+ si.hit = meshIndex > 0.0 ? true : false;
+ }
+`;
+
+ var random = `
+
+// 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;
+
+// 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;
+
+float pixelSeed;
+
+void initRandom() {
+ vec2 noiseSize = vec2(textureSize(noise, 0));
+
+ // tile the small noise texture across the entire screen
+ pixelSeed = texture(noise, vCoord / (pixelSize * noiseSize)).r;
+}
+
+float randomSample() {
+ float stratifiedSample = stratifiedSamples[sampleIndex++];
+
+ float random = fract((stratifiedSample + pixelSeed) * strataSize); // blue noise + stratified samples
+
+ // transform random number between [0, 1] to (0, 1)
+ return EPS + (1.0 - 2.0 * EPS) * random;
+}
+
+vec2 randomSampleVec2() {
+ return vec2(randomSample(), randomSample());
+}
+
+struct MaterialSamples {
+ vec2 s1;
+ vec2 s2;
+ vec2 s3;
+};
+
+MaterialSamples getRandomMaterialSamples() {
+ MaterialSamples samples;
+
+ samples.s1 = randomSampleVec2();
+ samples.s2 = randomSampleVec2();
+ samples.s3 = randomSampleVec2();
+
+ return samples;
+}
+`;
+
+ // Sample the environment map using a cumulative distribution function as described in
+ // http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Sampling_Light_Sources.html#InfiniteAreaLights
+
+ var envmap = `
+
+uniform sampler2D envmap;
+uniform sampler2D envmapDistribution;
+uniform sampler2D backgroundMap;
+
+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);
+
+ int left = 0;
+ int right = size.y + 1; // cdf length is the length of the envmap + 1
+ while (left < right) {
+ int mid = (left + right) >> 1;
+ float s = texelFetch(envmapDistribution, ivec2(0, mid), 0).x;
+ if (s <= u) {
+ left = mid + 1;
+ } else {
+ right = mid;
+ }
+ }
+ vOffset = left - 1;
+
+ // x channel is cumulative distribution of envmap luminance
+ // y channel is partial probability density of envmap luminance
+ vec2 s0 = texelFetch(envmapDistribution, ivec2(0, vOffset), 0).xy;
+ vec2 s1 = texelFetch(envmapDistribution, ivec2(0, vOffset + 1), 0).xy;
+
+ pdf = s0.y;
+
+ return (float(vOffset) + (u - s0.x) / (s1.x - s0.x)) / float(size.y);
+}
+
+float getEnvmapU(float u, int vOffset, out float pdf) {
+ ivec2 size = textureSize(envmap, 0);
+
+ int left = 0;
+ int right = size.x + 1; // cdf length is the length of the envmap + 1
+ while (left < right) {
+ int mid = (left + right) >> 1;
+ float s = texelFetch(envmapDistribution, ivec2(1 + mid, vOffset), 0).x;
+ if (s <= u) {
+ left = mid + 1;
+ } else {
+ right = mid;
+ }
+ }
+ int uOffset = left - 1;
+
+ // x channel is cumulative distribution of envmap luminance
+ // y channel is partial probability density of envmap luminance
+ vec2 s0 = texelFetch(envmapDistribution, ivec2(1 + uOffset, vOffset), 0).xy;
+ vec2 s1 = texelFetch(envmapDistribution, ivec2(1 + uOffset + 1, vOffset), 0).xy;
+
+ pdf = s0.y;
+
+ return (float(uOffset) + (u - s0.x) / (s1.x - s0.x)) / float(size.x);
+}
+
+// Perform two binary searches to find light direction.
+vec3 sampleEnvmap(vec2 random, out vec2 uv, out float pdf) {
+ vec2 partialPdf;
+ int vOffset;
+
+ uv.y = getEnvmapV(random.x, vOffset, partialPdf.y);
+ uv.x = getEnvmapU(random.y, vOffset, partialPdf.x);
+
+ float phi = uv.x * TWOPI;
+ float theta = uv.y * PI;
+ float cosTheta = cos(theta);
+ float sinTheta = sin(theta);
+ float cosPhi = cos(phi);
+ float sinPhi = sin(phi);
+
+ vec3 dir = vec3(-sinTheta * cosPhi, cosTheta, -sinTheta * sinPhi);
+
+ pdf = partialPdf.x * partialPdf.y * INVPI2 / (2.0 * sinTheta);
+
+ return dir;
+}
+
+float envmapPdf(vec2 uv) {
+ vec2 size = vec2(textureSize(envmap, 0));
+
+ float sinTheta = sin(uv.y * PI);
+
+ uv *= size;
+
+ float partialX = texelFetch(envmapDistribution, ivec2(1.0 + uv.x, uv.y), 0).y;
+ float partialY = texelFetch(envmapDistribution, ivec2(0, uv.y), 0).y;
+
+ return partialX * partialY * INVPI2 / (2.0 * sinTheta);
+}
+
+vec3 sampleEnvmapFromDirection(vec3 d) {
+ vec2 uv = cartesianToEquirect(d);
+ return textureLinear(envmap, uv).rgb;
+}
+
+vec3 sampleBackgroundFromDirection(vec3 d) {
+ vec2 uv = cartesianToEquirect(d);
+ return textureLinear(backgroundMap, uv).rgb;
+}
+
+`;
+
+ var bsdf = `
+
+// Computes the exact value of the Fresnel factor
+// https://seblagarde.wordpress.com/2013/04/29/memo-on-fresnel-equations/
+float fresnel(float cosTheta, float eta, float invEta) {
+ eta = cosTheta > 0.0 ? eta : invEta;
+ cosTheta = abs(cosTheta);
+
+ float gSquared = eta * eta + cosTheta * cosTheta - 1.0;
+
+ if (gSquared < 0.0) {
+ return 1.0;
+ }
+
+ float g = sqrt(gSquared);
+
+ float a = (g - cosTheta) / (g + cosTheta);
+ float b = (cosTheta * (g + cosTheta) - 1.0) / (cosTheta * (g - cosTheta) + 1.0);
+
+ return 0.5 * a * a * (1.0 + b * b);
+}
+
+float fresnelSchlickWeight(float cosTheta) {
+ float w = 1.0 - cosTheta;
+ return (w * w) * (w * w) * w;
+}
+
+// Computes Schlick's approximation of the Fresnel factor
+// Assumes ray is moving from a less dense to a more dense medium
+float fresnelSchlick(float cosTheta, float r0) {
+ return mix(fresnelSchlickWeight(cosTheta), 1.0, r0);
+}
+
+// Computes Schlick's approximation of Fresnel factor
+// Accounts for total internal reflection if ray is moving from a more dense to a less dense medium
+float fresnelSchlickTIR(float cosTheta, float r0, float ni) {
+
+ // moving from a more dense to a less dense medium
+ if (cosTheta < 0.0) {
+ float inv_eta = ni;
+ float SinT2 = inv_eta * inv_eta * (1.0f - cosTheta * cosTheta);
+ if (SinT2 > 1.0) {
+ return 1.0; // total internal reflection
+ }
+ cosTheta = sqrt(1.0f - SinT2);
+ }
+
+ return mix(fresnelSchlickWeight(cosTheta), 1.0, r0);
+}
+
+float trowbridgeReitzD(float cosTheta, float alpha2) {
+ float e = cosTheta * cosTheta * (alpha2 - 1.0) + 1.0;
+ return alpha2 / (PI * e * e);
+}
+
+float trowbridgeReitzLambda(float cosTheta, float alpha2) {
+ float cos2Theta = cosTheta * cosTheta;
+ float tan2Theta = (1.0 - cos2Theta) / cos2Theta;
+ return 0.5 * (-1.0 + sqrt(1.0 + alpha2 * tan2Theta));
+}
+
+// An implementation of Disney's principled BRDF
+// https://disney-animation.s3.amazonaws.com/library/s2012_pbs_disney_brdf_notes_v2.pdf
+vec3 materialBrdf(SurfaceInteraction si, vec3 viewDir, vec3 lightDir, float cosThetaL, float diffuseWeight, out float pdf) {
+ vec3 halfVector = normalize(viewDir + lightDir);
+
+ cosThetaL = abs(cosThetaL);
+ float cosThetaV = abs(dot(si.normal, viewDir));
+ float cosThetaH = abs(dot(si.normal, halfVector));
+ float cosThetaD = abs(dot(lightDir, halfVector));
+
+ float alpha2 = (si.roughness * si.roughness) * (si.roughness * si.roughness);
+
+ float F = fresnelSchlick(cosThetaD, mix(R0, 0.6, si.metalness));
+ float D = trowbridgeReitzD(cosThetaH, alpha2);
+
+ float roughnessRemapped = 0.5 + 0.5 * si.roughness;
+ float alpha2Remapped = (roughnessRemapped * roughnessRemapped) * (roughnessRemapped * roughnessRemapped);
+
+ float G = 1.0 / (1.0 + trowbridgeReitzLambda(cosThetaV, alpha2Remapped) + trowbridgeReitzLambda(cosThetaL, alpha2Remapped));
+
+ float specular = F * D * G / (4.0 * cosThetaV * cosThetaL);
+ float specularPdf = D * cosThetaH / (4.0 * cosThetaD);
+
+ float f = -0.5 + 2.0 * cosThetaD * cosThetaD * si.roughness;
+ float diffuse = diffuseWeight * INVPI * (1.0 + f * fresnelSchlickWeight(cosThetaL)) * (1.0 + f * fresnelSchlickWeight(cosThetaV));
+ float diffusePdf = cosThetaL * INVPI;
+
+ pdf = mix(0.5 * (specularPdf + diffusePdf), specularPdf, si.metalness);
+
+ return mix(si.color * diffuse + specular, si.color * specular, si.metalness);
+}
+
+`;
+
+ var sample = `
+
+// https://graphics.pixar.com/library/OrthonormalB/paper.pdf
+mat3 orthonormalBasis(vec3 n) {
+ float zsign = n.z >= 0.0 ? 1.0 : -1.0;
+ float a = -1.0 / (zsign + n.z);
+ float b = n.x * n.y * a;
+ vec3 s = vec3(1.0 + zsign * n.x * n.x * a, zsign * b, -zsign * n.x);
+ vec3 t = vec3(b, zsign + n.y * n.y * a, -n.y);
+ return mat3(s, t, n);
+}
+
+// http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations.html#SamplingaUnitDisk
+vec2 sampleCircle(vec2 p) {
+ p = 2.0 * p - 1.0;
+
+ bool greater = abs(p.x) > abs(p.y);
+
+ float r = greater ? p.x : p.y;
+ float theta = greater ? 0.25 * PI * p.y / p.x : PI * (0.5 - 0.25 * p.x / p.y);
+
+ return r * vec2(cos(theta), sin(theta));
+}
+
+// http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations.html#Cosine-WeightedHemisphereSampling
+vec3 cosineSampleHemisphere(vec2 p) {
+ vec2 h = sampleCircle(p);
+ float z = sqrt(max(0.0, 1.0 - h.x * h.x - h.y * h.y));
+ return vec3(h, z);
+}
+
+
+// http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Sampling_Reflection_Functions.html#MicrofacetBxDFs
+// Instead of Beckmann distrubtion, we use the GTR2 (GGX) distrubtion as covered in Disney's Principled BRDF paper
+vec3 lightDirSpecular(vec3 faceNormal, vec3 viewDir, mat3 basis, float roughness, vec2 random) {
+ float phi = TWOPI * random.y;
+ float alpha = roughness * roughness;
+ float cosTheta = sqrt((1.0 - random.x) / (1.0 + (alpha * alpha - 1.0) * random.x));
+ float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
+
+ vec3 halfVector = basis * sign(dot(faceNormal, viewDir)) * vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
+
+ vec3 lightDir = reflect(-viewDir, halfVector);
+
+ return lightDir;
+}
+
+vec3 lightDirDiffuse(vec3 faceNormal, vec3 viewDir, mat3 basis, vec2 random) {
+ return basis * sign(dot(faceNormal, viewDir)) * cosineSampleHemisphere(random);
+}
+
+float powerHeuristic(float f, float g) {
+ return (f * f) / (f * f + g * g);
+}
+
+`;
+
+ // Estimate the direct lighting integral using multiple importance sampling
+ // http://www.pbr-book.org/3ed-2018/Light_Transport_I_Surface_Reflection/Direct_Lighting.html#EstimatingtheDirectLightingIntegral
+
+ var sampleMaterial = `
+
+void sampleMaterial(SurfaceInteraction si, int bounce, inout Path path) {
+ bool lastBounce = bounce == BOUNCES;
+ mat3 basis = orthonormalBasis(si.normal);
+ vec3 viewDir = -path.ray.d;
+
+ MaterialSamples samples = getRandomMaterialSamples();
+
+ vec2 diffuseOrSpecular = samples.s1;
+ vec2 lightDirSample = samples.s2;
+ vec2 bounceDirSample = samples.s3;
+
+ // Step 1: Add direct illumination of the light source (the hdr map)
+ // On every bounce but the last, importance sample the light source
+ // On the last bounce, multiple importance sample the brdf AND the light source, determined by random var
+
+ vec3 lightDir;
+ vec2 uv;
+ float lightPdf;
+ bool brdfSample = false;
+
+ if (lastBounce && diffuseOrSpecular.x < 0.5) {
+ // reuse this sample by multiplying by 2 to bring sample from [0, 0.5), to [0, 1)
+ lightDir = 2.0 * diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ?
+ lightDirDiffuse(si.faceNormal, viewDir, basis, lightDirSample) :
+ lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, lightDirSample);
+
+ uv = cartesianToEquirect(lightDir);
+ lightPdf = envmapPdf(uv);
+ brdfSample = true;
+ } else {
+ lightDir = sampleEnvmap(lightDirSample, uv, lightPdf);
+ }
+
+ float cosThetaL = dot(si.normal, lightDir);
+
+ float occluded = 1.0;
+
+ float orientation = dot(si.faceNormal, viewDir) * cosThetaL;
+ if (orientation < 0.0) {
+ // light dir points towards surface. invalid dir.
+ occluded = 0.0;
+ }
+
+ float diffuseWeight = 1.0;
+
+ initRay(path.ray, si.position + EPS * lightDir, lightDir);
+ if (intersectSceneShadow(path.ray)) {
+ if (lastBounce) {
+ diffuseWeight = 0.0;
+ } else {
+ occluded = 0.0;
+ }
+ }
+
+ vec3 irr = textureLinear(envmap, uv).rgb;
+
+ float scatteringPdf;
+ vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, diffuseWeight, scatteringPdf);
+
+ float weight;
+ if (lastBounce) {
+ weight = brdfSample ?
+ 2.0 * powerHeuristic(scatteringPdf, lightPdf) / scatteringPdf :
+ 2.0 * powerHeuristic(lightPdf, scatteringPdf) / lightPdf;
+ } else {
+ weight = powerHeuristic(lightPdf, scatteringPdf) / lightPdf;
+ }
+
+ path.li += path.beta * occluded * brdf * irr * abs(cosThetaL) * weight;;
+
+ // Step 2: Setup ray direction for next bounce by importance sampling the BRDF
+
+ if (lastBounce) {
+ return;
+ }
+
+ lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ?
+ lightDirDiffuse(si.faceNormal, viewDir, basis, bounceDirSample) :
+ lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, bounceDirSample);
+
+ cosThetaL = dot(si.normal, lightDir);
+
+ orientation = dot(si.faceNormal, viewDir) * cosThetaL;
+ path.abort = orientation < 0.0;
+
+ if (path.abort) {
+ return;
+ }
+
+ brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf);
+
+ uv = cartesianToEquirect(lightDir);
+ lightPdf = envmapPdf(uv);
+
+ path.misWeight = powerHeuristic(scatteringPdf, lightPdf);
+
+ path.beta *= abs(cosThetaL) * brdf / scatteringPdf;
+
+ path.specularBounce = false;
+
+ initRay(path.ray, si.position + EPS * lightDir, lightDir);
+}
+`;
+
+ var sampleShadowCatcher = `
+
+#ifdef USE_SHADOW_CATCHER
+
+void sampleShadowCatcher(SurfaceInteraction si, int bounce, inout Path path) {
+ bool lastBounce = bounce == BOUNCES;
+ mat3 basis = orthonormalBasis(si.normal);
+ vec3 viewDir = -path.ray.d;
+ vec3 color = bounce == 1 || path.specularBounce ? sampleBackgroundFromDirection(-viewDir) : sampleEnvmapFromDirection(-viewDir);
+
+ si.color = vec3(1, 1, 1);
+
+ MaterialSamples samples = getRandomMaterialSamples();
+
+ vec2 diffuseOrSpecular = samples.s1;
+ vec2 lightDirSample = samples.s2;
+ vec2 bounceDirSample = samples.s3;
+
+ vec3 lightDir;
+ vec2 uv;
+ float lightPdf;
+ bool brdfSample = false;
+
+ if (diffuseOrSpecular.x < 0.5) {
+ lightDir = 2.0 * diffuseOrSpecular.x < mix(0.5, 0.0, si.metalness) ?
+ lightDirDiffuse(si.faceNormal, viewDir, basis, lightDirSample) :
+ lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, lightDirSample);
+ uv = cartesianToEquirect(lightDir);
+ lightPdf = envmapPdf(uv);
+ brdfSample = true;
+ } else {
+ lightDir = sampleEnvmap(lightDirSample, uv, lightPdf);
+ }
+
+ float cosThetaL = dot(si.normal, lightDir);
+
+ float liContrib = 1.0;
+
+ float orientation = dot(si.faceNormal, viewDir) * cosThetaL;
+ if (orientation < 0.0) {
+ liContrib = 0.0;
+ }
+
+ float occluded = 1.0;
+ initRay(path.ray, si.position + EPS * lightDir, lightDir);
+ if (intersectSceneShadow(path.ray)) {
+ occluded = 0.0;
+ }
+
+ float irr = dot(luminance, textureLinear(envmap, uv).rgb);
+
+ float scatteringPdf;
+ vec3 brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf);
+
+ float weight = brdfSample ?
+ 2.0 * powerHeuristic(scatteringPdf, lightPdf) / scatteringPdf :
+ 2.0 * powerHeuristic(lightPdf, scatteringPdf) / lightPdf;
+
+ float liEq = liContrib * brdf.r * irr * abs(cosThetaL) * weight;
+
+ float alpha = liEq;
+ path.alpha *= alpha;
+ path.li *= alpha;
+
+ path.li += occluded * path.beta * color * liEq;
+
+ if (lastBounce) {
+ return;
+ }
+
+ lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ?
+ lightDirDiffuse(si.faceNormal, viewDir, basis, bounceDirSample) :
+ lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, bounceDirSample);
+
+ cosThetaL = dot(si.normal, lightDir);
+
+ orientation = dot(si.faceNormal, viewDir) * cosThetaL;
+ path.abort = orientation < 0.0;
+
+ if (path.abort) {
+ return;
+ }
+
+ brdf = materialBrdf(si, viewDir, lightDir, cosThetaL, 1.0, scatteringPdf);
+
+ uv = cartesianToEquirect(lightDir);
+ lightPdf = envmapPdf(uv);
+
+ path.misWeight = 0.0;
+
+ path.beta = color * abs(cosThetaL) * brdf.r / scatteringPdf;
+
+ path.specularBounce = false;
+
+ initRay(path.ray, si.position + EPS * lightDir, lightDir);
+}
+
+#endif
+
+`;
+
+ var sampleGlass = `
+
+#ifdef USE_GLASS
+
+void sampleGlassSpecular(SurfaceInteraction si, int bounce, inout Path path) {
+ bool lastBounce = bounce == BOUNCES;
+ vec3 viewDir = -path.ray.d;
+ float cosTheta = dot(si.normal, viewDir);
+
+ MaterialSamples samples = getRandomMaterialSamples();
+
+ float reflectionOrRefraction = samples.s1.x;
+
+ float F = si.materialType == THIN_GLASS ?
+ fresnelSchlick(abs(cosTheta), R0) : // thin glass
+ fresnelSchlickTIR(cosTheta, R0, IOR); // thick glass
+
+ vec3 lightDir;
+
+ if (reflectionOrRefraction < F) {
+ lightDir = reflect(-viewDir, si.normal);
+ } else {
+ lightDir = si.materialType == THIN_GLASS ?
+ refract(-viewDir, sign(cosTheta) * si.normal, INV_IOR_THIN) : // thin glass
+ refract(-viewDir, sign(cosTheta) * si.normal, cosTheta < 0.0 ? IOR : INV_IOR); // thick glass
+ path.beta *= si.color;
+ }
+
+ path.misWeight = 1.0;
+
+ initRay(path.ray, si.position + EPS * lightDir, lightDir);
+
+ path.li += lastBounce ? path.beta * sampleBackgroundFromDirection(lightDir) : vec3(0.0);
+
+ path.specularBounce = true;
+}
+
+#endif
+
+`;
+
+ var fragment$1 = {
+ includes: [
+ constants$1,
+ rayTraceCore,
+ textureLinear,
+ materialBuffer,
+ intersect,
+ surfaceInteractionDirect,
+ random,
+ envmap,
+ bsdf,
+ sample,
+ sampleMaterial,
+ sampleGlass,
+ sampleShadowCatcher,
+ ],
+ outputs: ['light'],
+ source: (defines) => `
+ void bounce(inout Path path, int i, inout SurfaceInteraction si) {
+
+ if (!si.hit) {
+ vec3 irr = path.specularBounce ? sampleBackgroundFromDirection(path.ray.d) : sampleEnvmapFromDirection(path.ray.d);
+
+ // hit a light source (the hdr map)
+ // add contribution from light source
+ // path.misWeight is the multiple importance sampled weight of this light source
+ path.li += path.misWeight * path.beta * irr;
+ path.abort = true;
+ return;
+ }
+
+ #ifdef USE_GLASS
+ if (si.materialType == THIN_GLASS || si.materialType == THICK_GLASS) {
+ sampleGlassSpecular(si, i, path);
+ }
+ #endif
+ #ifdef USE_SHADOW_CATCHER
+ if (si.materialType == SHADOW_CATCHER) {
+ sampleShadowCatcher(si, i, path);
+ }
+ #endif
+ if (si.materialType == STANDARD) {
+ sampleMaterial(si, i, path);
+ }
+
+ // Russian Roulette sampling
+ if (i >= 2) {
+ float q = 1.0 - dot(path.beta, luminance);
+ if (randomSample() < q) {
+ path.abort = true;
+ }
+ path.beta /= 1.0 - q;
+ }
+
+ }
+
+ // 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) {
+ Path path;
+ path.ray = ray;
+ path.li = vec3(0);
+ path.alpha = 1.0;
+ path.beta = vec3(1.0);
+ path.specularBounce = true;
+ path.abort = false;
+ path.misWeight = 1.0;
+
+ SurfaceInteraction si;
+
+ // first surface interaction from g-buffer
+ surfaceInteractionDirect(vCoord, si);
+
+ // first surface interaction from ray interesction
+ // intersectScene(path.ray, si);
+
+ bounce(path, 1, si);
+
+ // Manually unroll for loop.
+ // Some hardware fails to iterate over a GLSL loop, so we provide this workaround
+ // for (int i = 1; i < defines.bounces + 1, i += 1)
+ // equivelant to
+ ${unrollLoop('i', 2, defines.BOUNCES + 1, 1, `
+ if (path.abort) {
+ return vec4(path.li, path.alpha);
+ }
+ intersectScene(path.ray, si);
+ bounce(path, i, si);
+ `)}
+
+ return vec4(path.li, path.alpha);
+ }
+
+ void main() {
+ initRandom();
+
+ vec2 vCoordAntiAlias = vCoord + jitter;
+
+ 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(randomSampleVec2());
+ // vec3 focusPoint = -direction * camera.focus / direction.z; // intersect ray direction with focus plane
+
+ // vec3 origin = vec3(lensPoint, 0.0);
+ // direction = normalize(focusPoint - origin);
+
+ // origin = vec3(camera.transform * vec4(origin, 1.0));
+ // direction = mat3(camera.transform) * direction;
+
+ vec3 origin = camera.transform[3].xyz;
+ direction = mat3(camera.transform) * direction;
+
+ Ray cam;
+ initRay(cam, origin, direction);
+
+ vec4 liAndAlpha = integrator(cam);
+
+ if (!(liAndAlpha.x < INF && liAndAlpha.x > -EPS)) {
+ liAndAlpha = vec4(0, 0, 0, 1);
+ }
+
+ out_light = liAndAlpha;
+
+ // Stratified Sampling Sample Count Test
+ // ---------------
+ // Uncomment the following code
+ // Then observe the colors of the image
+ // If:
+ // * The resulting image is pure black
+ // Extra samples are being passed to the shader that aren't being used.
+ // * The resulting image contains red
+ // Not enough samples are being passed to the shader
+ // * The resulting image contains only white with some black
+ // All samples are used by the shader. Correct result!
+
+ // out_light = vec4(0, 0, 0, 1);
+ // if (sampleIndex == SAMPLING_DIMENSIONS) {
+ // out_light = vec4(1, 1, 1, 1);
+ // } else if (sampleIndex > SAMPLING_DIMENSIONS) {
+ // out_light = vec4(1, 0, 0, 1);
+ // }
+}
+`
+ };
+
+ /*
+ 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) {
+ const strata = [];
+ const l = strataCount ** dimensions;
+ for (let i = 0; i < l; i++) {
+ strata[i] = i;
+ }
+
+ let index = strata.length;
+
+ const sample = [];
+
+ function restart() {
+ index = 0;
+ }
+
+ function next() {
+ if (index >= strata.length) {
+ shuffle(strata);
+ restart();
+ }
+ let stratum = strata[index++];
+
+ for (let i = 0; i < dimensions; i++) {
+ sample[i] = stratum % strataCount + Math.random();
+ stratum = Math.floor(stratum / strataCount);
+ }
+
+ return sample;
+ }
+
+ return {
+ next,
+ restart,
+ strataCount
+ };
+ }
+
+ /*
+ 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) {
+ const strataObjs = [];
+
+ for (const dim of listOfDimensions) {
+ strataObjs.push(makeStratifiedSampler(strataCount, dim));
+ }
+
+ const combined = [];
+
+ function next() {
+ let i = 0;
+
+ for (const strata of strataObjs) {
+ const nums = strata.next();
+
+ for (const num of nums) {
+ combined[i++] = num;
+ }
+ }
+
+ return combined;
+ }
+
+ function restart() {
+ for (const strata of strataObjs) {
+ strata.restart();
+ }
+ }
+
+ return {
+ next,
+ restart,
+ strataCount
+ };
+ }
+
+ function makeRayTracePass(gl, {
+ bounces, // number of global illumination bounces
+ decomposedScene,
+ fullscreenQuad,
+ materialBuffer,
+ mergedMesh,
+ optionalExtensions,
+ }) {
+
+ bounces = clamp(bounces, 1, 6);
+
+ const samplingDimensions = [];
+
+ for (let i = 1; i <= bounces; i++) {
+ // specular or diffuse reflection, light importance sampling, next path direction
+ samplingDimensions.push(2, 2, 2);
+ if (i >= 2) {
+ // russian roulette sampling
+ // this step is skipped on the first bounce
+ samplingDimensions.push(1);
+ }
+ }
+
+ let samples;
+
+ const renderPass = makeRenderPassFromScene({
+ bounces, decomposedScene, fullscreenQuad, gl, materialBuffer, mergedMesh, optionalExtensions, samplingDimensions,
+ });
+
+ function setSize(width, height) {
+ renderPass.setUniform('pixelSize', 1 / width, 1 / height);
+ }
+
+ // noiseImage is a 32-bit PNG image
+ function setNoise(noiseImage) {
+ renderPass.setTexture('noise', makeTexture(gl, {
+ data: noiseImage,
+ wrapS: gl.REPEAT,
+ wrapT: gl.REPEAT,
+ storage: 'halfFloat',
+ }));
+ }
+
+ function setCamera(camera) {
+ renderPass.setUniform('camera.transform', camera.matrixWorld.elements);
+ renderPass.setUniform('camera.aspect', camera.aspect);
+ renderPass.setUniform('camera.fov', 0.5 / Math.tan(0.5 * Math.PI * camera.fov / 180));
+ }
+
+ function setJitter(x, y) {
+ renderPass.setUniform('jitter', x, y);
+ }
+
+ function setGBuffers({ position, normal, faceNormal, color, matProps }) {
+ renderPass.setTexture('gPosition', position);
+ renderPass.setTexture('gNormal', normal);
+ renderPass.setTexture('gFaceNormal', faceNormal);
+ renderPass.setTexture('gColor', color);
+ renderPass.setTexture('gMatProps', matProps);
+ }
+
+ function nextSeed() {
+ renderPass.setUniform('stratifiedSamples[0]', samples.next());
+ }
+
+ function setStrataCount(strataCount) {
+ 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();
+ }
+
+ renderPass.setUniform('strataSize', 1.0 / strataCount);
+ nextSeed();
+ }
+
+ function bindTextures() {
+ renderPass.bindTextures();
+ }
+
+ function draw() {
+ renderPass.useProgram(false);
+ fullscreenQuad.draw();
+ }
+
+ samples = makeStratifiedSamplerCombined(1, samplingDimensions);
+
+ return {
+ bindTextures,
+ draw,
+ nextSeed,
+ outputLocs: renderPass.outputLocs,
+ setCamera,
+ setJitter,
+ setGBuffers,
+ setNoise,
+ setSize,
+ setStrataCount,
+ };
+ }
+ function makeRenderPassFromScene({
+ bounces,
+ decomposedScene,
+ fullscreenQuad,
+ gl,
+ materialBuffer,
+ mergedMesh,
+ optionalExtensions,
+ samplingDimensions,
+ }) {
+ const { OES_texture_float_linear } = optionalExtensions;
+
+ const { background, directionalLights, ambientLights, environmentLights } = decomposedScene;
+
+ const { geometry, materials, materialIndices } = mergedMesh;
+
+ // create bounding volume hierarchy from a static scene
+ const bvh = bvhAccel(geometry);
+ const flattenedBvh = flattenBvh(bvh);
+ const numTris = geometry.index.count / 3;
+
+ const renderPass = makeRenderPass(gl, {
+ defines: {
+ OES_texture_float_linear,
+ BVH_COLUMNS: textureDimensionsFromArray(flattenedBvh.count).columnsLog,
+ INDEX_COLUMNS: textureDimensionsFromArray(numTris).columnsLog,
+ VERTEX_COLUMNS: textureDimensionsFromArray(geometry.attributes.position.count).columnsLog,
+ STACK_SIZE: flattenedBvh.maxDepth,
+ BOUNCES: bounces,
+ USE_GLASS: materials.some(m => m.transparent),
+ USE_SHADOW_CATCHER: materials.some(m => m.shadowCatcher),
+ SAMPLING_DIMENSIONS: samplingDimensions.reduce((a, b) => a + b),
+ ...materialBuffer.defines
+ },
+ fragment: fragment$1,
+ vertex: fullscreenQuad.vertexShader
+ });
+
+ renderPass.setTexture('diffuseMap', materialBuffer.textures.diffuseMap);
+ renderPass.setTexture('normalMap', materialBuffer.textures.normalMap);
+ renderPass.setTexture('pbrMap', materialBuffer.textures.pbrMap);
+
+ renderPass.setTexture('positions', makeDataTexture(gl, geometry.getAttribute('position').array, 3));
+
+ renderPass.setTexture('normals', makeDataTexture(gl, geometry.getAttribute('normal').array, 3));
+
+ renderPass.setTexture('uvs', makeDataTexture(gl, geometry.getAttribute('uv').array, 2));
+
+ renderPass.setTexture('bvh', makeDataTexture(gl, flattenedBvh.buffer, 4));
+
+ const envImage = generateEnvMapFromSceneComponents(directionalLights, ambientLights, environmentLights);
+ const envImageTextureObject = makeTexture(gl, {
+ data: envImage.data,
+ storage: 'halfFloat',
+ minFilter: OES_texture_float_linear ? gl.LINEAR : gl.NEAREST,
+ magFilter: OES_texture_float_linear ? gl.LINEAR : gl.NEAREST,
+ width: envImage.width,
+ height: envImage.height,
+ });
+
+ renderPass.setTexture('envmap', envImageTextureObject);
+
+ let backgroundImageTextureObject;
+ if (background) {
+ const backgroundImage = generateBackgroundMapFromSceneBackground(background);
+ backgroundImageTextureObject = makeTexture(gl, {
+ data: backgroundImage.data,
+ storage: 'halfFloat',
+ minFilter: OES_texture_float_linear ? gl.LINEAR : gl.NEAREST,
+ magFilter: OES_texture_float_linear ? gl.LINEAR : gl.NEAREST,
+ width: backgroundImage.width,
+ height: backgroundImage.height,
+ });
+ } else {
+ backgroundImageTextureObject = envImageTextureObject;
+ }
+
+ renderPass.setTexture('backgroundMap', backgroundImageTextureObject);
+
+ const distribution = envmapDistribution(envImage);
+
+ renderPass.setTexture('envmapDistribution', makeTexture(gl, {
+ data: distribution.data,
+ storage: 'halfFloat',
+ width: distribution.width,
+ height: distribution.height,
+ }));
+
+ return renderPass;
+ }
+
+ 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 makeDataTexture(gl, dataArray, channels) {
+ const textureDim = textureDimensionsFromArray(dataArray.length / channels);
+ return makeTexture(gl, {
+ data: padArray(dataArray, channels * textureDim.size),
+ width: textureDim.columns,
+ height: textureDim.rows,
+ });
+ }
+
+ // expand array to the given length
+ function padArray(typedArray, length) {
+ const newArray = new typedArray.constructor(length);
+ newArray.set(typedArray);
+ return newArray;
+ }
+
+ var fragment$2 = {
+ outputs: ['light'],
+ includes: [textureLinear],
+ source: `
+ in vec2 vCoord;
+
+ uniform mediump sampler2D light;
+ uniform mediump sampler2D position;
+ uniform vec2 lightScale;
+ uniform vec2 previousLightScale;
+
+ uniform mediump sampler2D previousLight;
+ uniform mediump sampler2D previousPosition;
+
+ uniform mat4 historyCamera;
+ uniform float blendAmount;
+ uniform vec2 jitter;
+
+ vec2 reproject(vec3 position) {
+ vec4 historyCoord = historyCamera * vec4(position, 1.0);
+ return 0.5 * historyCoord.xy / historyCoord.w + 0.5;
+ }
+
+ float getMeshId(sampler2D meshIdTex, vec2 vCoord) {
+ return floor(texture(meshIdTex, vCoord).w);
+ }
+
+ void main() {
+ vec3 currentPosition = textureLinear(position, vCoord).xyz;
+ float currentMeshId = getMeshId(position, vCoord);
+
+ vec4 currentLight = texture(light, lightScale * vCoord);
+
+ if (currentMeshId == 0.0) {
+ out_light = currentLight;
+ return;
+ }
+
+ vec2 hCoord = reproject(currentPosition) - jitter;
+
+ vec2 hSizef = previousLightScale * vec2(textureSize(previousLight, 0));
+ vec2 hSizeInv = 1.0 / hSizef;
+ ivec2 hSize = ivec2(hSizef);
+
+ vec2 hTexelf = hCoord * hSizef - 0.5;
+ ivec2 hTexel = ivec2(hTexelf);
+ vec2 f = fract(hTexelf);
+
+ ivec2 texel[] = ivec2[](
+ hTexel + ivec2(0, 0),
+ hTexel + ivec2(1, 0),
+ hTexel + ivec2(0, 1),
+ hTexel + ivec2(1, 1)
+ );
+
+ float weights[] = float[](
+ (1.0 - f.x) * (1.0 - f.y),
+ f.x * (1.0 - f.y),
+ (1.0 - f.x) * f.y,
+ f.x * f.y
+ );
+
+ vec4 history;
+ float sum;
+
+ // bilinear sampling, rejecting samples that don't have a matching mesh id
+ for (int i = 0; i < 4; i++) {
+ vec2 gCoord = (vec2(texel[i]) + 0.5) * hSizeInv;
+
+ float histMeshId = getMeshId(previousPosition, gCoord);
+
+ float isValid = histMeshId != currentMeshId || any(greaterThanEqual(texel[i], hSize)) ? 0.0 : 1.0;
+
+ float weight = isValid * weights[i];
+ history += weight * texelFetch(previousLight, texel[i], 0);
+ sum += weight;
+ }
+
+ if (sum > 0.0) {
+ history /= sum;
+ } else {
+ // If all samples of bilinear fail, try a 3x3 box filter
+ hTexel = ivec2(hTexelf + 0.5);
+
+ for (int x = -1; x <= 1; x++) {
+ for (int y = -1; y <= 1; y++) {
+ ivec2 texel = hTexel + ivec2(x, y);
+ vec2 gCoord = (vec2(texel) + 0.5) * hSizeInv;
+
+ float histMeshId = getMeshId(previousPosition, gCoord);
+
+ float isValid = histMeshId != currentMeshId || any(greaterThanEqual(texel, hSize)) ? 0.0 : 1.0;
+
+ float weight = isValid;
+ vec4 h = texelFetch(previousLight, texel, 0);
+ history += weight * h;
+ sum += weight;
+ }
+ }
+ history = sum > 0.0 ? history / sum : history;
+ }
+
+ if (history.w > MAX_SAMPLES) {
+ history.xyz *= MAX_SAMPLES / history.w;
+ history.w = MAX_SAMPLES;
+ }
+
+ out_light = blendAmount * history + currentLight;
+ }
+`
+ };
+
+ function makeReprojectPass(gl, params) {
+ const {
+ fullscreenQuad,
+ maxReprojectedSamples,
+ } = params;
+
+ const renderPass = makeRenderPass(gl, {
+ defines: {
+ MAX_SAMPLES: maxReprojectedSamples.toFixed(1)
+ },
+ vertex: fullscreenQuad.vertexShader,
+ fragment: fragment$2
+ });
+
+ const historyCamera = new THREE$1.Matrix4();
+
+ function setPreviousCamera(camera) {
+ historyCamera.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
+
+ renderPass.setUniform('historyCamera', historyCamera.elements);
+ }
+
+ function setJitter(x, y) {
+ renderPass.setUniform('jitter', x, y);
+ }
+
+ function draw(params) {
+ const {
+ blendAmount,
+ light,
+ lightScale,
+ position,
+ previousLight,
+ previousLightScale,
+ previousPosition,
+ } = params;
+
+ renderPass.setUniform('blendAmount', blendAmount);
+ renderPass.setUniform('lightScale', lightScale.x, lightScale.y);
+ renderPass.setUniform('previousLightScale', previousLightScale.x, previousLightScale.y);
+
+ renderPass.setTexture('light', light);
+ renderPass.setTexture('position', position);
+ renderPass.setTexture('previousLight', previousLight);
+ renderPass.setTexture('previousPosition', previousPosition);
+
+ renderPass.useProgram();
+ fullscreenQuad.draw();
+ }
+
+ return {
+ draw,
+ setJitter,
+ setPreviousCamera,
+ };
+ }
+
+ var fragment$3 = {
+ includes: [textureLinear],
+ outputs: ['color'],
+ source: `
+ in vec2 vCoord;
+
+ uniform sampler2D light;
+ uniform sampler2D position;
+
+ uniform vec2 lightScale;
+
+ // Tonemapping functions from THREE.js
+
+ vec3 linear(vec3 color) {
+ return color;
+ }
+ // https://www.cs.utah.edu/~reinhard/cdrom/
+ vec3 reinhard(vec3 color) {
+ return clamp(color / (vec3(1.0) + color), vec3(0.0), vec3(1.0));
+ }
+ // http://filmicworlds.com/blog/filmic-tonemapping-operators/
+ #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))
+ const vec3 uncharted2WhitePoint = 1.0 / uncharted2Helper(vec3(WHITE_POINT));
+ vec3 uncharted2( vec3 color ) {
+ // John Hable's filmic operator from Uncharted 2 video game
+ return clamp(uncharted2Helper(color) * uncharted2WhitePoint, vec3(0.0), vec3(1.0));
+ }
+ // http://filmicworlds.com/blog/filmic-tonemapping-operators/
+ vec3 cineon( vec3 color ) {
+ // optimized filmic operator by Jim Hejl and Richard Burgess-Dawson
+ color = max(vec3( 0.0 ), color - 0.004);
+ return pow((color * (6.2 * color + 0.5)) / (color * (6.2 * color + 1.7) + 0.06), vec3(2.2));
+ }
+ // https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
+ vec3 acesFilmic( vec3 color ) {
+ return clamp((color * (2.51 * color + 0.03)) / (color * (2.43 * color + 0.59) + 0.14), vec3(0.0), vec3(1.0));
+ }
+
+ #ifdef EDGE_PRESERVING_UPSCALE
+
+ float getMeshId(sampler2D meshIdTex, vec2 vCoord) {
+ return floor(texture(meshIdTex, vCoord).w);
+ }
+
+ vec4 getUpscaledLight(vec2 coord) {
+ float meshId = getMeshId(position, coord);
+
+ vec2 sizef = lightScale * vec2(textureSize(position, 0));
+ vec2 texelf = coord * sizef - 0.5;
+ ivec2 texel = ivec2(texelf);
+ vec2 f = fract(texelf);
+
+ ivec2 texels[] = ivec2[](
+ texel + ivec2(0, 0),
+ texel + ivec2(1, 0),
+ texel + ivec2(0, 1),
+ texel + ivec2(1, 1)
+ );
+
+ float weights[] = float[](
+ (1.0 - f.x) * (1.0 - f.y),
+ f.x * (1.0 - f.y),
+ (1.0 - f.x) * f.y,
+ f.x * f.y
+ );
+
+ vec4 upscaledLight;
+ float sum;
+ for (int i = 0; i < 4; i++) {
+ vec2 pCoord = (vec2(texels[i]) + 0.5) / sizef;
+ float isValid = getMeshId(position, pCoord) == meshId ? 1.0 : 0.0;
+ float weight = isValid * weights[i];
+ upscaledLight += weight * texelFetch(light, texels[i], 0);
+ sum += weight;
+ }
+
+ if (sum > 0.0) {
+ upscaledLight /= sum;
+ } else {
+ upscaledLight = texture(light, lightScale * coord);
+ }
+
+ return upscaledLight;
+ }
+ #endif
+
+ void main() {
+ #ifdef EDGE_PRESERVING_UPSCALE
+ vec4 upscaledLight = getUpscaledLight(vCoord);
+ #else
+ vec4 upscaledLight = texture(light, lightScale * vCoord);
+ #endif
+
+ // alpha channel stores the number of samples progressively rendered
+ // divide the sum of light by alpha to obtain average contribution of light
+
+ // in addition, alpha contains a scale factor for the shadow catcher material
+ // dividing by alpha normalizes the brightness of the shadow catcher to match the background envmap.
+ vec3 light = upscaledLight.rgb / upscaledLight.a;
+
+ light *= EXPOSURE;
+
+ light = TONE_MAPPING(light);
+
+ light = pow(light, vec3(1.0 / 2.2)); // gamma correction
+
+ out_color = vec4(light, 1.0);
+ }
+`
+ };
+
+ const toneMapFunctions = {
+ [THREE$1.LinearToneMapping]: 'linear',
+ [THREE$1.ReinhardToneMapping]: 'reinhard',
+ [THREE$1.Uncharted2ToneMapping]: 'uncharted2',
+ [THREE$1.CineonToneMapping]: 'cineon',
+ [THREE$1.ACESFilmicToneMapping]: 'acesFilmic'
+ };
+
+ function makeToneMapPass(gl, params) {
+ const {
+ fullscreenQuad,
+ toneMappingParams
+ } = params;
+
+ const renderPassConfig = {
+ gl,
+ defines: {
+ TONE_MAPPING: toneMapFunctions[toneMappingParams.toneMapping] || 'linear',
+ WHITE_POINT: toneMappingParams.whitePoint.toExponential(), // toExponential allows integers to be represented as GLSL floats
+ EXPOSURE: toneMappingParams.exposure.toExponential()
+ },
+ vertex: fullscreenQuad.vertexShader,
+ fragment: fragment$3,
+ };
+
+ renderPassConfig.defines.EDGE_PRESERVING_UPSCALE = true;
+ const renderPassUpscale = makeRenderPass(gl, renderPassConfig);
+
+ renderPassConfig.defines.EDGE_PRESERVING_UPSCALE = false;
+ const renderPassNative = makeRenderPass(gl, renderPassConfig);
+
+ function draw(params) {
+ const {
+ light,
+ lightScale,
+ position
+ } = params;
+
+ const renderPass =
+ lightScale.x !== 1 && lightScale.y !== 1 ?
+ renderPassUpscale :
+ renderPassNative;
+
+ renderPass.setUniform('lightScale', lightScale.x, lightScale.y);
+ renderPass.setTexture('light', light);
+ renderPass.setTexture('position', position);
+
+ renderPass.useProgram();
+ fullscreenQuad.draw();
+ }
+
+ return {
+ draw
+ };
+ }
+
+ // TileRender is based on the concept of a compute shader's work group.
+
+ // Sampling the scene with the RayTracingRenderer can be very slow (<1 fps).
+ // This overworks the GPU and tends to lock up the OS, making it unresponsive.
+
+ // To fix this, we can split the screen into smaller tiles, and sample the scene one tile at a time
+ // The tile size is set such that each tile takes approximatly a constant amount of time to render.
+
+ // Since the render time of a tile is dependent on the device, we find the desired tile dimensions by measuring
+ // the time it takes to render an arbitrarily-set tile size and adjusting the size according to the benchmark.
+
+ function makeTileRender(gl) {
+ let currentTile = -1;
+ let numTiles = 1;
+ let tileWidth;
+ let tileHeight;
+ let columns;
+ let rows;
+
+ let width = 0;
+ let height = 0;
+
+ // initial number of pixels per rendered tile
+ // based on correlation between system performance and max supported render buffer size
+ // adjusted dynamically according to system performance
+ let pixelsPerTile = pixelsPerTileEstimate(gl);
+
+ let pixelsPerTileQuantized = pixelsPerTile;
+
+ let desiredTimePerTile = 20;
+
+ let timePerPixel = desiredTimePerTile / pixelsPerTile;
+
+ let lastTime = 0;
+ let timeElapsed = 0;
+
+ function updateTime(time) {
+ if (lastTime) {
+ timeElapsed = time - lastTime;
+ }
+
+ lastTime = time;
+ }
+
+ function reset() {
+ currentTile = -1;
+ timeElapsed = 0;
+ lastTime = 0;
+ }
+
+ function setSize(w, h) {
+ width = w;
+ height = h;
+ reset();
+ }
+
+ function setTileDimensions(pixelsPerTile) {
+ const aspectRatio = width / height;
+
+ // quantize the width of the tile so that it evenly divides the entire window
+ tileWidth = Math.ceil(width / Math.round(width / Math.sqrt(pixelsPerTile * aspectRatio)));
+ tileHeight = Math.ceil(tileWidth / aspectRatio);
+ pixelsPerTileQuantized = tileWidth * tileHeight;
+
+ columns = Math.ceil(width / tileWidth);
+ rows = Math.ceil(height / tileHeight);
+ numTiles = columns * rows;
+ }
+
+ function initTiles() {
+ if (timeElapsed) {
+ const timePerTile = timeElapsed / numTiles;
+
+ const expAvg = 0.5;
+
+ const newPixelsPerTile = pixelsPerTile * desiredTimePerTile / timePerTile;
+ pixelsPerTile = expAvg * pixelsPerTile + (1 - expAvg) * newPixelsPerTile;
+
+ const newTimePerPixel = timePerTile / pixelsPerTileQuantized;
+ timePerPixel = expAvg * timePerPixel + (1 - expAvg) * newTimePerPixel;
+ }
+
+ pixelsPerTile = clamp(pixelsPerTile, 8192, width * height);
+
+ setTileDimensions(pixelsPerTile);
+ }
+
+ function nextTile() {
+ currentTile++;
+
+ if (currentTile % numTiles === 0) {
+ initTiles();
+ currentTile = 0;
+ timeElapsed = 0;
+ }
+
+ const isLastTile = currentTile === numTiles - 1;
+ if (isLastTile) {
+ requestAnimationFrame(updateTime);
+ }
+
+ const x = currentTile % columns;
+ const y = Math.floor(currentTile / columns) % rows;
+
+ return {
+ x: x * tileWidth,
+ y: y * tileHeight,
+ tileWidth,
+ tileHeight,
+ isFirstTile: currentTile === 0,
+ isLastTile,
+ };
+ }
+
+ return {
+ getTimePerPixel() {
+ return timePerPixel;
+ },
+ nextTile,
+ reset,
+ setSize,
+ };
+ }
+
+ function pixelsPerTileEstimate(gl) {
+ const maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);
+
+ if (maxRenderbufferSize <= 8192) {
+ return 200000;
+ } else if (maxRenderbufferSize === 16384) {
+ return 400000;
+ } else if (maxRenderbufferSize >= 32768) {
+ return 600000;
+ }
+ }
+
+ var noiseBase64 = 'data:image/png;base64,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';
+
+ function makeRenderingPipeline({
+ gl,
+ optionalExtensions,
+ scene,
+ toneMappingParams,
+ bounces, // number of global illumination bounces
+ }) {
+
+ const maxReprojectedSamples = 20;
+
+ // how many samples to render with uniform noise before switching to stratified noise
+ const numUniformSamples = 4;
+
+ // how many partitions of stratified noise should be created
+ // higher number results in faster convergence over time, but with lower quality initial samples
+ const strataCount = 6;
+
+ const desiredTimeForPreview = 14;
+
+ const decomposedScene = decomposeScene(scene);
+
+ const mergedMesh = mergeMeshesToGeometry(decomposedScene.meshes);
+
+ const materialBuffer = makeMaterialBuffer(gl, mergedMesh.materials);
+
+ const fullscreenQuad = makeFullscreenQuad(gl);
+
+ const rayTracePass = makeRayTracePass(gl, { bounces, decomposedScene, fullscreenQuad, materialBuffer, mergedMesh, optionalExtensions, scene });
+
+ const reprojectPass = makeReprojectPass(gl, { fullscreenQuad, maxReprojectedSamples });
+
+ const toneMapPass = makeToneMapPass(gl, { fullscreenQuad, toneMappingParams });
+
+ const gBufferPass = makeGBufferPass(gl, { materialBuffer, mergedMesh });
+
+ // used to sample only a portion of the scene to the HDR Buffer to prevent the GPU from locking up from excessive computation
+ const tileRender = makeTileRender(gl);
+
+ let ready = false;
+ const noiseImage = new Image();
+ noiseImage.src = noiseBase64;
+ noiseImage.onload = () => {
+ rayTracePass.setNoise(noiseImage);
+ ready = true;
+ };
+
+ let sampleCount = 0;
+
+ let sampleRenderedCallback = () => {};
+
+ const lastCamera = new THREE$1.PerspectiveCamera();
+ lastCamera.position.set(1, 1, 1);
+ lastCamera.updateMatrixWorld();
+
+ let screenWidth = 0;
+ let screenHeight = 0;
+
+ let previewWidth = 0;
+ let previewHeight = 0;
+
+ const previewScale = new THREE$1.Vector2(1, 1);
+ const fullscreenScale = new THREE$1.Vector2(1, 1);
+
+ let hdrBuffer;
+ let hdrBackBuffer;
+ let reprojectBuffer;
+ let reprojectBackBuffer;
+
+ let gBuffer;
+ let gBufferBack;
+
+ let lastToneMappedTexture;
+ let lastToneMappedScale;
+
+ function initFrameBuffers(width, height) {
+ const makeHdrBuffer = () => makeFramebuffer(gl, {
+ color: { 0: makeTexture(gl, { width, height, storage: 'float', magFilter: gl.LINEAR, minFilter: gl.LINEAR }) }
+ });
+
+ const makeReprojectBuffer = () => makeFramebuffer(gl, {
+ color: { 0: makeTexture(gl, { width, height, storage: 'float', magFilter: gl.LINEAR, minFilter: gl.LINEAR }) }
+ });
+
+ hdrBuffer = makeHdrBuffer();
+ hdrBackBuffer = makeHdrBuffer();
+
+ reprojectBuffer = makeReprojectBuffer();
+ reprojectBackBuffer = makeReprojectBuffer();
+
+ const normalBuffer = makeTexture(gl, { width, height, storage: 'halfFloat' });
+ const faceNormalBuffer = makeTexture(gl, { width, height, storage: 'halfFloat' });
+ const colorBuffer = makeTexture(gl, { width, height, storage: 'byte', channels: 3 });
+ const matProps = makeTexture(gl, { width, height, storage: 'byte', channels: 2 });
+ const depthTarget = makeDepthTarget(gl, width, height);
+
+ const makeGBuffer = () => makeFramebuffer(gl, {
+ color: {
+ [gBufferPass.outputLocs.position]: makeTexture(gl, { width, height, storage: 'float' }),
+ [gBufferPass.outputLocs.normal]: normalBuffer,
+ [gBufferPass.outputLocs.faceNormal]: faceNormalBuffer,
+ [gBufferPass.outputLocs.color]: colorBuffer,
+ [gBufferPass.outputLocs.matProps]: matProps,
+ },
+ depth: depthTarget
+ });
+
+ gBuffer = makeGBuffer();
+ gBufferBack = makeGBuffer();
+
+ lastToneMappedTexture = hdrBuffer.color[rayTracePass.outputLocs.light];
+ lastToneMappedScale = fullscreenScale;
+ }
+
+ function swapReprojectBuffer() {
+ let temp = reprojectBuffer;
+ reprojectBuffer = reprojectBackBuffer;
+ reprojectBackBuffer = temp;
+ }
+
+ function swapGBuffer() {
+ let temp = gBuffer;
+ gBuffer = gBufferBack;
+ gBufferBack = temp;
+ }
+
+ function swapHdrBuffer() {
+ let temp = hdrBuffer;
+ hdrBuffer = hdrBackBuffer;
+ hdrBackBuffer = temp;
+ }
+
+ // Shaders will read from the back buffer and draw to the front buffer
+ // Buffers are swapped after every render
+ function swapBuffers() {
+ swapReprojectBuffer();
+ swapGBuffer();
+ swapHdrBuffer();
+ }
+
+ function setSize(w, h) {
+ screenWidth = w;
+ screenHeight = h;
+
+ tileRender.setSize(w, h);
+ initFrameBuffers(w, h);
+ }
+
+ function setPreviewBufferDimensions() {
+ const numPixelsForPreview = desiredTimeForPreview / tileRender.getTimePerPixel();
+
+ const aspectRatio = screenWidth / screenHeight;
+
+ previewWidth = Math.round(clamp(Math.sqrt(numPixelsForPreview * aspectRatio), 1, screenWidth));
+ previewHeight = Math.round(clamp(previewWidth / aspectRatio, 1, screenHeight));
+ previewScale.set(previewWidth / screenWidth, previewHeight / screenHeight);
+ }
+
+ function areCamerasEqual(cam1, cam2) {
+ return numberArraysEqual(cam1.matrixWorld.elements, cam2.matrixWorld.elements) &&
+ cam1.aspect === cam2.aspect &&
+ cam1.fov === cam2.fov &&
+ cam1.focus === cam2.focus;
+ }
+
+ function updateSeed(width, height, useJitter = true) {
+ rayTracePass.setSize(width, height);
+
+ const jitterX = useJitter ? (Math.random() - 0.5) / width : 0;
+ const jitterY = useJitter ? (Math.random() - 0.5) / height : 0;
+ gBufferPass.setJitter(jitterX, jitterY);
+ rayTracePass.setJitter(jitterX, jitterY);
+ reprojectPass.setJitter(jitterX, jitterY);
+
+ if (sampleCount === 0) {
+ rayTracePass.setStrataCount(1);
+ } else if (sampleCount === numUniformSamples) {
+ rayTracePass.setStrataCount(strataCount);
+ } else {
+ rayTracePass.nextSeed();
+ }
+ }
+
+ function clearBuffer(buffer) {
+ buffer.bind();
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ buffer.unbind();
+ }
+
+ function addSampleToBuffer(buffer, width, height) {
+ buffer.bind();
+
+ gl.blendEquation(gl.FUNC_ADD);
+ gl.blendFunc(gl.ONE, gl.ONE);
+ gl.enable(gl.BLEND);
+
+ gl.viewport(0, 0, width, height);
+ rayTracePass.draw();
+
+ gl.disable(gl.BLEND);
+ buffer.unbind();
+ }
+
+ function newSampleToBuffer(buffer, width, height) {
+ buffer.bind();
+ gl.viewport(0, 0, width, height);
+ rayTracePass.draw();
+ buffer.unbind();
+ }
+
+ function toneMapToScreen(lightTexture, lightScale) {
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+ toneMapPass.draw({
+ light: lightTexture,
+ lightScale,
+ position: gBuffer.color[gBufferPass.outputLocs.position],
+ });
+
+ lastToneMappedTexture = lightTexture;
+ lastToneMappedScale = lightScale;
+ }
+
+ function renderGBuffer() {
+ gBuffer.bind();
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+ gl.viewport(0, 0, screenWidth, screenHeight);
+ gBufferPass.draw();
+ gBuffer.unbind();
+
+ rayTracePass.setGBuffers({
+ position: gBuffer.color[gBufferPass.outputLocs.position],
+ normal: gBuffer.color[gBufferPass.outputLocs.normal],
+ faceNormal: gBuffer.color[gBufferPass.outputLocs.faceNormal],
+ color: gBuffer.color[gBufferPass.outputLocs.color],
+ matProps: gBuffer.color[gBufferPass.outputLocs.matProps]
+ });
+ }
+
+ function renderTile(buffer, x, y, width, height) {
+ gl.scissor(x, y, width, height);
+ gl.enable(gl.SCISSOR_TEST);
+ addSampleToBuffer(buffer, screenWidth, screenHeight);
+ gl.disable(gl.SCISSOR_TEST);
+ }
+
+ function drawPreview(camera, lastCamera) {
+ if (sampleCount > 0) {
+ swapBuffers();
+ }
+
+ sampleCount = 0;
+ tileRender.reset();
+ setPreviewBufferDimensions();
+
+ updateSeed(previewWidth, previewHeight, false);
+
+ rayTracePass.setCamera(camera);
+ gBufferPass.setCamera(camera);
+ reprojectPass.setPreviousCamera(lastCamera);
+ lastCamera.copy(camera);
+
+ renderGBuffer();
+
+ rayTracePass.bindTextures();
+ newSampleToBuffer(hdrBuffer, previewWidth, previewHeight);
+
+ reprojectBuffer.bind();
+ gl.viewport(0, 0, previewWidth, previewHeight);
+ reprojectPass.draw({
+ blendAmount: 1.0,
+ light: hdrBuffer.color[0],
+ lightScale: previewScale,
+ position: gBuffer.color[gBufferPass.outputLocs.position],
+ previousLight: lastToneMappedTexture,
+ previousLightScale: lastToneMappedScale,
+ previousPosition: gBufferBack.color[gBufferPass.outputLocs.position],
+ });
+ reprojectBuffer.unbind();
+
+ toneMapToScreen(reprojectBuffer.color[0], previewScale);
+
+ swapBuffers();
+ }
+
+ function drawTile() {
+ const { x, y, tileWidth, tileHeight, isFirstTile, isLastTile } = tileRender.nextTile();
+
+ if (isFirstTile) {
+
+ if (sampleCount === 0) { // previous rendered image was a preview image
+ clearBuffer(hdrBuffer);
+ reprojectPass.setPreviousCamera(lastCamera);
+ }
+
+ updateSeed(screenWidth, screenHeight, true);
+ renderGBuffer();
+ rayTracePass.bindTextures();
+ }
+
+ renderTile(hdrBuffer, x, y, tileWidth, tileHeight);
+
+ if (isLastTile) {
+ sampleCount++;
+
+ let blendAmount = clamp(1.0 - sampleCount / maxReprojectedSamples, 0, 1);
+ blendAmount *= blendAmount;
+
+ if (blendAmount > 0.0) {
+ reprojectBuffer.bind();
+ gl.viewport(0, 0, screenWidth, screenHeight);
+ reprojectPass.draw({
+ blendAmount,
+ light: hdrBuffer.color[0],
+ lightScale: fullscreenScale,
+ position: gBuffer.color[gBufferPass.outputLocs.position],
+ previousLight: reprojectBackBuffer.color[0],
+ previousLightScale: previewScale,
+ previousPosition: gBufferBack.color[gBufferPass.outputLocs.position],
+ });
+ reprojectBuffer.unbind();
+
+ toneMapToScreen(reprojectBuffer.color[0], fullscreenScale);
+ } else {
+ toneMapToScreen(hdrBuffer.color[0], fullscreenScale);
+ }
+
+ sampleRenderedCallback(sampleCount);
+ }
+ }
+
+ function draw(camera) {
+ if (!ready) {
+ return;
+ }
+
+ if (!areCamerasEqual(camera, lastCamera)) {
+ drawPreview(camera, lastCamera);
+ } else {
+ drawTile();
+ }
+ }
+
+ // debug draw call to measure performance
+ // use full resolution buffers every frame
+ // reproject every frame
+ function drawFull(camera) {
+ if (!ready) {
+ return;
+ }
+
+ swapGBuffer();
+ swapReprojectBuffer();
+
+ if (sampleCount === 0) {
+ reprojectPass.setPreviousCamera(lastCamera);
+ }
+
+ if (!areCamerasEqual(camera, lastCamera)) {
+ sampleCount = 0;
+ rayTracePass.setCamera(camera);
+ gBufferPass.setCamera(camera);
+ lastCamera.copy(camera);
+ clearBuffer(hdrBuffer);
+ } else {
+ sampleCount++;
+ }
+
+ updateSeed(screenWidth, screenHeight, true);
+
+ renderGBuffer();
+
+ rayTracePass.bindTextures();
+ addSampleToBuffer(hdrBuffer, screenWidth, screenHeight);
+
+ reprojectBuffer.bind();
+ gl.viewport(0, 0, screenWidth, screenHeight);
+ reprojectPass.draw({
+ blendAmount: 1.0,
+ light: hdrBuffer.color[0],
+ lightScale: fullscreenScale,
+ position: gBuffer.color[gBufferPass.outputLocs.position],
+ previousLight: lastToneMappedTexture,
+ previousLightScale: lastToneMappedScale,
+ previousPosition: gBufferBack.color[gBufferPass.outputLocs.position],
+ });
+ reprojectBuffer.unbind();
+
+ toneMapToScreen(reprojectBuffer.color[0], fullscreenScale);
+ }
+
+ return {
+ draw,
+ drawFull,
+ restartTimer: tileRender.reset,
+ setSize,
+ getTotalSamplesRendered() {
+ return sampleCount;
+ },
+ set onSampleRendered(cb) {
+ sampleRenderedCallback = cb;
+ },
+ get onSampleRendered() {
+ return sampleRenderedCallback;
+ }
+ };
+ }
+
+ const glRequiredExtensions = [
+ 'EXT_color_buffer_float', // enables rendering to float buffers
+ 'EXT_float_blend',
+ ];
+
+ const glOptionalExtensions = [
+ 'OES_texture_float_linear', // enables gl.LINEAR texture filtering for float textures,
+ ];
+
+ function RayTracingRenderer(params = {}) {
+ const canvas = params.canvas || document.createElement('canvas');
+
+ const gl = canvas.getContext('webgl2', {
+ alpha: false,
+ depth: true,
+ stencil: false,
+ antialias: false,
+ powerPreference: 'high-performance',
+ failIfMajorPerformanceCaveat: true
+ });
+
+ loadExtensions(gl, glRequiredExtensions);
+ const optionalExtensions = loadExtensions(gl, glOptionalExtensions);
+
+ let pipeline = null;
+ const size = new THREE$1.Vector2();
+ let pixelRatio = 1;
+
+ const module = {
+ bounces: 2,
+ domElement: canvas,
+ maxHardwareUsage: false,
+ needsUpdate: true,
+ onSampleRendered: null,
+ renderWhenOffFocus: true,
+ renderToScreen: true,
+ toneMapping: THREE$1.LinearToneMapping,
+ toneMappingExposure: 1,
+ toneMappingWhitePoint: 1,
+ };
+
+ function initScene(scene) {
+ scene.updateMatrixWorld();
+
+ const toneMappingParams = {
+ exposure: module.toneMappingExposure,
+ whitePoint: module.toneMappingWhitePoint,
+ toneMapping: module.toneMapping
+ };
+
+ const bounces = module.bounces;
+
+ pipeline = makeRenderingPipeline({gl, optionalExtensions, scene, toneMappingParams, bounces});
+
+ pipeline.onSampleRendered = (...args) => {
+ if (module.onSampleRendered) {
+ module.onSampleRendered(...args);
+ }
+ };
+
+ module.setSize(size.width, size.height);
+ module.needsUpdate = false;
+ }
+
+ function restartTimer() {
+ if (pipeline) {
+ pipeline.restartTimer();
+ }
+ }
+
+ module.setSize = (width, height, updateStyle = true) => {
+ size.set(width, height);
+ canvas.width = size.width * pixelRatio;
+ canvas.height = size.height * pixelRatio;
+
+ if (updateStyle) {
+ canvas.style.width = `${ size.width }px`;
+ canvas.style.height = `${ size.height }px`;
+ }
+
+ if (pipeline) {
+ pipeline.setSize(size.width * pixelRatio, size.height * pixelRatio);
+ }
+ };
+
+ module.getSize = (target) => {
+ if (!target) {
+ target = new THREE$1.Vector2();
+ }
+
+ return target.copy(size);
+ };
+
+ module.setPixelRatio = (x) => {
+ if (!x) {
+ return;
+ }
+ pixelRatio = x;
+ module.setSize(size.width, size.height, false);
+ };
+
+ module.getPixelRatio = () => pixelRatio;
+
+ module.getTotalSamplesRendered = () => {
+ if (pipeline) {
+ return pipeline.getTotalSamplesRendered();
+ }
+ };
+
+ module.sendToScreen = () => {
+ if (pipeline) {
+ pipeline.hdrBufferToScreen();
+ }
+ };
+
+ let lastFocus = false;
+ module.render = (scene, camera) => {
+ if (!module.renderWhenOffFocus) {
+ const hasFocus = document.hasFocus();
+ if (!hasFocus) {
+ lastFocus = hasFocus;
+ return;
+ } else if (hasFocus && !lastFocus) {
+ lastFocus = hasFocus;
+ restartTimer();
+ }
+ }
+
+ if (module.needsUpdate) {
+ initScene(scene);
+ }
+
+ camera.updateMatrixWorld();
+
+ if (module.renderToScreen) {
+ 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.draw(camera);
+ }
+
+ } else {
+ 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,
+ // the timer should be reset when the user switches browser tabs
+ document.addEventListener('visibilitychange', restartTimer);
+
+ module.dispose = () => {
+ document.removeEventListener('visibilitychange', restartTimer);
+ pipeline = null;
+ };
+
+ return module;
+ }
+
+ RayTracingRenderer.isSupported = () => {
+ const gl = document.createElement('canvas')
+ .getContext('webgl2', {
+ failIfMajorPerformanceCaveat: true
+ });
+
+ if (!gl) {
+ return false;
+ }
+
+ const extensions = loadExtensions(gl, glRequiredExtensions);
+ for (let e in extensions) {
+ if (!extensions[e]) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+
+ if (window.THREE) {
+ /* global THREE */
+ THREE.LensCamera = LensCamera;
+ THREE.SoftDirectionalLight = SoftDirectionalLight;
+ THREE.EnvironmentLight = EnvironmentLight;
+ THREE.RayTracingMaterial = RayTracingMaterial;
+ THREE.RayTracingRenderer = RayTracingRenderer;
+ THREE.ThickMaterial = ThickMaterial;
+ THREE.ThinMaterial = ThinMaterial;
+ }
+
+ exports.EnvironmentLight = EnvironmentLight;
+ exports.LensCamera = LensCamera;
+ exports.RayTracingMaterial = RayTracingMaterial;
+ exports.RayTracingRenderer = RayTracingRenderer;
+ exports.SoftDirectionalLight = SoftDirectionalLight;
+ exports.constants = constants;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+}));
diff --git a/scorpion.js b/scorpion.js
index a79f9df..cc2de31 100644
--- a/scorpion.js
+++ b/scorpion.js
@@ -1,524 +1,40 @@
-// Scorpion library engine built on three.js
-// rewritten 10/4/19
+// scorpion
+// 2/28/20
+// ray tracing build (?)
-log('_scorpion_ v0.3b -- *made by smallkitten development*');
+// goal: clean stuff up, add ray tracing renderer.
-var stats = new Stats(); // stats.js by mrdoob
-stats.showPanel(0);
+// misc:
-function spnStats(bool) {
- if (bool == true) {
- document.body.appendChild(stats.dom);
- }
+function log(v) {
+ console.log(v);
}
-// ---------------------------------------------------------
-// DEBUGGING:
-// using spnDebug() will cause some functions to give more verbose console output
-
-var scorpionDebug = false;
-
-function spnDebug(bool) {
- scorpionDebug = bool;
-
- log('_scorpion debugging_ is set to ' + bool);
-}
-
-//---------------------------------------------------------
-// GENERAL SETTINGS AND VARIABLES:
-
-var globalObject = 'global'; // for piggy backing dynamic global variables using this[] =
-var scorpionSelectedObject; // for pointer raycast, click an object to fill this
-
-var globalRenderTarget = 'gblRT'; // for globally storing information from the custom render target
-
-var windowWidth = window.innerWidth;
-var windowHeight = window.innerHeight;
-
-var textureLoader = new THREE.TextureLoader(); // set this here so the code in the material creator is easier to read
-
-var isDynamic = false; // is the renderer dynamic?
-var spnCustomRenderTarget = false; // is there a custom render target to render to?
-
-// -- DEV PATH TRACING --
-
-var spnRay = 'ray'; // holds ray info
-var spnRaycaster = new THREE.Raycaster();
-
-// -- -- -- -- -- -- -- --
-
-window.addEventListener('resize', resizeRenderer, false); // resize renderer if the screen size changes
-
-function resizeRenderer() {
- if (isDynamic == true) {
- spnCamera.aspect = window.innerWidth / window.innerHeight;
- spnCamera.updateProjectionMatrix();
-
- spnRenderer.setSize(window.innerWidth, window.innerHeight);
- }
-
- // verbose debug logging could be added here
-}
-
-// ---------------------------------------------------------
-// ANIMATIONS:
-// these animations cannot be stopped, however they can be applied to multiple objects.
-// using scripts like these outside of scorpion works perfectly fine as long as you know the name of the object you want to apply it to
-
-function spnRotationAnimation(object, speed, x, y, z, animationName) { // for rotation animations, makes a global variable for animation name
- this[globalObject + animationName] = window.setInterval(function(){
- object.rotation.x -= speed * x;
- object.rotation.y -= speed * y;
- object.rotation.z -= speed * z;
- }, 16);
-
- log('added a rotation animation called global' + animationName + ' to a global object.');
-}
-
-function spnPositionAnimation(object, speed, x, y, z, animationName) {
- this[globalObject + animationName] = window.setInterval(function(){
- object.position.x -= speed * x;
- object.position.y -= speed * y;
- object.position.z -= speed * z;
- }, 16);
-
- log('added a positional animation called global' + animationName + ' to a global object.');
-}
-
-function spnScaleAnimation(object, speed, width, height, depth, animationName) {
- this[globalObject + animationName] = window.setInterval(function(){
- object.scale.x -= speed * width;
- object.scale.y -= speed * height;
- object.scale.z -= speed * depth;
- }, 16);
-}
-
-// ---------------------------------------------------------
-// COLORS:
-
-var white = 0xffffff;
-var black = 0x000000;
-var red = 0xff0000;
-var orange = 0xffa500;
-var yellow = 0xffff00;
-var green = 0x008000;
-var blue = 0x0000ff;
-var purple = 0x800080;
-
-// ^ these are variables that makes coloring things easier so you don't have to google hex codes
-// ---------------------------------------------------------
-// ANIMATION FRAME LOOP:
-// this is the loop that is called to render the scene and camera
-
-function animate() {
- stats.begin(); // call the fps monitor here
-
- requestAnimationFrame(animate);
-
- spnRenderer.render(spnScene, spnCamera);
-
- if (spnCustomRenderTarget == true) {
- spnRenderer.setRenderTarget(gblRTSecondaryTarget);
- } else {
- // do nothing
- }
-
- stats.end(); // end stats call here
-}
-
-// ---------------------------------------------------------
-// CREATE SCENE/CAMERA:
-
-function spnScene(fov, x, y, z, backgroundColor) {
- window.spnScene = new THREE.Scene();
- spnScene.backgroundColor = new THREE.Color(backgroundColor);
-
- // ^ sets up scene (spnScene as of rewrite) with a background color
-
- spnRenderer = new THREE.WebGLRenderer({antialias: true});
- spnRenderer.shadowMap.enabled = true;
-
- // ^ sets up a renderer with antialiasing variable and a shadow map
-
- spnRenderer.setSize(windowWidth, windowHeight);
- document.body.appendChild(spnRenderer.domElement); // adds the renderer as a child to the body
-
- // CAMERA:
-
- spnCamera = new THREE.PerspectiveCamera(fov, windowWidth / windowHeight, 0.1, 2000); // adds a perspective camera with a near of 0.1 and a far of 2000
- spnCamera.position.set(x, y, z); // sets spnCamera position
- spnScene.add(spnCamera); // add camera to the scene
-
- // OTHER SETTING VARIABLES:
-
- isDynamic = true; // sets the global variable for resizing the renderer when the screen size changes (enabled by default 10/8)
-
- animate(); // calls the start of the animation loop once all objects have been added to the scene
-}
-
-// ---------------------------------------------------------
-// GRID
-
-function spnGrid(size, sections, x, y, z, object) {
- var spnGrid = new THREE.GridHelper(size, sections);
- spnGrid.position.x = x;
- spnGrid.position.y = y;
- spnGrid.position.z = z;
- object.add(spnGrid);
-}
-
-// ---------------------------------------------------------
-// TEXTURE LOADER / CUSTOM MATERIALS
-// this creates a texture loader for making custom materials and allows you to save them globally for applying to objects
-
-function spnCustomMaterial(material, albedo, normal, specular, emissive, materialName) {
- // load texture maps using the texture loader:
-
- var loadedAlbedoMap = textureLoader.load(albedo);
- var loadedNormalMap = textureLoader.load(normal);
- var loadedSpecularMap = textureLoader.load(specular);
- var loadedEmissiveMap = textureLoader.load(emissive);
-
- albedoMap.encoding = THREE.sRGBEncoding; // set the color space to sRGB for the albedo map
- albedo.anistrophy = 16;
-
- // BASIC MATERIAL:
-
- if (material == 'basic') {
- this[globalObject + materialName] = new THREE.MeshBasicMaterial({ // creates a global stored material 'globalMaterialName'
- map: loadedAlbedoMap
- })
- }
-
- // LAMBERT MATERIAL:
-
- if (material == 'lambert') {
- this[globalObject + materialName] = new THREE.MeshLambertMaterial({
- map: loadedAlbedoMap,
- specularMap: loadedSpecularMap,
- emissiveMap: loadedEmissiveMap
- })
- }
-
- // PHONG MATERIAL:
-
- if (material == 'phong') {
- this[globalObject + materialName] = new THREE.MeshPhongMaterial({
- map: loadedAlbedoMap,
- normalMap: loadedNormalMap,
- emissiveMap: loadedEmissiveMap,
- specularMap: loadedSpecularMap
- })
- }
-}
-
-// ---------------------------------------------------------
-// LIGHTS:
-
-function spnLight(type, color, x, y, z, intensity, shadow, lightName) {
- if (type == 'ambient') {
- // CREATE AMBIENT LIGHT:
-
- var spnAmbientLight = new THREE.AmbientLight(color);
- spnScene.add(spnAmbientLight);
-
- this[globalObject + lightName] = spnAmbientLight;
- }
-
- if (type == 'directional') {
- // CREATE DIRECTIONAL LIGHT:
-
- var spnDirectionalLight = new THREE.DirectionalLight(color, intensity);
- spnDirectionalLight.position.set(x, y, z);
- spnDirectionalLight.castShadow = shadow;
- spnScene.add(spnAmbientLight);
-
- this[globalObject + lightName] = spnDirectionalLight;
- }
-
- if (type == 'point') {
- // CREATE POINT LIGHT:
-
- var spnPointLight = new THREE.PointLight(color, intensity);
- spnPointLight.position.set(x, y, z);
- spnPointLight.castShadow = shadow;
- spnScene.add(spnPointLight);
-
- this[globalObject + lightName] = spnPointLight;
- }
-
- log('_scorpion_ has created a ' + type + ' light.');
-}
-
-// ---------------------------------------------------------
-// CUBES:
-
-function spnCube(material, clr, l, w, depth, x, y, z, wirefrm, objectName) {
- if (material == 'basic') {
- // CREATE BASIC CUBE MESH:
-
- var spnBasicCube = new THREE.Mesh(new THREE.CubeGeometry(l, w, depth), new THREE.MeshBasicMaterial({color: clr, wireframe: wirefrm}));
- spnBasicCube.position.x = x;
- spnBasicCube.position.y = y;
- spnBasicCube.position.z = z;
- spnBasicCube.castShadow = true;
- spnScene.add(spnBasicCube);
-
- // now we need to store the mesh data in a dynamic global variable
-
- this[globalObject + objectName] = spnBasicCube;
- }
-
- if (material == 'lambert') {
- // CREATE LAMBERT CUBE MESH:
-
- var spnLambertCube = new THREE.Mesh(new THREE.CubeGeometry(l, w, depth), new THREE.MeshLambertMaterial({color: clr, wireframe: wirefrm}));
- spnLambertCube.position.x = x;
- spnLambertCube.position.y = y;
- spnLambertCube.position.z = z;
- spnLambertCube.castShadow = true;
- spnScene.add(spnLambertCube);
-
- this[globalObject + objectName] = spnLambertCube;
- }
-
- if (material == 'phong') {
- // CREATE PHONG CUBE MESH:
-
- var spnPhongCube = new THREE.Mesh(new THREE.CubeGeometry(l, w, depth), new THREE.MeshPhongMaterial({color: clr, wireframe: wirefrm}));
- spnPhongCube.position.x = x;
- spnPhongCube.position.y = y;
- spnPhongCube.position.z = z;
- spnPhongCube.castShadow = true;
- spnScene.add(spnPhongCube);
-
- this[globalObject + objectName] = spnPhongCube;
- }
-
- log('_scorpion_ has created a ' + material + ' cube.');
-}
-
-// ---------------------------------------------------------
-// SPHERES:
-
-function spnSphere(material, radius, widthSegments, heightSegments, clr, x, y, z, wirefrm, objectName) {
- if (material == 'basic') {
- // CREATE BASIC SPHERE MESH
-
- var spnBasicSphere = new THREE.Mesh(new THREE.SphereGeometry(radius, widthSegments, heightSegments, 0, Math.PI * 2, 0, Math.PI * 2), new THREE.MeshBasicMaterial({color: clr, wireframe: wirefrm}));
- spnBasicSphere.position.x = x;
- spnBasicSphere.position.y = y;
- spnBasicSphere.position.z = z;
- spnBasicSphere.castShadow = true;
- spnScene.add(spnBasicSphere);
-
- this[globalObject + objectName] = spnBasicSphere;
- }
-
- if (material == 'lambert') {
- // CREATE LAMBERT SPHERE MESH
-
- var spnLambertSphere = new THREE.Mesh(new THREE.SphereGeometry(radius, widthSegments, heightSegments, 0, Math.PI * 2, 0, Math.PI * 2), new THREE.MeshLambertMaterial({color: clr, wireframe: wirefrm}));
- spnLambertSphere.position.x = x;
- spnLambertSphere.position.y = y;
- spnLambertSphere.position.z = z;
- spnLambertSphere.castShadow = true;
- spnScene.add(spnLambertSphere);
-
- this[globalObject + objectName] = spnLambertSphere;
- }
-
- if (material == 'phong') {
- // CREATE PHONG SPHERE MESH
-
- var spnPhongSphere = new THREE.Mesh(new THREE.SphereGeometry(radius, widthSegments, heightSegments, 0, Math.PI * 2, 0, Math.PI * 2), new THREE.MeshPhongMaterial({color: clr, wireframe: wirefrm}));
- spnPhongSphere.position.x = x;
- spnPhongSphere.position.y = y;
- spnPhongSphere.position.z = z;
- spnPhongSphere.castShadow = true;
- spnScene.add(spnPhongSphere);
-
- this[globalObject + objectName] = spnPhongSphere;
- }
-
- log('_scorpion_ has created a ' + material + ' sphere.');
-}
-
-// ---------------------------------------------------------
-// WALLS:
-
-function spnWall(material, width, height, x, y, z, xRotate, yRotate, triangles, clr, wirefrm, objectName) {
- if (triangles > 100) {
- log('[c="color: red"]the triangles in this mesh are very high. you may want to lower this to maintain stable performance.[c]')
- }
-
- if (material == 'basic') {
- // CREATE BASIC WALL MESH:
-
- var spnBasicWallPlane = new THREE.Mesh(new THREE.PlaneGeometry(width, height, triangles, triangles), new THREE.MeshBasicMaterial({color: clr, opacity: 1, wireframe: wirefrm}));
- spnBasicWallPlane.position.x = x;
- spnBasicWallPlane.position.y = y;
- spnBasicWallPlane.position.z = z;
- spnBasicWallPlane.material.side = THREE.DoubleSide;
- spnBasicWallPlane.rotation.x = xRotate;
- spnBasicWallPlane.rotation.y = yRotate;
- spnScene.add(spnBasicWallPlane);
-
- this[globalObject + objectName] = spnBasicWallPlane;
- }
-
- if (material == 'lambert') {
- // CREATE LAMBERT WALL MESH:
-
- var spnLambertWallPlane = new THREE.Mesh(new THREE.PlaneGeometry(width, height, triangles, triangles), new THREE.MeshLambertMaterial({color: clr, opacity: 1, wireframe: wirefrm}));
- spnLambertWallPlane.position.x = x;
- spnLambertWallPlane.position.y = y;
- spnLambertWallPlane.position.z = z;
- spnLambertWallPlane.material.side = THREE.DoubleSide;
- spnLambertWallPlane.rotation.x = xRotate;
- spnLambertWallPlane.rotation.y = yRotate;
- spnLambertWallPlane.receiveShadow = true;
- spnScene.add(spnLambertWallPlane);
-
- this[globalObject + objectName] = spnLambertWallPlane;
- }
-
- if (material == 'phong') {
- // CREATE PHONG WALL MESH:
-
- var spnPhongWallPlane = new THREE.Mesh(new THREE.PlaneGeometry(width, height, triangles, triangles), new THREE.MeshPhongMaterial({color: clr, opacity: 1, wireframe: wirefrm}));
- spnPhongWallPlane.position.x = x;
- spnPhongWallPlane.position.y = y;
- spnPhongWallPlane.position.z = z;
- spnPhongWallPlane.material.side = THREE.DoubleSide;
- spnPhongWallPlane.rotation.x = xRotate;
- spnPhongWallPlane.rotation.y = yRotate;
- spnPhongWallPlane.receiveShadow = true;
- spnScene.add(spnPhongWallPlane);
-
- this[globalObject + objectName] = spnPhongWallPlane;
- }
-
- log('_scorpion_ has created a ' + material + ' wall.');
-}
-
-// ---------------------------------------------------------
-// FLOORS:
-
-function spnFloor(material, width, height, x, y, z, triangles, clr, wirefrm, objectName) {
- if (material == 'basic') {
- // CREATE BASIC FLOOR:
-
- var spnBasicFloorPlane = new THREE.Mesh(new THREE.PlaneGeometry(width, height, triangles, triangles), new THREE.MeshBasicMaterial({color: clr, opacity: 1, wireframe: wirefrm}));
- spnBasicFloorPlane.position.x = x;
- spnBasicFloorPlane.position.y = y;
- spnBasicFloorPlane.position.z = z;
- spnBasicFloorPlane.material.side = THREE.DoubleSide;
- spnBasicFloorPlane.rotation.x = -Math.PI / 2;
- spnScene.add(spnBasicFloorPlane);
-
- this[globalObject + objectName] = spnBasicFloorPlane;
- }
-
- if (material == 'lambert') {
- // CREATE LAMBERT FLOOR:
-
- var spnLambertFloorPlane = new THREE.Mesh(new THREE.PlaneGeometry(width, height, triangles, triangles), new THREE.MeshLambertMaterial({color: clr, opacity: 1, wireframe: wirefrm}));
- spnLambertFloorPlane.position.x = x;
- spnLambertFloorPlane.position.y = y;
- spnLambertFloorPlane.position.z = z;
- spnLambertFloorPlane.material.side = THREE.DoubleSide;
- spnLambertFloorPlane.rotation.x = -Math.PI / 2;
- spnLambertFloorPlane.receiveShadow = true;
- spnScene.add(spnLambertFloorPlane);
-
- this[globalObject + objectName] = spnLambertFloorPlane;
- }
-
- if (material == 'phong') {
- // CREATE PHONG FLOOR:
-
- var spnPhongFloorPlane = new THREE.Mesh(new THREE.PlaneGeometry(width, height, triangles, triangles), new THREE.MeshPhongMaterial({color: clr, opacity: 1, wireframe: wirefrm}));
- spnPhongFloorPlane.position.x = x;
- spnPhongFloorPlane.position.y = y;
- spnPhongFloorPlane.position.z = z;
- spnPhongFloorPlane.material.side = THREE.DoubleSide;
- spnPhongFloorPlane.rotation.x = -Math.PI / 2;
- spnPhongFloorPlane.receiveShadow = true;
- spnScene.add(spnPhongFloorPlane);
+var stats = new Stats();
+stats.showPanel(0);
- this[globalObject + objectName] = spnPhongFloorPlane;
+function displayPerformanceGraph(v) {
+ if (v == true) {
+ document.body.appendChild(stats.dom);
}
}
-// ---------------------------------------------------------
-// OBJECT AND MATERIAL LOADER:
-
-function spnObjectLoader(path, material, object, x, y, z, xRotate, yRotate, zRotate, objectName) {
- var spnMaterialLoader = new THREE.MTLLoader();
- spnMaterialLoader.setPath(path);
- spnMaterialLoader.load(material, function(importedMaterials) {
- importedMaterials.preload();
-
- // OBJECT LOADING:
-
- var spnObjectLoader = new THREE.OBJLoader();
- spnObjectLoader.setMaterials(importedMaterials);
- spnObjectLoader.setPath(path);
- spnObjectLoader.load(object, function(importedObject) {
- importedObject.position.x = x;
- importedObject.position.y = y;
- importedObject.position.z = z;
-
- importedObject.rotation.x = xRotate;
- importedObject.rotation.y = yRotate;
- importedObject.rotation.z = zRotate;
-
- spnScene.add(importedObject);
- this[globalObject + objectName] = importedObject;
- })
- })
-}
-
-// ^ this should load textures from a material and apply them to an imported mesh
-
-// ---------------------------------------------------------
-// POST PROCESSING:
-
-function spnPostProcessing(scale, shader, renderTarget, swap) {
- log('post processing ' + shader + 'pass at ' + scale + ' resolution');
- var pass;
-
- postProcessingRT = new THREE.WebGLRenderTarget(spnRenderer.width * scale, spnRenderer.height * scale);
- postProcessingComposer = new THREE.EffectComposer(spnRenderer, postProcessingRT);
+// --------------------------
- postProcessingComposer.addPass(new THREE.RenderPass(spnScene, spnCamera));
+// more organized variables:
- pass = new THREE.ShaderPass(shader);
- pass.needsSwap = swap;
+var wWidth = window.innerWidth;
+var wHeight = window.innerHeight;
- postProcessingComposer.addPass(pass);
+// --------------------------
- pass.renderToScreen = true;
-}
-
-// ---------------------------------------------------------
-// EXPERIMENTAL AND DEVELOPER FEATURES:
-
-// SECONDARY RENDER TARGET:
+// global variables/storing functions:
-function spnCreateRenderTarget(targetName) { // name -- "SecondaryTarget" -- to be rendered into
- // create a seperate render target for pulling per-pixel data and depth info.
+var scorpionObject = "scpn";
- this[globalRenderTarget + targetName] = new THREE.WebGLRenderTarget(spnRenderer.width, spnRenderer.height);
+function storeObject(objectName, object) {
+ this[scorpionObject + objectName] = object;
- // ^^ should create a custom render target that gets rendered to ("SecondaryTarget")
- // ADD A BUFFER FOR GRABBING PER PIXEL INFO
-
- spnCustomRenderTarget = true;
- log('a new _render target_ has been created');
+ log(object);
}
-// TESTING:
-
-// no testing functions now
\ No newline at end of file