diff --git a/README.md b/README.md index a903608..876ccb2 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,56 @@ WebGL Clustered Deferred and Forward+ Shading **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5** -* (TODO) YOUR NAME HERE -* Tested on: (TODO) **Google Chrome 222.2** on - Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab) - +* William Ho +* Tested on: **Google Chrome** on + OS X El Capitan, MacBook Pro 2013, 2.4, GHz Intel Core i5, Intel Iris 1536 MB ### Live Online -[![](img/thumb.png)](http://TODO.github.io/Project5B-WebGL-Deferred-Shading) +[![](img/thumb.jpg)](https://williamkho.github.io/Project5-WebGL-Clustered-Deferred-Forward-Plus/) + +### Overview + +This project consists of implementations of a **Clustered Forward+** renderer and a **Clustered Deferred** renderer, compared against a general case **Forward** renderer. + +A forward renderer performs brute force lighting by shading fragments against every possible light in the scene. The unnecessary work is in the negligible effects a given light has on the majority of fragments. A solution to this is to divide our rendered area up into "clusters", where the fragments in a given cluster need only be shaded for the lights that affect that specific cluster. This involves a CPU side preprocessing of light information to map clusters to their relevant lights. This is what **Clustered Forward+** performs. + +To further decrease the amount of unnecessary work done, a further solution is to defer lighting to a second stage after fragment attributes have been computed. On a first pass, the scene is processed and attributes are passed to a g-buffer, which is then passed to a second pass render (in which we can again use our clustering technique). We are able to leverage the fact that the first render pass discards all unnecessary fragments (such as those occluded). This is what **Clustered Deferred** performs. + +### Comparisons of Implementation + +| Forward Renderer | Clustered Forward+ | Clustered Deferred (w/ Blinn-Phong)| +|:----:|:----:|:----:| +| ![](img/forward.gif) | ![](img/clusteredforwardplus.gif) | ![](img/clustereddeferred.gif) | + +### Analysis + +![](img/chart1.png) + +We can see here that as we increase the number of lights in the scene, our Clustered implementations are able to render at a higher frame rate than the basic Forward implementation, and our Deferred shader grants us extra wins on time. Our Clustered Forward+ manages to eliminate the unnecessary lighting work by culling redundant lights from fragments. The Deferred renderer culls unnecessary fragments from being shaded. + +It should be noted that utilizing the clustering technique has other costs. Clustering the scene must be done before the scene is processed by the vertex and fragment shaders, and this CPU-side preprocessing step can have potential downsides. If for, instance, clustering did not effectively cull lights, such as in a case where relevant lights span all clusters, clustering could prove detrimental. There is also the added overhead of generating clustering data. + +Deferred shading similarly has an extra step that effectively culls unnecessary fragments, but in cases where such fragments are not as common, such as scenes with little occlusion, it could be ineffective. Additionally, deferred shading requires the ability to specify multiple render targets, which is not supported in all cases. The other drawback to deferred shading is the increased bandwidth requirements of passing the g-buffer from one render pass to the next. + +## Potential Improvements + +There are several improvements to these implementations that are worth exploring: + +* Improved cluster testing: my implementation currently calculates a conservative bounding frustrum per light to calculate its relevant clusters. It is possible that clusters could be more effectively culled from the light. + +* Optimized g-buffer: better packing of data in the g-buffer reduces bandwidth requirements. -### Demo Video/GIF -[![](img/video.png)](TODO) +### Debug Views -### (TODO: Your README) +![](img/clusterDebug01.png) +Debugging view of clustering in screen space. -*DO NOT* leave the README to the last minute! It is a crucial part of the -project, and we will not be able to grade you without a good README. +![](img/clusterDebug02.png) +Debugging view to illustrate cluster plane calculation. -This assignment has a considerable amount of performance analysis compared -to implementation work. Complete the implementation early to leave time! +![](img/clusterDebug03.png) +Debugging view of frozen clustering frustrum to visualize fragment cluster placement. ### Credits @@ -31,3 +62,4 @@ to implementation work. Complete the implementation early to leave time! * [webgl-debug](https://github.com/KhronosGroup/WebGLDeveloperTools) by Khronos Group Inc. * [glMatrix](https://github.com/toji/gl-matrix) by [@toji](https://github.com/toji) and contributors * [minimal-gltf-loader](https://github.com/shrekshao/minimal-gltf-loader) by [@shrekshao](https://github.com/shrekshao) +* [Practical Clustered Shading](http://www.humus.name/Articles/PracticalClusteredShading.pdf) by Emil Persson, Head of Research, Avalanche Studios diff --git a/img/chart1.png b/img/chart1.png new file mode 100644 index 0000000..ab06001 Binary files /dev/null and b/img/chart1.png differ diff --git a/img/clusterDebug01.png b/img/clusterDebug01.png new file mode 100644 index 0000000..29e873d Binary files /dev/null and b/img/clusterDebug01.png differ diff --git a/img/clusterDebug02.png b/img/clusterDebug02.png new file mode 100644 index 0000000..401fa16 Binary files /dev/null and b/img/clusterDebug02.png differ diff --git a/img/clusterDebug03.png b/img/clusterDebug03.png new file mode 100644 index 0000000..18b74c7 Binary files /dev/null and b/img/clusterDebug03.png differ diff --git a/img/clustereddeferred.gif b/img/clustereddeferred.gif new file mode 100644 index 0000000..1be3b27 Binary files /dev/null and b/img/clustereddeferred.gif differ diff --git a/img/clusteredforwardplus.gif b/img/clusteredforwardplus.gif new file mode 100644 index 0000000..f14fc03 Binary files /dev/null and b/img/clusteredforwardplus.gif differ diff --git a/img/forward.gif b/img/forward.gif new file mode 100644 index 0000000..9f2f4ee Binary files /dev/null and b/img/forward.gif differ diff --git a/img/thumb.jpg b/img/thumb.jpg new file mode 100644 index 0000000..6b8777a Binary files /dev/null and b/img/thumb.jpg differ diff --git a/src/init.js b/src/init.js index 1b09377..9de2fc2 100644 --- a/src/init.js +++ b/src/init.js @@ -1,5 +1,5 @@ // TODO: Change this to enable / disable debug mode -export const DEBUG = true && process.env.NODE_ENV === 'development'; +export const DEBUG = false && process.env.NODE_ENV === 'development'; import DAT from 'dat-gui'; import WebGLDebug from 'webgl-debug'; diff --git a/src/main.js b/src/main.js index 1cbbf9a..f8b2e8b 100644 --- a/src/main.js +++ b/src/main.js @@ -9,7 +9,7 @@ const CLUSTERED_FORWARD_PLUS = 'Clustered Forward+'; const CLUSTERED_DEFFERED = 'Clustered Deferred'; const params = { - renderer: CLUSTERED_FORWARD_PLUS, + renderer: CLUSTERED_DEFFERED, _renderer: null, }; diff --git a/src/renderers/clustered.js b/src/renderers/clustered.js index 9521fbd..e05fc9d 100644 --- a/src/renderers/clustered.js +++ b/src/renderers/clustered.js @@ -2,7 +2,7 @@ import { mat4, vec4, vec3 } from 'gl-matrix'; import { NUM_LIGHTS } from '../scene'; import TextureBuffer from './textureBuffer'; -export const MAX_LIGHTS_PER_CLUSTER = 100; +export const MAX_LIGHTS_PER_CLUSTER = 250; export default class ClusteredRenderer { constructor(xSlices, ySlices, zSlices) { @@ -13,10 +13,113 @@ export default class ClusteredRenderer { this._zSlices = zSlices; } + //Helper function to convert degrees to radians + getTanDeg(deg) { + var rad = deg * Math.PI/180; + return Math.tan(rad); + + } + + getContainingZPlane(posZ) { + if (posZ > -5) { + return 0; + } else { + let logPosZ = Math.log2(Math.abs(posZ) - 5.0); + if (logPosZ < 0.0) return 1; + return Math.floor(logPosZ) + 1.0; + } + } + /* + Options parameter includes: + stride : distance between clusters + strideMultiple : how many strides to take for the given plane + axis : which axis of cluster we are using + output : output vec3 + */ + calculatePlaneNormal(options) { + if (options.axis === 'horizontal') { + let d = options.stride * options.strideMultiple; + let x = 1 / Math.sqrt(1 + d * d); + let z = d / Math.sqrt(1 + d * d); + options.output.x = x; + options.output.y = 0; + options.output.z = z; + } else { + let d = options.stride * options.strideMultiple; + let y = 1 / Math.sqrt(1 + d * d); + let z = d / Math.sqrt(1 + d * d); + options.output.x = 0; + options.output.y = y; + options.output.z = z; + } + } + + //Take in light position in view space FOR + intersectsPlane(planeNormal, planeOrigin, position, radius) { + let relativePosition = vec3.create(); + vec3.subtract(relativePosition, position, planeOrigin); + return radius > Math.abs(vec3.dot(planeNormal, relativePosition)); + } + + clamp(a, b, c) { + return Math.max(b, Math.min(c, a)); + } + + //Calculate the normalized frustrum space coordinates given a position + getClusterUVD(options) { + let absZ = Math.abs(options.position[2]); + let radius = options.radius; + + let height = options.fovTan * absZ * 2; + let pHeight = (options.position[1] + height / 2); + + let pv1 = (pHeight - radius) / height; + let pv2 = (pHeight + radius) / height; + + let width = height * options.aspect; + let pWidth = (options.position[0] + width / 2); + + let pu1 = (pWidth - radius) / width; + let pu2 = (pWidth + radius) / width; + + let pd1 = ((absZ - radius) - options.near) / (options.far - options.near); + let pd2 = ((absZ + radius) - options.near) / (options.far - options.near); + + return { + x1 : pu1, + y1 : pv1, + z1 : pd1, + x2 : pu2, + y2 : pv2, + z2 : pd2 + }; + + //vec3.set(options.output, pu, pv, pd); + } + updateClusters(camera, viewMatrix, scene) { // TODO: Update the cluster texture with the count and indices of the lights in each cluster // This will take some time. The math is nontrivial... + var fovTan = this.getTanDeg(camera.fov / 2); + var minClusterUVDScratch = vec3.create(); + var maxClusterUVDScratch = vec3.create(); + + + //scratch variables for normals + /* + var origin = vec3.create(); + var z1OriginScratch = vec3.create(); + var z2OriginScratch = vec3.create(); + var x1NormalScratch = vec3.create(); + var x2NormalScratch = vec3.create(); + var y1NormalScratch = vec3.create(); + var y2NormalScratch = vec3.create(); + var zNormal = vec3.create(); + zNormal.z = 1; + */ + var lightPositionScratch = vec3.create(); + for (let z = 0; z < this._zSlices; ++z) { for (let y = 0; y < this._ySlices; ++y) { for (let x = 0; x < this._xSlices; ++x) { @@ -27,6 +130,80 @@ export default class ClusteredRenderer { } } + for (let l = 0; l < scene.lights.length; ++l) { + let light = scene.lights[l]; + let radius = light.radius * 1.4; + let outOfView = false; + vec3.copy(lightPositionScratch, light.position); + vec3.transformMat4(lightPositionScratch, lightPositionScratch, viewMatrix); + + let boundingBox = this.getClusterUVD({ + radius : radius, + position : lightPositionScratch, + fovTan : fovTan, + aspect : camera.aspect, + near : camera.near, + far : camera.far + }); + + if (boundingBox.x1 > 0.999 && boundingBox.x2 > 0.999) { + outOfView = true; + } else if (boundingBox.y1 > 0.999 && boundingBox.y2 > 0.999) { + outOfView = true; + } else if (boundingBox.z1 > 0.999 && boundingBox.z2 > 0.999) { + outOfView = true; + } else if (boundingBox.x1 < 0 && boundingBox.x2 < 0) { + outOfView = true; + } else if (boundingBox.y1 < 0 && boundingBox.y2 < 0) { + outOfView = true; + } else if (boundingBox.z1 < 0 && boundingBox.z2 < 0) { + outOfView = true; + } + + if (!outOfView) { + let pu1 = this.clamp(boundingBox.x1, 0, 0.999); + let pu2 = this.clamp(boundingBox.x2, 0, 0.999); + + let minX = Math.floor(pu1 * this._xSlices); + let maxX = Math.floor(pu2 * this._xSlices); + + let pv1 = this.clamp(boundingBox.y1, 0, 0.999); + let pv2 = this.clamp(boundingBox.y2, 0, 0.999); + + let minY = Math.floor(pv1 * this._ySlices); + let maxY = Math.floor(pv2 * this._ySlices); + + //Exponential zplanes + let pd1 = this.clamp(boundingBox.z1, 0, 0.999); + let pd2 = this.clamp(boundingBox.z2, 0, 0.999); + + pd1 = pd1 * pd1 * (3.0 - 2.0 * pd1); + Math.pow(pd1, 0.25); + + pd2 = pd2 * pd2 * (3.0 - 2.0 * pd2); + Math.pow(pd2, 0.25); + + let minZ = Math.floor(pd1 * this._zSlices); + let maxZ = Math.floor(pd2 * this._zSlices); + + + // loop through bounding frustrum + for (let cz = minZ; cz < maxZ + 1; ++cz) { + for (let cy = minY; cy < maxY + 1; ++cy) { + for (let cx = minX; cx < maxX + 1; ++cx) { + let clusterId = cx + cy * this._xSlices + cz * this._xSlices * this._ySlices; + let numLights = this._clusterTexture.buffer[this._clusterTexture.bufferIndex(clusterId, 0)]; + ++numLights; + let lidInBuffer = this._clusterTexture.bufferIndex(clusterId, Math.floor(numLights / 4)); + lidInBuffer += numLights % 4; + this._clusterTexture.buffer[lidInBuffer] = l; + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(clusterId, 0)] = numLights; + } + } + } + } + } + this._clusterTexture.update(); } } \ No newline at end of file diff --git a/src/renderers/clusteredDeferred.js b/src/renderers/clusteredDeferred.js index 5e28e84..7583eb6 100644 --- a/src/renderers/clusteredDeferred.js +++ b/src/renderers/clusteredDeferred.js @@ -9,7 +9,7 @@ import fsSource from '../shaders/deferred.frag.glsl.js'; import TextureBuffer from './textureBuffer'; import ClusteredRenderer from './clustered'; -export const NUM_GBUFFERS = 4; +export const NUM_GBUFFERS = 3; export default class ClusteredDeferredRenderer extends ClusteredRenderer { constructor(xSlices, ySlices, zSlices) { @@ -28,8 +28,12 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { this._progShade = loadShaderProgram(QuadVertSource, fsSource({ numLights: NUM_LIGHTS, numGBuffers: NUM_GBUFFERS, + xSliceNum : xSlices, + ySliceNum : ySlices, + zSliceNum : zSlices }), { - uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]'], + uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]', 'u_lightbuffer', 'u_clusterbuffer', + 'u_frustrumRatios', 'u_nearFar', 'u_viewMatrix'], attribs: ['a_uv'], }); @@ -155,6 +159,18 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { // TODO: Bind any other shader inputs + //upload the width and height ratios + let heightRatio = Math.tan((camera.fov / 2) * Math.PI/180) * 2; + let widthRatio = camera.aspect * heightRatio; + gl.uniform2f(this._progShade.u_frustrumRatios, widthRatio, heightRatio); + + //upload near and far clipping planes + gl.uniform2f(this._progShade.u_nearFar, camera.near, camera.far); + + //bind view matrix + gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix); + + // Bind g-buffers const firstGBufferBinding = 0; // You may have to change this if you use other texture slots for (let i = 0; i < NUM_GBUFFERS; i++) { @@ -163,6 +179,14 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { gl.uniform1i(this._progShade[`u_gbuffers[${i}]`], i + firstGBufferBinding); } + gl.activeTexture(gl[`TEXTURE${NUM_GBUFFERS}`]); + gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture); + gl.uniform1i(this._progShade.u_lightbuffer, NUM_GBUFFERS); + + gl.activeTexture(gl[`TEXTURE${NUM_GBUFFERS + 1}`]); + gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture); + gl.uniform1i(this._progShade.u_clusterbuffer, NUM_GBUFFERS + 1); + renderFullscreenQuad(this._progShade); } }; diff --git a/src/renderers/clusteredForwardPlus.js b/src/renderers/clusteredForwardPlus.js index 9e8afbe..dfafe22 100644 --- a/src/renderers/clusteredForwardPlus.js +++ b/src/renderers/clusteredForwardPlus.js @@ -16,14 +16,24 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { this._shaderProgram = loadShaderProgram(vsSource, fsSource({ numLights: NUM_LIGHTS, + xSliceNum : xSlices, + ySliceNum : ySlices, + zSliceNum : zSlices }), { - uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer'], + uniforms: ['u_viewProjectionMatrix', 'u_viewMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer', + 'u_frustrumRatios', 'u_nearFar'], attribs: ['a_position', 'a_normal', 'a_uv'], }); this._projectionMatrix = mat4.create(); this._viewMatrix = mat4.create(); this._viewProjectionMatrix = mat4.create(); + + //Debugging view : frozen frustrum + this._debug = false; + this._constViewMatrix = mat4.create(); + this._capturedViewMatrix = false; + } render(camera, scene) { @@ -33,8 +43,18 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { mat4.copy(this._projectionMatrix, camera.projectionMatrix.elements); mat4.multiply(this._viewProjectionMatrix, this._projectionMatrix, this._viewMatrix); + //Debugging + if (this._debug && !(this._capturedViewMatrix)) { + mat4.copy(this._constViewMatrix, this._viewMatrix); + this._capturedViewMatrix = true; + } + // Update cluster texture which maps from cluster index to light list - this.updateClusters(camera, this._viewMatrix, scene); + if (this._debug) { + this.updateClusters(camera, this._constViewMatrix, scene); + } else { + this.updateClusters(camera, this._viewMatrix, scene); + } // Update the buffer used to populate the texture packed with light data for (let i = 0; i < NUM_LIGHTS; ++i) { @@ -64,6 +84,19 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { // Upload the camera matrix gl.uniformMatrix4fv(this._shaderProgram.u_viewProjectionMatrix, false, this._viewProjectionMatrix); + if (this._debug) { + gl.uniformMatrix4fv(this._shaderProgram.u_viewMatrix, false, this._constViewMatrix); + } else { + gl.uniformMatrix4fv(this._shaderProgram.u_viewMatrix, false, this._viewMatrix); + } + + //upload the width and height ratios + let heightRatio = Math.tan((camera.fov / 2) * Math.PI/180) * 2; + let widthRatio = camera.aspect * heightRatio; + gl.uniform2f(this._shaderProgram.u_frustrumRatios, widthRatio, heightRatio); + + //upload near and far clipping planes + gl.uniform2f(this._shaderProgram.u_nearFar, camera.near, camera.far); // Set the light texture as a uniform input to the shader gl.activeTexture(gl.TEXTURE2); diff --git a/src/scene.js b/src/scene.js index 35f6700..5d5e71e 100644 --- a/src/scene.js +++ b/src/scene.js @@ -8,7 +8,7 @@ export const LIGHT_RADIUS = 5.0; export const LIGHT_DT = -0.03; // TODO: This controls the number of lights -export const NUM_LIGHTS = 100; +export const NUM_LIGHTS = 250; class Scene { constructor() { diff --git a/src/shaders/clusteredForward.frag.glsl.js b/src/shaders/clusteredForward.frag.glsl.js index 022fda7..f83a410 100644 --- a/src/shaders/clusteredForward.frag.glsl.js +++ b/src/shaders/clusteredForward.frag.glsl.js @@ -9,6 +9,10 @@ export default function(params) { uniform sampler2D u_normap; uniform sampler2D u_lightbuffer; + uniform vec2 u_frustrumRatios; + uniform mat4 u_viewMatrix; + uniform vec2 u_nearFar; + // TODO: Read this buffer to determine the lights influencing a cluster uniform sampler2D u_clusterbuffer; @@ -74,28 +78,105 @@ export default function(params) { } } + // + vec3 getClusterUVD(vec3 pos, vec2 frustrumRatios, vec2 nearFar) { + float height = abs(pos.z) * frustrumRatios.y; + float pHeight = (pos.y + height / 2.0); + float pv = pHeight / height; + + float width = abs(pos.z) * frustrumRatios.x; + float pWidth = (pos.x + width / 2.0); + float pu = pWidth / width; + + float pd = (abs(pos.z) - nearFar.x) / (nearFar.y - nearFar.x); + + pu = max(0.0, min(pu, 0.999)); + pv = max(0.0, min(pv, 0.999)); + pd = max(0.0, min(pd, 0.999)); + + return vec3(pu, pv, pd); + } + + int getContainingZPlane(float posZ) { + bool firstPlane = posZ > -5.0; + if (firstPlane) { + return 0; + } else { + float logPosZ = log2(abs(posZ) - 5.0); + if (logPosZ < 0.0) return 1; + return int(floor(logPosZ) + 1.0); + } + } + + int getClusterID(vec3 position) { + vec3 clusterUVD = getClusterUVD(position, u_frustrumRatios, u_nearFar); + vec3 sliceDimensions = vec3(float(${params.xSliceNum}), float(${params.ySliceNum}), float(${params.zSliceNum})); + + int clusterXID = int(floor(clusterUVD.x * sliceDimensions.x)); + + int clusterYID = int(floor(clusterUVD.y * sliceDimensions.y)); + + float pd = clusterUVD.z; + pd = pd * pd * (3.0 - 2.0 * pd); + pow(pd, 0.25); + + int clusterZID = int(floor(pd * sliceDimensions.z)); + + return clusterXID + clusterYID * int(sliceDimensions.x) + clusterZID * int(sliceDimensions.y * sliceDimensions.z); + } + + vec3 getClusterColor(vec3 position) { + vec3 clusterUVD = getClusterUVD(position, u_frustrumRatios, u_nearFar); + vec3 sliceDimensions = vec3(float(${params.xSliceNum}), float(${params.ySliceNum}), float(${params.zSliceNum})); + + float clusterXID = floor(clusterUVD.x * sliceDimensions.x); + + float clusterYID = floor(clusterUVD.y * sliceDimensions.y); + + float pd = clusterUVD.z; + pd = pd * pd * (3.0 - 2.0 * pd); + pow(pd, 0.25); + float clusterZID = floor(pd * sliceDimensions.z); + + if (clusterUVD.x == 0.999) clusterXID = 0.0; + if (clusterUVD.y == 0.999) clusterYID = 0.0; + + return vec3(0.0 / sliceDimensions.x, 0.0 / sliceDimensions.y, (15.0 - clusterZID) / sliceDimensions.z); + } + void main() { vec3 albedo = texture2D(u_colmap, v_uv).rgb; vec3 normap = texture2D(u_normap, v_uv).xyz; vec3 normal = applyNormalMap(v_normal, normap); vec3 fragColor = vec3(0.0); + int numClusters = ${params.xSliceNum} * ${params.ySliceNum} * ${params.zSliceNum}; + int clusterBufferHeight = int(ceil((float(${params.numLights}) + 1.0) / 4.0)); + int clusterIndex = getClusterID(vec3(u_viewMatrix * vec4(v_position, 1.0))); - for (int i = 0; i < ${params.numLights}; ++i) { - Light light = UnpackLight(i); - float lightDistance = distance(light.position, v_position); - vec3 L = (light.position - v_position) / lightDistance; + float numAffectingLights = ExtractFloat(u_clusterbuffer, numClusters, clusterBufferHeight, clusterIndex, 0); - float lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius); - float lambertTerm = max(dot(L, normal), 0.0); + for (int i = 1; i < ${params.numLights}; ++i) { + if (i < int(numAffectingLights)) { + int lid = int(ExtractFloat(u_clusterbuffer, numClusters, clusterBufferHeight, clusterIndex, i)); + Light light = UnpackLight(lid); + float lightDistance = distance(light.position, v_position); + vec3 L = (light.position - v_position) / lightDistance; - fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity); + float lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius); + float lambertTerm = max(dot(L, normal), 0.0); + + fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity); + } } const vec3 ambientLight = vec3(0.025); - fragColor += albedo * ambientLight; + fragColor += albedo * ambientLight; + + // fragColor = 0.5 * fragColor; + // fragColor += 0.5 * getClusterColor(vec3(u_viewMatrix * vec4(v_position, 1.0))); gl_FragColor = vec4(fragColor, 1.0); } `; -} +} \ No newline at end of file diff --git a/src/shaders/deferred.frag.glsl.js b/src/shaders/deferred.frag.glsl.js index 50f1e75..5dcd3dd 100644 --- a/src/shaders/deferred.frag.glsl.js +++ b/src/shaders/deferred.frag.glsl.js @@ -4,17 +4,169 @@ export default function(params) { precision highp float; uniform sampler2D u_gbuffers[${params.numGBuffers}]; + + uniform sampler2D u_lightbuffer; + uniform sampler2D u_clusterbuffer; + + uniform vec2 u_frustrumRatios; + uniform mat4 u_viewMatrix; + uniform vec2 u_nearFar; + uniform vec3 u_camerapos; varying vec2 v_uv; + + //Extract float from a texture + float ExtractFloat(sampler2D texture, int textureWidth, int textureHeight, int index, int component) { + float u = float(index + 1) / float(textureWidth + 1); + int pixel = component / 4; + float v = float(pixel + 1) / float(textureHeight + 1); + vec4 texel = texture2D(texture, vec2(u, v)); + int pixelComponent = component - pixel * 4; + if (pixelComponent == 0) { + return texel[0]; + } else if (pixelComponent == 1) { + return texel[1]; + } else if (pixelComponent == 2) { + return texel[2]; + } else if (pixelComponent == 3) { + return texel[3]; + } + } + struct Light { + vec3 position; + float radius; + vec3 color; + }; + + Light UnpackLight(int index) { + Light light; + float u = float(index + 1) / float(${params.numLights + 1}); + vec4 v1 = texture2D(u_lightbuffer, vec2(u, 0.3)); + vec4 v2 = texture2D(u_lightbuffer, vec2(u, 0.6)); + light.position = v1.xyz; + + // LOOK: This extracts the 4th float (radius) of the (index)th light in the buffer + // Note that this is just an example implementation to extract one float. + // There are more efficient ways if you need adjacent values + light.radius = ExtractFloat(u_lightbuffer, ${params.numLights}, 2, index, 3); + + + light.color = v2.rgb; + return light; + } + + + float cubicGaussian(float h) { + if (h < 1.0) { + return 0.25 * pow(2.0 - h, 3.0) - pow(1.0 - h, 3.0); + } else if (h < 2.0) { + return 0.25 * pow(2.0 - h, 3.0); + } else { + return 0.0; + } + } + + //Get normalized coordinates in cluster space for fragment + vec3 getClusterUVD(vec3 pos, vec2 frustrumRatios, vec2 nearFar) { + float height = abs(pos.z) * frustrumRatios.y; + float pHeight = (pos.y + height / 2.0); + float pv = pHeight / height; + + float width = abs(pos.z) * frustrumRatios.x; + float pWidth = (pos.x + width / 2.0); + float pu = pWidth / width; + + float pd = (abs(pos.z) - nearFar.x) / (nearFar.y - nearFar.x); + + pu = max(0.0, min(pu, 0.999)); + pv = max(0.0, min(pv, 0.999)); + pd = max(0.0, min(pd, 0.999)); + + return vec3(pu, pv, pd); + } + + //calculate cluster id given position + int getClusterID(vec3 position) { + vec3 clusterUVD = getClusterUVD(position, u_frustrumRatios, u_nearFar); + vec3 sliceDimensions = vec3(float(${params.xSliceNum}), float(${params.ySliceNum}), float(${params.zSliceNum})); + + int clusterXID = int(floor(clusterUVD.x * sliceDimensions.x)); + + int clusterYID = int(floor(clusterUVD.y * sliceDimensions.y)); + + float pd = clusterUVD.z; + pd = pd * pd * (3.0 - 2.0 * pd); + pow(pd, 0.25); + + int clusterZID = int(floor(pd * sliceDimensions.z)); + + return clusterXID + clusterYID * int(sliceDimensions.x) + clusterZID * int(sliceDimensions.y * sliceDimensions.z); + } + + vec3 getClusterColor(vec3 position) { + vec3 clusterUVD = getClusterUVD(position, u_frustrumRatios, u_nearFar); + vec3 sliceDimensions = vec3(float(${params.xSliceNum}), float(${params.ySliceNum}), float(${params.zSliceNum})); + + float clusterXID = floor(clusterUVD.x * sliceDimensions.x); + + float clusterYID = floor(clusterUVD.y * sliceDimensions.y); + + float pd = clusterUVD.z; + pd = pd * pd * (3.0 - 2.0 * pd); + pow(pd, 0.25); + float clusterZID = floor(pd * sliceDimensions.z); + + if (clusterUVD.x == 0.999) clusterXID = 0.0; + if (clusterUVD.y == 0.999) clusterYID = 0.0; + + return vec3(clusterXID / sliceDimensions.x, clusterYID / sliceDimensions.y, (15.0 - clusterZID) / sliceDimensions.z); + } + void main() { // TODO: extract data from g buffers and do lighting - // vec4 gb0 = texture2D(u_gbuffers[0], v_uv); - // vec4 gb1 = texture2D(u_gbuffers[1], v_uv); - // vec4 gb2 = texture2D(u_gbuffers[2], v_uv); + vec4 albedo = texture2D(u_gbuffers[0], v_uv); + vec4 normal = texture2D(u_gbuffers[1], v_uv); + vec4 position = texture2D(u_gbuffers[2], v_uv); // vec4 gb3 = texture2D(u_gbuffers[3], v_uv); - gl_FragColor = vec4(v_uv, 0.0, 1.0); + vec3 fragColor = vec3(0.0); + + int numClusters = ${params.xSliceNum} * ${params.ySliceNum} * ${params.zSliceNum}; + int clusterBufferHeight = int(ceil((float(${params.numLights}) + 1.0) / 4.0)); + vec3 viewSpacePosition = vec3(u_viewMatrix * position); + int clusterIndex = getClusterID(viewSpacePosition); + + float numAffectingLights = ExtractFloat(u_clusterbuffer, numClusters, clusterBufferHeight, clusterIndex, 0); + + for (int i = 1; i < ${params.numLights}; ++i) { + if (i < int(numAffectingLights)) { + int lid = int(ExtractFloat(u_clusterbuffer, numClusters, clusterBufferHeight, clusterIndex, i)); + Light light = UnpackLight(lid); + float lightDistance = distance(light.position, vec3(position)); + vec3 L = (light.position - vec3(position)) / lightDistance; + + float lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius); + float lambertTerm = max(dot(L, vec3(normal)), 0.0); + + fragColor += vec3(albedo) * lambertTerm * light.color * vec3(lightIntensity); + + //Calculate specular + vec3 viewSpaceL = vec3(u_viewMatrix * vec4(L, 1.0)); + vec3 viewSpaceNormal = vec3(u_viewMatrix * normal); + float cameraDistance = length(viewSpacePosition); + vec3 cameraVector = (-viewSpacePosition) / cameraDistance; + vec3 halfVector = (viewSpaceL + cameraVector) / length(viewSpaceL + cameraVector); + float blinnPhongTerm = max(0.0, dot(viewSpaceNormal, halfVector)); + + fragColor += 2.0 * vec3(albedo) * blinnPhongTerm * light.color * vec3(lightIntensity); + + } + } + + fragColor += vec3(0.05); + + gl_FragColor = vec4(fragColor, 1.0); } `; } \ No newline at end of file diff --git a/src/shaders/deferredToTexture.frag.glsl b/src/shaders/deferredToTexture.frag.glsl index bafc086..3874362 100644 --- a/src/shaders/deferredToTexture.frag.glsl +++ b/src/shaders/deferredToTexture.frag.glsl @@ -22,8 +22,8 @@ void main() { vec3 col = vec3(texture2D(u_colmap, v_uv)); // TODO: populate your g buffer - // gl_FragData[0] = ?? - // gl_FragData[1] = ?? - // gl_FragData[2] = ?? + gl_FragData[0] = vec4(col, 1.0); + gl_FragData[1] = vec4(norm, 0.0); + gl_FragData[2] = vec4(v_position, 1.0); // gl_FragData[3] = ?? } \ No newline at end of file