diff --git a/src/RayTracingRenderer.js b/src/RayTracingRenderer.js index 24fa396..0a71325 100644 --- a/src/RayTracingRenderer.js +++ b/src/RayTracingRenderer.js @@ -16,7 +16,7 @@ export function RayTracingRenderer(params = {}) { const gl = canvas.getContext('webgl2', { alpha: false, - depth: false, + depth: true, stencil: false, antialias: false, powerPreference: 'high-performance', diff --git a/src/renderer/Framebuffer.js b/src/renderer/Framebuffer.js index a02c9fc..9be119c 100644 --- a/src/renderer/Framebuffer.js +++ b/src/renderer/Framebuffer.js @@ -1,4 +1,4 @@ -export function makeFramebuffer(gl, { attachments }) { +export function makeFramebuffer(gl, { color, depth }) { const framebuffer = gl.createFramebuffer(); @@ -15,27 +15,31 @@ export function makeFramebuffer(gl, { attachments }) { const drawBuffers = []; - for (let location in attachments) { + for (let location in color) { location = Number(location); if (location === undefined) { console.error('invalid location'); } - const tex = attachments[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 { - attachments, + color, bind, unbind }; diff --git a/src/renderer/FullscreenQuad.js b/src/renderer/FullscreenQuad.js index 076c32c..1e48c66 100644 --- a/src/renderer/FullscreenQuad.js +++ b/src/renderer/FullscreenQuad.js @@ -2,7 +2,10 @@ import vertex from './glsl/fullscreenQuad.vert'; import { makeVertexShader } from './RenderPass'; export function makeFullscreenQuad(gl) { - // TODO: use VAOs + 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); @@ -12,9 +15,12 @@ export function makeFullscreenQuad(gl) { 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); } diff --git a/src/renderer/GBufferPass.js b/src/renderer/GBufferPass.js new file mode 100644 index 0000000..85683f0 --- /dev/null +++ b/src/renderer/GBufferPass.js @@ -0,0 +1,92 @@ +import { makeRenderPass } from './RenderPass'; +import vertex from './glsl/gBuffer.vert'; +import fragment from './glsl/gBuffer.frag'; +import { Matrix4 } from 'three'; + +export function makeGBufferPass(gl, { materialBuffer, mergedMesh }) { + const renderPass = makeRenderPass(gl, { + defines: materialBuffer.defines, + vertex, + 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 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) { + 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'; + } +} diff --git a/src/renderer/MaterialBuffer.js b/src/renderer/MaterialBuffer.js new file mode 100644 index 0000000..f172429 --- /dev/null +++ b/src/renderer/MaterialBuffer.js @@ -0,0 +1,175 @@ +import { ThinMaterial, ThickMaterial, ShadowCatcherMaterial } from '../constants'; +import materialBufferChunk from './glsl/chunks/materialBuffer.glsl'; +import { makeUniformBuffer } from './UniformBuffer'; +import { makeRenderPass } from "./RenderPass"; +import { makeTexture } from './Texture'; +import { getTexturesFromMaterials, mergeTexturesFromMaterials } from './texturesFromMaterials'; + +export 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: [ materialBufferChunk ], + 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; +} diff --git a/src/renderer/RayTracePass.js b/src/renderer/RayTracePass.js index 0e87e64..a60de22 100644 --- a/src/renderer/RayTracePass.js +++ b/src/renderer/RayTracePass.js @@ -1,22 +1,19 @@ import { bvhAccel, flattenBvh } from './bvhAccel'; -import { ThinMaterial, ThickMaterial, ShadowCatcherMaterial } from '../constants'; import { generateEnvMapFromSceneComponents, generateBackgroundMapFromSceneBackground } from './envMapCreation'; import { envmapDistribution } from './envmapDistribution'; import fragment from './glsl/rayTrace.frag'; -import { mergeMeshesToGeometry } from './mergeMeshesToGeometry'; import { makeRenderPass } from './RenderPass'; import { makeStratifiedSamplerCombined } from './StratifiedSamplerCombined'; import { makeTexture } from './Texture'; -import { getTexturesFromMaterials, mergeTexturesFromMaterials } from './texturesFromMaterials'; -import * as THREE from 'three'; -import { uploadBuffers } from './uploadBuffers'; import { clamp } from './util'; export function makeRayTracePass(gl, { bounces, // number of global illumination bounces + decomposedScene, fullscreenQuad, + materialBuffer, + mergedMesh, optionalExtensions, - scene, }) { bounces = clamp(bounces, 1, 6); @@ -36,7 +33,7 @@ export function makeRayTracePass(gl, { let samples; const renderPass = makeRenderPassFromScene({ - bounces, fullscreenQuad, gl, optionalExtensions, samplingDimensions, scene + bounces, decomposedScene, fullscreenQuad, gl, materialBuffer, mergedMesh, optionalExtensions, samplingDimensions, }); function setSize(width, height) { @@ -47,9 +44,9 @@ export function makeRayTracePass(gl, { function setNoise(noiseImage) { renderPass.setTexture('noise', makeTexture(gl, { data: noiseImage, - minFilter: gl.NEAREST, - magFilter: gl.NEAREST, - storage: 'float' + wrapS: gl.REPEAT, + wrapT: gl.REPEAT, + storage: 'halfFloat', })); } @@ -63,12 +60,19 @@ export function makeRayTracePass(gl, { 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 @@ -100,6 +104,7 @@ export function makeRayTracePass(gl, { outputLocs: renderPass.outputLocs, setCamera, setJitter, + setGBuffers, setNoise, setSize, setStrataCount, @@ -107,34 +112,25 @@ export function makeRayTracePass(gl, { } function makeRenderPassFromScene({ bounces, + decomposedScene, fullscreenQuad, gl, + materialBuffer, + mergedMesh, optionalExtensions, samplingDimensions, - scene, }) { const { OES_texture_float_linear } = optionalExtensions; - const { meshes, directionalLights, ambientLights, environmentLights } = decomposeScene(scene); - if (meshes.length === 0) { - throw 'RayTracingRenderer: Scene contains no renderable meshes.'; - } - - // merge meshes in scene to a single, static geometry - const { geometry, materials, materialIndices } = mergeMeshesToGeometry(meshes); + const { background, directionalLights, ambientLights, environmentLights } = decomposedScene; - // extract textures shared by meshes in scene - const maps = getTexturesFromMaterials(materials, ['map', 'normalMap']); - const pbrMap = mergeTexturesFromMaterials(materials, ['roughnessMap', 'metalnessMap']); + const { geometry, materials, materialIndices } = mergedMesh; // create bounding volume hierarchy from a static scene const bvh = bvhAccel(geometry, materialIndices); const flattenedBvh = flattenBvh(bvh); const numTris = geometry.index.count / 3; - const useGlass = materials.some(m => m.transparent); - const useShadowCatcher = materials.some(m => m.shadowCatcher); - const renderPass = makeRenderPass(gl, { defines: { OES_texture_float_linear, @@ -142,60 +138,19 @@ function makeRenderPassFromScene({ INDEX_COLUMNS: textureDimensionsFromArray(numTris).columnsLog, VERTEX_COLUMNS: textureDimensionsFromArray(geometry.attributes.position.count).columnsLog, STACK_SIZE: flattenedBvh.maxDepth, - NUM_TRIS: numTris, - NUM_MATERIALS: materials.length, - NUM_DIFFUSE_MAPS: maps.map.textures.length, - NUM_NORMAL_MAPS: maps.normalMap.textures.length, - NUM_DIFFUSE_NORMAL_MAPS: Math.max(maps.map.textures.length, maps.normalMap.textures.length), - NUM_PBR_MAPS: pbrMap.textures.length, BOUNCES: bounces, - USE_GLASS: useGlass, - USE_SHADOW_CATCHER: useShadowCatcher, - SAMPLING_DIMENSIONS: samplingDimensions.reduce((a, b) => a + b) + 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, vertex: fullscreenQuad.vertexShader }); - 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); - renderPass.setTexture('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); - renderPass.setTexture('normalMap', texture); - bufferData.normalMapSize = relativeSizes; - bufferData.normalMapIndex = maps.normalMap.indices; - } - - if (pbrMap.textures.length > 0) { - const { relativeSizes, texture } = makeTextureArray(gl, pbrMap.textures, false); - renderPass.setTexture('pbrMap', texture); - bufferData.pbrMapSize = relativeSizes; - bufferData.roughnessMapIndex = pbrMap.indices.roughnessMap; - bufferData.metalnessMapIndex = pbrMap.indices.metalnessMap; - } - - uploadBuffers(gl, renderPass.program, bufferData); + 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)); @@ -208,6 +163,7 @@ function makeRenderPassFromScene({ 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, @@ -217,10 +173,11 @@ function makeRenderPassFromScene({ renderPass.setTexture('envmap', envImageTextureObject); let backgroundImageTextureObject; - if (scene.background) { - const backgroundImage = generateBackgroundMapFromSceneBackground(scene.background); + 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, @@ -236,8 +193,7 @@ function makeRenderPassFromScene({ renderPass.setTexture('envmapDistribution', makeTexture(gl, { data: distribution.data, - minFilter: gl.NEAREST, - magFilter: gl.NEAREST, + storage: 'halfFloat', width: distribution.width, height: distribution.height, })); @@ -245,46 +201,6 @@ function makeRenderPassFromScene({ return renderPass; } -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'); - } - } - }); - - return { - meshes, directionalLights, ambientLights, environmentLights - }; -} - function textureDimensionsFromArray(count) { const columnsLog = Math.round(Math.log2(Math.sqrt(count))); const columns = 2 ** columnsLog; @@ -301,63 +217,14 @@ function makeDataTexture(gl, dataArray, channels) { const textureDim = textureDimensionsFromArray(dataArray.length / channels); return makeTexture(gl, { data: padArray(dataArray, channels * textureDim.size), - minFilter: gl.NEAREST, - magFilter: gl.NEAREST, width: textureDim.columns, height: textureDim.rows, }); } -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 - }); - - 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 }; -} - // expand array to the given length function padArray(typedArray, length) { const newArray = new typedArray.constructor(length); newArray.set(typedArray); return newArray; } - -function isHDRTexture(texture) { - return texture.map - && texture.map.image - && (texture.map.encoding === THREE.RGBEEncoding || texture.map.encoding === THREE.LinearEncoding); -} diff --git a/src/renderer/RenderPass.js b/src/renderer/RenderPass.js index 5d5cc95..bb020be 100644 --- a/src/renderer/RenderPass.js +++ b/src/renderer/RenderPass.js @@ -1,4 +1,4 @@ -import { compileShader, createProgram } from './glUtil'; +import { compileShader, createProgram, getAttributes } from './glUtil'; import { makeUniformSetter } from './UniformSetter'; export function makeRenderPass(gl, params) { @@ -28,7 +28,6 @@ export function makeFragmentShader(gl, { defines, fragment }) { return makeShaderStage(gl, gl.FRAGMENT_SHADER, fragment, defines); } - function makeRenderPassFromProgram(gl, program) { const uniformSetter = makeUniformSetter(gl, program); @@ -38,19 +37,22 @@ function makeRenderPassFromProgram(gl, program) { let nextTexUnit = 1; function setTexture(name, texture) { - let cachedTex = textures[name]; + if (!texture) { + return; + } - if (!cachedTex) { + if (!textures[name]) { const unit = nextTexUnit++; uniformSetter.setUniform(name, unit); - cachedTex = { unit }; - - textures[name] = cachedTex; + textures[name] = { + unit, + tex: texture + }; + } else { + textures[name].tex = texture; } - - cachedTex.tex = texture; } function bindTextures() { @@ -70,6 +72,7 @@ function makeRenderPassFromProgram(gl, program) { } return { + attribLocs: getAttributes(gl, program), bindTextures, program, setTexture, @@ -86,7 +89,7 @@ function makeShaderStage(gl, type, shader, defines) { str += addDefines(defines); } - if (type === gl.FRAGMENT_SHADER) { + if (type === gl.FRAGMENT_SHADER && shader.outputs) { str += addOutputs(shader.outputs); } diff --git a/src/renderer/RenderingPipeline.js b/src/renderer/RenderingPipeline.js index a58dc60..c79bf48 100644 --- a/src/renderer/RenderingPipeline.js +++ b/src/renderer/RenderingPipeline.js @@ -1,13 +1,16 @@ +import { decomposeScene } from './decomposeScene'; +import { makeFramebuffer } from './Framebuffer'; import { makeFullscreenQuad } from './FullscreenQuad'; +import { makeGBufferPass } from './GBufferPass'; +import { makeMaterialBuffer } from './MaterialBuffer'; +import { mergeMeshesToGeometry } from './mergeMeshesToGeometry'; import { makeRayTracePass } from './RayTracePass'; +import { makeReprojectPass } from './ReprojectPass'; import { makeToneMapPass } from './ToneMapPass'; -import { makeFramebuffer } from './Framebuffer'; -import { numberArraysEqual } from './util'; +import { clamp, numberArraysEqual } from './util'; import { makeTileRender } from './TileRender'; -import { makeTexture } from './Texture'; -import { makeReprojectPass } from './ReprojectPass'; +import { makeDepthTarget, makeTexture } from './Texture'; import noiseBase64 from './texture/noise'; -import { clamp } from './util'; import { PerspectiveCamera, Vector2 } from 'three'; export function makeRenderingPipeline({ @@ -27,21 +30,25 @@ export function makeRenderingPipeline({ // higher number results in faster convergence over time, but with lower quality initial samples const strataCount = 6; + const decomposedScene = decomposeScene(scene); + + const mergedMesh = mergeMeshesToGeometry(decomposedScene.meshes); + + const materialBuffer = makeMaterialBuffer(gl, mergedMesh.materials); + const fullscreenQuad = makeFullscreenQuad(gl); - const rayTracePass = makeRayTracePass(gl, { bounces, fullscreenQuad, optionalExtensions, scene }); + const rayTracePass = makeRayTracePass(gl, { bounces, decomposedScene, fullscreenQuad, materialBuffer, mergedMesh, optionalExtensions, scene }); const reprojectPass = makeReprojectPass(gl, { fullscreenQuad, maxReprojectedSamples }); - const toneMapPass = makeToneMapPass(gl, { - fullscreenQuad, optionalExtensions, toneMappingParams - }); + 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); - const clearToBlack = new Float32Array([0, 0, 0, 0]); - let ready = false; const noiseImage = new Image(); noiseImage.src = noiseBase64; @@ -50,6 +57,14 @@ export function makeRenderingPipeline({ ready = true; }; + let sampleCount = 0; + + let sampleRenderedCallback = () => {}; + + const lastCamera = new PerspectiveCamera(); + lastCamera.position.set(1, 1, 1); + lastCamera.updateMatrixWorld(); + let screenWidth = 0; let screenHeight = 0; @@ -64,29 +79,19 @@ export function makeRenderingPipeline({ let reprojectBuffer; let reprojectBackBuffer; - let lastToneMappedScale; - let lastToneMappedTexture; + let gBuffer; + let gBufferBack; - const lastCamera = new PerspectiveCamera(); - lastCamera.position.set(1, 1, 1); - lastCamera.updateMatrixWorld(); - - let sampleCount = 0; - - let sampleRenderedCallback = () => {}; + let lastToneMappedTexture; + let lastToneMappedScale; function initFrameBuffers(width, height) { - const floatTex = () => makeTexture(gl, { width, height, storage: 'float' }); - const makeHdrBuffer = () => makeFramebuffer(gl, { - attachments: { - [rayTracePass.outputLocs.light]: floatTex(), - [rayTracePass.outputLocs.position]: floatTex(), - } - }); + color: { 0: makeTexture(gl, { width, height, storage: 'float', magFilter: gl.LINEAR, minFilter: gl.LINEAR }) } + }); const makeReprojectBuffer = () => makeFramebuffer(gl, { - attachments: { 0: floatTex() } + color: { 0: makeTexture(gl, { width, height, storage: 'float', magFilter: gl.LINEAR, minFilter: gl.LINEAR }) } }); hdrBuffer = makeHdrBuffer(); @@ -95,8 +100,28 @@ export function makeRenderingPipeline({ 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; - lastToneMappedTexture = hdrBuffer.attachments[rayTracePass.outputLocs.light]; } function swapReprojectBuffer() { @@ -105,6 +130,12 @@ export function makeRenderingPipeline({ reprojectBackBuffer = temp; } + function swapGBuffer() { + let temp = gBuffer; + gBuffer = gBufferBack; + gBufferBack = temp; + } + function swapHdrBuffer() { let temp = hdrBuffer; hdrBuffer = hdrBackBuffer; @@ -114,8 +145,9 @@ export function makeRenderingPipeline({ // Shaders will read from the back buffer and draw to the front buffer // Buffers are swapped after every render function swapBuffers() { - swapHdrBuffer(); swapReprojectBuffer(); + swapGBuffer(); + swapHdrBuffer(); } function setSize(w, h) { @@ -144,6 +176,24 @@ export function makeRenderingPipeline({ 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); @@ -157,8 +207,6 @@ export function makeRenderingPipeline({ gl.blendFunc(gl.ONE, gl.ONE); gl.enable(gl.BLEND); - gl.clearBufferfv(gl.COLOR, rayTracePass.outputLocs.position, clearToBlack); - gl.viewport(0, 0, width, height); rayTracePass.draw(); @@ -173,15 +221,32 @@ export function makeRenderingPipeline({ buffer.unbind(); } - function toneMapToScreen(lightTexture, textureScale) { + function toneMapToScreen(lightTexture, lightScale) { gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); toneMapPass.draw({ light: lightTexture, - textureScale + lightScale, + position: gBuffer.color[gBufferPass.outputLocs.position], }); lastToneMappedTexture = lightTexture; - lastToneMappedScale = textureScale; + 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) { @@ -191,26 +256,6 @@ export function makeRenderingPipeline({ gl.disable(gl.SCISSOR_TEST); } - function updateSeed(width, height) { - rayTracePass.setSize(width, height); - - const jitterX = (Math.random() - 0.5) / width; - const jitterY = (Math.random() - 0.5) / height; - rayTracePass.setJitter(jitterX, jitterY); - reprojectPass.setJitter(jitterX, jitterY); - - if (sampleCount === 0) { - rayTracePass.setStrataCount(1); - } else if (sampleCount === numUniformSamples) { - rayTracePass.setStrataCount(strataCount); - } else { - rayTracePass.nextSeed(); - } - - rayTracePass.bindTextures(); - } - - function drawPreview(camera, lastCamera) { if (sampleCount > 0) { swapBuffers(); @@ -220,27 +265,32 @@ export function makeRenderingPipeline({ tileRender.reset(); setPreviewBufferDimensions(); + updateSeed(previewWidth, previewHeight, false); + rayTracePass.setCamera(camera); + gBufferPass.setCamera(camera); reprojectPass.setPreviousCamera(lastCamera); lastCamera.copy(camera); - updateSeed(previewWidth, previewHeight); + renderGBuffer(); + + rayTracePass.bindTextures(); newSampleToBuffer(hdrBuffer, previewWidth, previewHeight); reprojectBuffer.bind(); gl.viewport(0, 0, previewWidth, previewHeight); reprojectPass.draw({ blendAmount: 1.0, - light: hdrBuffer.attachments[rayTracePass.outputLocs.light], - position: hdrBuffer.attachments[rayTracePass.outputLocs.position], - textureScale: previewScale, + light: hdrBuffer.color[0], + lightScale: previewScale, + position: gBuffer.color[gBufferPass.outputLocs.position], previousLight: lastToneMappedTexture, - previousPosition: hdrBackBuffer.attachments[rayTracePass.outputLocs.position], - previousTextureScale: lastToneMappedScale, + previousLightScale: lastToneMappedScale, + previousPosition: gBufferBack.color[gBufferPass.outputLocs.position], }); reprojectBuffer.unbind(); - toneMapToScreen(reprojectBuffer.attachments[0], previewScale); + toneMapToScreen(reprojectBuffer.color[0], previewScale); swapBuffers(); } @@ -255,7 +305,9 @@ export function makeRenderingPipeline({ reprojectPass.setPreviousCamera(lastCamera); } - updateSeed(screenWidth, screenHeight); + updateSeed(screenWidth, screenHeight, true); + renderGBuffer(); + rayTracePass.bindTextures(); } renderTile(hdrBuffer, x, y, tileWidth, tileHeight); @@ -271,18 +323,18 @@ export function makeRenderingPipeline({ gl.viewport(0, 0, screenWidth, screenHeight); reprojectPass.draw({ blendAmount, - light: hdrBuffer.attachments[rayTracePass.outputLocs.light], - position: hdrBuffer.attachments[rayTracePass.outputLocs.position], - textureScale: fullscreenScale, - previousLight: reprojectBackBuffer.attachments[0], - previousPosition: hdrBackBuffer.attachments[rayTracePass.outputLocs.position], - previousTextureScale: previewScale, + 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.attachments[0], fullscreenScale); + toneMapToScreen(reprojectBuffer.color[0], fullscreenScale); } else { - toneMapToScreen(hdrBuffer.attachments[rayTracePass.outputLocs.light], fullscreenScale); + toneMapToScreen(hdrBuffer.color[0], fullscreenScale); } sampleRenderedCallback(sampleCount); @@ -309,6 +361,9 @@ export function makeRenderingPipeline({ return; } + swapGBuffer(); + swapReprojectBuffer(); + if (sampleCount === 0) { reprojectPass.setPreviousCamera(lastCamera); } @@ -316,34 +371,34 @@ export function makeRenderingPipeline({ if (!areCamerasEqual(camera, lastCamera)) { sampleCount = 0; rayTracePass.setCamera(camera); + gBufferPass.setCamera(camera); lastCamera.copy(camera); - swapHdrBuffer(); clearBuffer(hdrBuffer); } else { sampleCount++; } - updateSeed(screenWidth, screenHeight); + updateSeed(screenWidth, screenHeight, true); + + renderGBuffer(camera); + rayTracePass.bindTextures(); addSampleToBuffer(hdrBuffer, screenWidth, screenHeight); reprojectBuffer.bind(); gl.viewport(0, 0, screenWidth, screenHeight); reprojectPass.draw({ blendAmount: 1.0, - light: hdrBuffer.attachments[rayTracePass.outputLocs.light], - position: hdrBuffer.attachments[rayTracePass.outputLocs.position], - previousLight: reprojectBackBuffer.attachments[0], - previousPosition: hdrBackBuffer.attachments[rayTracePass.outputLocs.position], - textureScale: fullscreenScale, - previousTextureScale: fullscreenScale - + 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.attachments[0], fullscreenScale); - - swapReprojectBuffer(); + toneMapToScreen(reprojectBuffer.color[0], fullscreenScale); } return { diff --git a/src/renderer/ReprojectPass.js b/src/renderer/ReprojectPass.js index 6beb7bf..5c48a71 100644 --- a/src/renderer/ReprojectPass.js +++ b/src/renderer/ReprojectPass.js @@ -32,16 +32,16 @@ export function makeReprojectPass(gl, params) { const { blendAmount, light, + lightScale, position, previousLight, + previousLightScale, previousPosition, - textureScale, - previousTextureScale, } = params; renderPass.setUniform('blendAmount', blendAmount); - renderPass.setUniform('textureScale', textureScale.x, textureScale.y); - renderPass.setUniform('previousTextureScale', previousTextureScale.x, previousTextureScale.y); + renderPass.setUniform('lightScale', lightScale.x, lightScale.y); + renderPass.setUniform('previousLightScale', previousLightScale.x, previousLightScale.y); renderPass.setTexture('light', light); renderPass.setTexture('position', position); diff --git a/src/renderer/Texture.js b/src/renderer/Texture.js index 8e38371..f5c9557 100644 --- a/src/renderer/Texture.js +++ b/src/renderer/Texture.js @@ -24,10 +24,10 @@ export function makeTexture(gl, params) { // sampling properties gammaCorrection = false, - wrapS = gl.REPEAT, - wrapT = gl.REPEAT, - minFilter = gl.LINEAR, - magFilter = gl.LINEAR, + wrapS = gl.CLAMP_TO_EDGE, + wrapT = gl.CLAMP_TO_EDGE, + minFilter = gl.NEAREST, + magFilter = gl.NEAREST, } = params; width = width || data.width || 0; @@ -64,45 +64,7 @@ export function makeTexture(gl, params) { channels = clamp(channels, 1, 4); - const format = [ - gl.RED, - gl.RG, - gl.RGB, - gl.RGBA - ][channels - 1]; - - const isByteArray = - storage === 'byte' || - data instanceof Uint8Array || - data instanceof HTMLImageElement || - data instanceof HTMLCanvasElement || - data instanceof ImageData; - - const isFloatArray = - storage === 'float' || - data instanceof Float32Array; - - let type; - let internalFormat; - if (isByteArray) { - type = gl.UNSIGNED_BYTE; - internalFormat = [ - gl.R8, - gl.RG8, - gammaCorrection ? gl.SRGB8 : gl.RGB8, - gammaCorrection ? gl.SRGB8_ALPHA8 : gl.RGBA8 - ][channels - 1]; - } else if (isFloatArray) { - type = gl.FLOAT; - internalFormat = [ - gl.R32F, - gl.RG32F, - gl.RGB32F, - gl.RGBA32F - ][channels - 1]; - } else { - console.error('Texture of unknown type:', storage || data); - } + const { type, format, internalFormat } = getTextureFormat(gl, channels, storage, data, gammaCorrection); if (dataArray) { gl.texStorage3D(target, 1, internalFormat, width, height, dataArray.length); @@ -135,3 +97,91 @@ export function makeTexture(gl, params) { texture }; } + +export 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 getFormat(gl, channels) { + const map = { + 1: gl.RED, + 2: gl.RG, + 3: gl.RGB, + 4: gl.RGBA + }; + return map[channels]; +} + +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 + }; +} diff --git a/src/renderer/ToneMapPass.js b/src/renderer/ToneMapPass.js index 82f9efd..4ddc611 100644 --- a/src/renderer/ToneMapPass.js +++ b/src/renderer/ToneMapPass.js @@ -13,34 +13,41 @@ const toneMapFunctions = { export function makeToneMapPass(gl, params) { const { fullscreenQuad, - // optionalExtensions, toneMappingParams } = params; - // const { OES_texture_float_linear } = optionalExtensions; - const { toneMapping, whitePoint, exposure } = toneMappingParams; - - const renderPass = makeRenderPass(gl, { + const renderPassConfig = { gl, defines: { - // OES_texture_float_linear, - TONE_MAPPING: toneMapFunctions[toneMapping] || 'linear', - WHITE_POINT: whitePoint.toExponential(), // toExponential allows integers to be represented as GLSL floats - EXPOSURE: exposure.toExponential() + 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, - }); + }; + + 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, - textureScale + lightScale, + position } = params; - renderPass.setUniform('textureScale', textureScale.x, textureScale.y); + 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(); diff --git a/src/renderer/UniformBuffer.js b/src/renderer/UniformBuffer.js new file mode 100644 index 0000000..659a2ae --- /dev/null +++ b/src/renderer/UniformBuffer.js @@ -0,0 +1,92 @@ +export 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); + } + } +} diff --git a/src/renderer/UniformSetter.js b/src/renderer/UniformSetter.js index 8b24f9e..7205fe0 100644 --- a/src/renderer/UniformSetter.js +++ b/src/renderer/UniformSetter.js @@ -7,7 +7,9 @@ export function makeUniformSetter(gl, program) { const uniforms = {}; const needsUpload = []; - for (let { name, type, location } of uniformInfo) { + for (let name in uniformInfo) { + const { type, location } = uniformInfo[name]; + const uniform = { type, location, @@ -28,10 +30,10 @@ export function makeUniformSetter(gl, program) { const uni = uniforms[name]; if (!uni) { - if (!failedUnis.has(name)) { - console.warn(`Uniform "${name}" does not exist in shader`); - failedUnis.add(name); - } + // if (!failedUnis.has(name)) { + // console.warn(`Uniform "${name}" does not exist in shader`); + // failedUnis.add(name); + // } return; } diff --git a/src/renderer/bvhAccel.js b/src/renderer/bvhAccel.js index 3131c5d..35c4044 100644 --- a/src/renderer/bvhAccel.js +++ b/src/renderer/bvhAccel.js @@ -7,8 +7,8 @@ import { partition, nthElement } from './bvhUtil'; const size = new Vector3(); -export function bvhAccel(geometry, materialIndices) { - const primitiveInfo = makePrimitiveInfo(geometry, materialIndices); +export function bvhAccel(geometry) { + const primitiveInfo = makePrimitiveInfo(geometry); const node = recursiveBuild(primitiveInfo, 0, primitiveInfo.length); return node; @@ -89,10 +89,12 @@ export function flattenBvh(bvh) { }; } -function makePrimitiveInfo(geometry, materialIndices) { +function makePrimitiveInfo(geometry) { const primitiveInfo = []; const indices = geometry.getIndex().array; const position = geometry.getAttribute('position'); + const materialMeshIndex = geometry.getAttribute('materialMeshIndex'); + const v0 = new Vector3(); const v1 = new Vector3(); const v2 = new Vector3(); @@ -100,11 +102,15 @@ function makePrimitiveInfo(geometry, materialIndices) { const e1 = new 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 Box3(); - v0.fromBufferAttribute(position, indices[i]); - v1.fromBufferAttribute(position, indices[i + 1]); - v2.fromBufferAttribute(position, indices[i + 2]); + v0.fromBufferAttribute(position, i0); + v1.fromBufferAttribute(position, i1); + v2.fromBufferAttribute(position, i2); e0.subVectors(v2, v0); e1.subVectors(v1, v0); @@ -115,9 +121,9 @@ function makePrimitiveInfo(geometry, materialIndices) { const info = { bounds: bounds, center: bounds.getCenter(new Vector3()), - indices: [indices[i], indices[i + 1], indices[i + 2]], + indices: [i0, i1, i2], faceNormal: new Vector3().crossVectors(e1, e0).normalize(), - materialIndex: materialIndices[i / 3] + materialIndex: materialMeshIndex.getX(i0) }; primitiveInfo.push(info); diff --git a/src/renderer/decomposeScene.js b/src/renderer/decomposeScene.js new file mode 100644 index 0000000..de74727 --- /dev/null +++ b/src/renderer/decomposeScene.js @@ -0,0 +1,50 @@ +import * as THREE from 'three'; + +export 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.RGBEEncoding || texture.map.encoding === THREE.LinearEncoding); +} diff --git a/src/renderer/glUtil.js b/src/renderer/glUtil.js index 66fc187..27761c1 100644 --- a/src/renderer/glUtil.js +++ b/src/renderer/glUtil.js @@ -46,110 +46,32 @@ export function createProgram(gl, vertexShader, fragmentShader, transformVarying } export function getUniforms(gl, program) { - const uniforms = []; + 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.push({ - name, type, location - }); + uniforms[name] = { + type, location + }; } } return uniforms; } -export 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; - } +export function getAttributes(gl, program) { + const attributes = {}; - 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'); + 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); } } - function bind(index) { - 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); - } - } + return attributes; } diff --git a/src/renderer/glsl/chunks/constants.glsl b/src/renderer/glsl/chunks/constants.glsl new file mode 100644 index 0000000..902efc1 --- /dev/null +++ b/src/renderer/glsl/chunks/constants.glsl @@ -0,0 +1,10 @@ +export default ` + #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 +` diff --git a/src/renderer/glsl/chunks/intersect.glsl b/src/renderer/glsl/chunks/intersect.glsl index 6045a3d..25182b1 100644 --- a/src/renderer/glsl/chunks/intersect.glsl +++ b/src/renderer/glsl/chunks/intersect.glsl @@ -1,47 +1,17 @@ export default ` -uniform highp isampler2D indices; uniform sampler2D positions; uniform sampler2D normals; uniform sampler2D uvs; uniform sampler2D bvh; -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 - struct Triangle { vec3 p0; vec3 p1; vec3 p2; }; -void surfaceInteractionFromIntersection(inout SurfaceInteraction si, Triangle tri, vec3 barycentric, ivec3 index, vec3 faceNormal, int materialIndex) { +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; @@ -52,90 +22,30 @@ void surfaceInteractionFromIntersection(inout SurfaceInteraction si, Triangle tr vec3 n0 = texelFetch(normals, i0, 0).xyz; vec3 n1 = texelFetch(normals, i1, 0).xyz; vec3 n2 = texelFetch(normals, i2, 0).xyz; - si.normal = normalize(barycentric.x * n0 + barycentric.y * n1 + barycentric.z * n2); - - si.color = materials.colorAndMaterialType[materialIndex].xyz; - si.roughness = materials.roughnessMetalnessNormalScale[materialIndex].x; - si.metalness = materials.roughnessMetalnessNormalScale[materialIndex].y; - - si.materialType = int(materials.colorAndMaterialType[materialIndex].w); - - // TODO: meshId should be the actual mesh id instead of the material id, which can be shared amoung meshes. - // This will involve storing the mesh id AND the material id in the BVH texture - si.meshId = materialIndex + 1; // +1 so that the mesh id is never 0 + 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(); #endif - #ifdef NUM_DIFFUSE_MAPS - int diffuseMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].x; - if (diffuseMapIndex >= 0) { - si.color *= texture(diffuseMap, vec3(uv * materials.diffuseNormalMapSize[diffuseMapIndex].xy, diffuseMapIndex)).rgb; - } - #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 - int normalMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].y; - if (normalMapIndex >= 0) { - vec2 duv02 = uv0 - uv2; - vec2 duv12 = uv1 - uv2; - vec3 dp02 = tri.p0 - tri.p2; - vec3 dp12 = tri.p1 - tri.p2; - - // Method One - // http://www.pbr-book.org/3ed-2018/Shapes/Triangle_Meshes.html#fragment-Computetrianglepartialderivatives-0 - // Compute tangent vectors relative to the face normal. These vectors won't necessarily be orthogonal to the smoothed normal - // This means the TBN matrix won't be orthogonal which is technically incorrect. - // This is Three.js's method (https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderChunk/normalmap_pars_fragment.glsl.js) - // -------------- - // float scale = sign(duv02.x * duv12.y - duv02.y * duv12.x); - // vec3 dpdu = normalize((duv12.y * dp02 - duv02.y * dp12) * scale); - // vec3 dpdv = normalize((-duv12.x * dp02 + duv02.x * dp12) * scale); - - // Method Two - // Compute tangent vectors as in Method One but apply Gram-Schmidt process to make vectors orthogonal to smooth normal - // This might inadvertently flip coordinate space orientation - // -------------- - // float scale = sign(duv02.x * duv12.y - duv02.y * duv12.x); - // vec3 dpdu = normalize((duv12.y * dp02 - duv02.y * dp12) * scale); - // dpdu = (dpdu - dot(dpdu, si.normal) * si.normal); // Gram-Schmidt process - // vec3 dpdv = cross(si.normal, dpdu) * scale; - - // Method Three - // http://www.thetenthplanet.de/archives/1180 - // Compute co-tangent and co-bitangent vectors - // These vectors are orthongal and maintain a consistent coordinate space - // -------------- - vec3 dp12perp = cross(dp12, si.normal); - vec3 dp02perp = cross(si.normal, dp02); - vec3 dpdu = dp12perp * duv02.x + dp02perp * duv12.x; - vec3 dpdv = dp12perp * duv02.y + dp02perp * duv12.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, si.normal); - - si.normal = normalize(tbn * n); - } - #endif - - #ifdef NUM_PBR_MAPS - int roughnessMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].z; - int metalnessMapIndex = materials.diffuseNormalRoughnessMetalnessMapIndex[materialIndex].w; - if (roughnessMapIndex >= 0) { - si.roughness *= texture(pbrMap, vec3(uv * materials.pbrMapSize[roughnessMapIndex].xy, roughnessMapIndex)).g; - } - if (metalnessMapIndex >= 0) { - si.metalness *= texture(pbrMap, vec3(uv * materials.pbrMapSize[metalnessMapIndex].xy, metalnessMapIndex)).b; - } + 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 } @@ -236,8 +146,8 @@ int maxDimension(vec3 v) { } // Traverse BVH, find closest triangle intersection, and return surface information -SurfaceInteraction intersectScene(inout Ray ray) { - SurfaceInteraction si; +void intersectScene(inout Ray ray, inout SurfaceInteraction si) { + si.hit = false; int maxDim = maxDimension(abs(ray.d)); @@ -294,16 +204,14 @@ SurfaceInteraction intersectScene(inout Ray ray) { ray.tMax = hit.t; int materialIndex = floatBitsToInt(r2.w); vec3 faceNormal = r2.xyz; - surfaceInteractionFromIntersection(si, tri, hit.barycentric, index, faceNormal, materialIndex); + 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, 0.03, 1.0); + si.roughness = clamp(si.roughness, ROUGHNESS_MIN, 1.0); si.metalness = clamp(si.metalness, 0.0, 1.0); - - return si; } bool intersectSceneShadow(inout Ray ray) { diff --git a/src/renderer/glsl/chunks/materialBuffer.glsl b/src/renderer/glsl/chunks/materialBuffer.glsl new file mode 100644 index 0000000..afcdee1 --- /dev/null +++ b/src/renderer/glsl/chunks/materialBuffer.glsl @@ -0,0 +1,100 @@ +export default ` + +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 +`; diff --git a/src/renderer/glsl/chunks/random.glsl b/src/renderer/glsl/chunks/random.glsl index 1c6765f..7be8ff4 100644 --- a/src/renderer/glsl/chunks/random.glsl +++ b/src/renderer/glsl/chunks/random.glsl @@ -12,8 +12,6 @@ uniform float strataSize; // This allows us to use stratified sampling for each random variable in our path tracing int sampleIndex = 0; -const highp float maxUint = 1.0 / 4294967295.0; - float pixelSeed; void initRandom() { diff --git a/src/renderer/glsl/chunks/core.glsl b/src/renderer/glsl/chunks/rayTraceCore.glsl similarity index 91% rename from src/renderer/glsl/chunks/core.glsl rename to src/renderer/glsl/chunks/rayTraceCore.glsl index 251b674..818879f 100644 --- a/src/renderer/glsl/chunks/core.glsl +++ b/src/renderer/glsl/chunks/rayTraceCore.glsl @@ -1,12 +1,4 @@ export default ` - #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 RAY_MAX_DISTANCE 9999.0 - #define STANDARD 0 #define THIN_GLASS 1 #define THICK_GLASS 2 @@ -25,6 +17,8 @@ export default ` // 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; @@ -41,7 +35,6 @@ export default ` float roughness; float metalness; int materialType; - int meshId; }; struct Camera { diff --git a/src/renderer/glsl/chunks/sampleMaterial.glsl b/src/renderer/glsl/chunks/sampleMaterial.glsl index 89ec28e..f40b924 100644 --- a/src/renderer/glsl/chunks/sampleMaterial.glsl +++ b/src/renderer/glsl/chunks/sampleMaterial.glsl @@ -97,6 +97,10 @@ void sampleMaterial(SurfaceInteraction si, int bounce, inout Path path) { // Get new path direction + if (lastBounce) { + return; + } + lightDir = diffuseOrSpecular.y < mix(0.5, 0.0, si.metalness) ? lightDirDiffuse(si.faceNormal, viewDir, basis, randomSampleVec2()) : lightDirSpecular(si.faceNormal, viewDir, basis, si.roughness, randomSampleVec2()); diff --git a/src/renderer/glsl/chunks/surfaceInteractionDirect.glsl b/src/renderer/glsl/chunks/surfaceInteractionDirect.glsl new file mode 100644 index 0000000..594f3ee --- /dev/null +++ b/src/renderer/glsl/chunks/surfaceInteractionDirect.glsl @@ -0,0 +1,27 @@ +export default ` + + uniform sampler2D gPosition; + uniform sampler2D gNormal; + uniform sampler2D gFaceNormal; + uniform sampler2D gColor; + uniform sampler2D gMatProps; + + void surfaceInteractionDirect(vec2 coord, inout SurfaceInteraction si) { + si.position = texture(gPosition, coord).xyz; + + 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 = dot(si.normal, si.normal) > 0.0 ? true : false; + } +`; diff --git a/src/renderer/glsl/gBuffer.frag b/src/renderer/glsl/gBuffer.frag new file mode 100644 index 0000000..cd79ce9 --- /dev/null +++ b/src/renderer/glsl/gBuffer.frag @@ -0,0 +1,57 @@ +import constants from './chunks/constants.glsl'; +import materialBuffer from './chunks/materialBuffer.glsl'; + +export default { + +outputs: ['position', 'normal', 'faceNormal', 'color', 'matProps'], +includes: [ + constants, + 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 = vNormal; + vec3 faceNormal = 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); + } +` + +} diff --git a/src/renderer/glsl/gBuffer.vert b/src/renderer/glsl/gBuffer.vert new file mode 100644 index 0000000..a8d46d8 --- /dev/null +++ b/src/renderer/glsl/gBuffer.vert @@ -0,0 +1,24 @@ +export default { + +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); + } +` +} diff --git a/src/renderer/glsl/rayTrace.frag b/src/renderer/glsl/rayTrace.frag index 154e6be..8eedb15 100644 --- a/src/renderer/glsl/rayTrace.frag +++ b/src/renderer/glsl/rayTrace.frag @@ -1,7 +1,10 @@ import { unrollLoop } from '../glslUtil'; -import core from './chunks/core.glsl'; +import constants from './chunks/constants.glsl'; +import rayTraceCore from './chunks/rayTraceCore.glsl'; import textureLinear from './chunks/textureLinear.glsl'; +import materialBuffer from './chunks/materialBuffer.glsl'; import intersect from './chunks/intersect.glsl'; +import surfaceInteractionDirect from './chunks/surfaceInteractionDirect.glsl'; import random from './chunks/random.glsl'; import envmap from './chunks/envmap.glsl'; import bsdf from './chunks/bsdf.glsl'; @@ -9,13 +12,15 @@ import sample from './chunks/sample.glsl'; import sampleMaterial from './chunks/sampleMaterial.glsl'; import sampleShadowCatcher from './chunks/sampleShadowCatcher.glsl'; import sampleGlass from './chunks/sampleGlassSpecular.glsl'; -// import sampleGlass from './chunks/sampleGlassMicrofacet.glsl'; export default { includes: [ - core, + constants, + rayTraceCore, textureLinear, + materialBuffer, intersect, + surfaceInteractionDirect, random, envmap, bsdf, @@ -24,15 +29,9 @@ includes: [ sampleGlass, sampleShadowCatcher, ], -outputs: ['light', 'position'], +outputs: ['light'], source: (defines) => ` void bounce(inout Path path, int i, inout SurfaceInteraction si) { - if (path.abort) { - return; - } - - si = intersectScene(path.ray); - if (!si.hit) { if (path.specularBounce) { path.li += path.beta * sampleBackgroundFromDirection(path.ray.d); @@ -67,7 +66,7 @@ source: (defines) => ` // 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, inout SurfaceInteraction si) { + vec4 integrator(inout Ray ray) { Path path; path.ray = ray; path.li = vec3(0); @@ -76,16 +75,25 @@ source: (defines) => ` path.specularBounce = true; path.abort = false; - bounce(path, 1, si); + SurfaceInteraction si; + + // first surface interaction from g-buffer + surfaceInteractionDirect(vCoord, si); - SurfaceInteraction indirectSi; + // first surface interaction from ray interesction + // intersectScene(path.ray, si); + + bounce(path, 1, si); // Manually unroll for loop. - // Some hardware fails to interate over a GLSL loop, so we provide this workaround + // 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, ` - bounce(path, i, indirectSi); + if (!path.abort) { + intersectScene(path.ray, si); + bounce(path, i, si); + } `)} return vec4(path.li, path.alpha); @@ -115,20 +123,13 @@ source: (defines) => ` Ray cam; initRay(cam, origin, direction); - SurfaceInteraction si; - - vec4 liAndAlpha = integrator(cam, si); - - if (dot(si.position, si.position) == 0.0) { - si.position = origin + direction * RAY_MAX_DISTANCE; - } + vec4 liAndAlpha = integrator(cam); if (!(liAndAlpha.x < INF && liAndAlpha.x > -EPS)) { liAndAlpha = vec4(0, 0, 0, 1); } out_light = liAndAlpha; - out_position = vec4(si.position, si.meshId); // Stratified Sampling Sample Count Test // --------------- diff --git a/src/renderer/glsl/reproject.frag b/src/renderer/glsl/reproject.frag index c4d8a59..1ef8d1f 100644 --- a/src/renderer/glsl/reproject.frag +++ b/src/renderer/glsl/reproject.frag @@ -1,15 +1,18 @@ +import textureLinear from './chunks/textureLinear.glsl'; + export default { outputs: ['light'], +includes: [textureLinear], source: ` in vec2 vCoord; uniform mediump sampler2D light; uniform mediump sampler2D position; - uniform vec2 textureScale; + uniform vec2 lightScale; + uniform vec2 previousLightScale; uniform mediump sampler2D previousLight; uniform mediump sampler2D previousPosition; - uniform vec2 previousTextureScale; uniform mat4 historyCamera; uniform float blendAmount; @@ -20,18 +23,25 @@ source: ` return 0.5 * historyCoord.xy / historyCoord.w + 0.5; } + float getMeshId(sampler2D meshIdTex, vec2 vCoord) { + return floor(texture(meshIdTex, vCoord).w); + } + void main() { - vec2 scaledCoord = textureScale * vCoord; + vec3 currentPosition = textureLinear(position, vCoord).xyz; + float currentMeshId = getMeshId(position, vCoord); - vec4 positionTex = texture(position, scaledCoord); - vec4 lightTex = texture(light, scaledCoord); + vec4 currentLight = texture(light, lightScale * vCoord); - vec3 currentPosition = positionTex.xyz; - float currentMeshId = positionTex.w; + if (currentMeshId == 0.0) { + out_light = currentLight; + return; + } vec2 hCoord = reproject(currentPosition) - jitter; - vec2 hSizef = previousTextureScale * vec2(textureSize(previousPosition, 0)); + vec2 hSizef = previousLightScale * vec2(textureSize(previousLight, 0)); + vec2 hSizeInv = 1.0 / hSizef; ivec2 hSize = ivec2(hSizef); vec2 hTexelf = hCoord * hSizef - 0.5; @@ -57,10 +67,11 @@ source: ` // bilinear sampling, rejecting samples that don't have a matching mesh id for (int i = 0; i < 4; i++) { - float histMeshId = texelFetch(previousPosition, texel[i], 0).w; + 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 isValid = 0.0; float weight = isValid * weights[i]; history += weight * texelFetch(previousLight, texel[i], 0); @@ -76,8 +87,9 @@ source: ` 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 = texelFetch(previousPosition, texel, 0).w; + float histMeshId = getMeshId(previousPosition, gCoord); float isValid = histMeshId != currentMeshId || any(greaterThanEqual(texel, hSize)) ? 0.0 : 1.0; @@ -95,8 +107,7 @@ source: ` history.w = MAX_SAMPLES; } - out_light = blendAmount * history + lightTex; - + out_light = blendAmount * history + currentLight; } ` } diff --git a/src/renderer/glsl/toneMap.frag b/src/renderer/glsl/toneMap.frag index 70aa8b0..4d60edb 100644 --- a/src/renderer/glsl/toneMap.frag +++ b/src/renderer/glsl/toneMap.frag @@ -6,9 +6,10 @@ outputs: ['color'], source: ` in vec2 vCoord; - uniform mediump sampler2D light; + uniform sampler2D light; + uniform sampler2D position; - uniform vec2 textureScale; + uniform vec2 lightScale; // Tonemapping functions from THREE.js @@ -37,15 +38,62 @@ source: ` 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 + vec4 getUpscaledLight(vec2 coord) { + float meshId = texture(position, coord).w; + + 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 = texture(position, pCoord).w == 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() { - vec4 tex = texture(light, textureScale * vCoord); + #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 = tex.rgb / tex.a; + vec3 light = upscaledLight.rgb / upscaledLight.a; light *= EXPOSURE; diff --git a/src/renderer/mergeMeshesToGeometry.js b/src/renderer/mergeMeshesToGeometry.js index b1d7154..40d79a3 100644 --- a/src/renderer/mergeMeshesToGeometry.js +++ b/src/renderer/mergeMeshesToGeometry.js @@ -40,51 +40,51 @@ export function mergeMeshesToGeometry(meshes) { }); } - const { geometry, materialIndices } = mergeGeometry(geometryAndMaterialIndex, vertexCount, indexCount); + const geometry = mergeGeometry(geometryAndMaterialIndex, vertexCount, indexCount); return { geometry, - materialIndices, materials: Array.from(materialIndexMap.keys()) }; } function mergeGeometry(geometryAndMaterialIndex, vertexCount, indexCount) { - const position = new BufferAttribute(new Float32Array(3 * vertexCount), 3, false); - const normal = new BufferAttribute(new Float32Array(3 * vertexCount), 3, false); - const uv = new BufferAttribute(new Float32Array(2 * vertexCount), 2, false); - const index = new BufferAttribute(new Uint32Array(indexCount), 1, false); - - const materialIndices = []; - - const bg = new BufferGeometry(); - bg.addAttribute('position', position); - bg.addAttribute('normal', normal); - bg.addAttribute('uv', uv); - bg.setIndex(index); + const positionAttrib = new BufferAttribute(new Float32Array(3 * vertexCount), 3, false); + const normalAttrib = new BufferAttribute(new Float32Array(3 * vertexCount), 3, false); + const uvAttrib = new BufferAttribute(new Float32Array(2 * vertexCount), 2, false); + const materialMeshIndexAttrib = new BufferAttribute(new Int32Array(2 * vertexCount), 2, false); + const indexAttrib = new BufferAttribute(new Uint32Array(indexCount), 1, false); + + const mergedGeometry = new 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; - bg.merge(geometry, currentVertex); + mergedGeometry.merge(geometry, currentVertex); const meshIndex = geometry.getIndex(); for (let i = 0; i < meshIndex.count; i++) { - index.setX(currentIndex + i, currentVertex + meshIndex.getX(i)); + indexAttrib.setX(currentIndex + i, currentVertex + meshIndex.getX(i)); } - const triangleCount = meshIndex.count / 3; - for (let i = 0; i < triangleCount; i++) { - materialIndices.push(materialIndex); + for (let i = 0; i < vertexCount; i++) { + materialMeshIndexAttrib.setXY(currentVertex + i, materialIndex, currentMesh); } currentVertex += vertexCount; currentIndex += meshIndex.count; + currentMesh++; } - return { geometry: bg, materialIndices }; + return mergedGeometry; } // Similar to buffergeometry.clone(), except we only copy diff --git a/src/renderer/uploadBuffers.js b/src/renderer/uploadBuffers.js deleted file mode 100644 index 27b9f3e..0000000 --- a/src/renderer/uploadBuffers.js +++ /dev/null @@ -1,68 +0,0 @@ -import { makeUniformBuffer } from './glUtil'; - -// Upload arrays to uniform buffer objects -// Packs different arrays into vec4's to take advantage of GLSL's std140 memory layout - -export function uploadBuffers(gl, program, bufferData) { - const materialBuffer = makeUniformBuffer(gl, program, 'Materials'); - - const { - color = [], - roughness = [], - metalness = [], - normalScale = [], - type = [], - diffuseMapIndex = [], - diffuseMapSize = [], - normalMapIndex = [], - normalMapSize = [], - roughnessMapIndex = [], - metalnessMapIndex = [], - pbrMapSize = [], - } = bufferData; - - materialBuffer.set('Materials.colorAndMaterialType[0]', interleave( - { data: [].concat(...color.map(d => d.toArray())), channels: 3 }, - { data: type, channels: 1} - )); - - materialBuffer.set('Materials.roughnessMetalnessNormalScale[0]', interleave( - { data: roughness, channels: 1 }, - { data: metalness, channels: 1 }, - { data: [].concat(...normalScale.map(d => d.toArray())), channels: 2 } - )); - - materialBuffer.set('Materials.diffuseNormalRoughnessMetalnessMapIndex[0]', interleave( - { data: diffuseMapIndex, channels: 1 }, - { data: normalMapIndex, channels: 1 }, - { data: roughnessMapIndex, channels: 1 }, - { data: metalnessMapIndex, channels: 1 } - )); - - materialBuffer.set('Materials.diffuseNormalMapSize[0]', interleave( - { data: diffuseMapSize, channels: 2 }, - { data: normalMapSize, channels: 2 } - )); - - materialBuffer.set('Materials.pbrMapSize[0]', pbrMapSize); - - materialBuffer.bind(0); -} - -function interleave(...arrays) { - const maxLength = arrays.reduce((m, a) => { - return Math.max(m, a.data.length / a.channels); - }, 0); - - 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; -}