diff --git a/README.md b/README.md index a903608..bd07040 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,61 @@ 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) +* Rishabh Shah +* Tested on: **Version 62.0.3202.75 (Official Build) (64-bit)** on + Windows 10, i7-6700HQ @ 2.6GHz 16GB, GTX 960M 4096MB (Laptop) ### Live Online -[![](img/thumb.png)](http://TODO.github.io/Project5B-WebGL-Deferred-Shading) +[![](images/Capture.png)](https://rms13.github.io/Project5-WebGL-Clustered-Deferred-Forward-Plus/) -### Demo Video/GIF +### Demo GIF -[![](img/video.png)](TODO) +![](images/video2.gif) -### (TODO: Your README) +### Overview +In this project, I worked on implementing Clustered Forward+ and Clustered Deferred renderers. Clustered Forward+ renderer works in a similar way as a Forward rendered, but with one optimization. Here, we divide the view frustum in slices in three axes and bin the lights into the clusters. So in the fragment, only the lights in the cluster of the fragment need to be iterated through. Clustered Deferred takes this one step further by changing the way the scene is rendered. A Deferred shader postpones shading until the end. So we do per-pixel shading and not per-fragment. When shading is the heaviest stage, this is highly efficient than forward methods. -*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. +#### Features +* Clustered Forward+ +* Clustered Deferred +* Blinn-Phong shading (diffuse + specular) for point lights (in Clustered Deferred) +* Simple Toon shading (in Clustered Forward+) +* Optimizations + * Pack values together into vec4s + * Use 2-component normals + +### Performance Analysis + +#### Forward vs. Forward+ vs Deferred + +![](images/chart.png) + +As expected, Deferred shading is the fastest followed by Forward+ followed by Forward. The tests were performed using 100 lights in the sponza model. + +#### 3-Buffers vs 2-Buffers + +![](images/chart1.png) + +Deferred shading requires passing all the data between shaders using g-buffers. For this implementation, I tried reducing the number of g-buffers by packing position, color and normals in 2 vec4s. This can be done by storing the x and y components of the normal in the 4th position of the 2 g-buffers, and computing the z in the fragment shader. This is known as screen-space normals. But the method gives low performance advantage, and causes artefacts as seen below. This happens because the sign of recomputed Z is not retained. So the deployed version does not contain that code. But it can be found in the files in comments. + +**Screen-space normal artefacts** + +![](images/2cn_artefacts.gif) + + +#### Lambertian Shading + +![](images/diffuse.gif) + +#### Blinn-Phong Shading + +![](images/blinn_phong.gif) + +#### Toon Shading + +![](images/toon.gif) -This assignment has a considerable amount of performance analysis compared -to implementation work. Complete the implementation early to leave time! ### Credits diff --git a/images/2cn_artefacts.gif b/images/2cn_artefacts.gif new file mode 100644 index 0000000..afc437c Binary files /dev/null and b/images/2cn_artefacts.gif differ diff --git a/images/Capture.png b/images/Capture.png new file mode 100644 index 0000000..be9079d Binary files /dev/null and b/images/Capture.png differ diff --git a/images/blinn_phong.gif b/images/blinn_phong.gif new file mode 100644 index 0000000..5b5ed5c Binary files /dev/null and b/images/blinn_phong.gif differ diff --git a/images/chart.png b/images/chart.png new file mode 100644 index 0000000..e831952 Binary files /dev/null and b/images/chart.png differ diff --git a/images/chart1.png b/images/chart1.png new file mode 100644 index 0000000..860d25c Binary files /dev/null and b/images/chart1.png differ diff --git a/images/diffuse.gif b/images/diffuse.gif new file mode 100644 index 0000000..33e47df Binary files /dev/null and b/images/diffuse.gif differ diff --git a/images/toon.gif b/images/toon.gif new file mode 100644 index 0000000..53a5667 Binary files /dev/null and b/images/toon.gif differ diff --git a/images/video2.gif b/images/video2.gif new file mode 100644 index 0000000..1f826b3 Binary files /dev/null and b/images/video2.gif differ diff --git a/src/init.js b/src/init.js index 1b09377..d0229bc 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'; @@ -60,7 +60,7 @@ stats.domElement.style.top = '0px'; document.body.appendChild(stats.domElement); // Initialize camera -export const camera = new PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000); +export const camera = new PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 50); // Initialize camera controls export const cameraControls = new OrbitControls(camera, canvas); 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..0a69517 100644 --- a/src/renderers/clustered.js +++ b/src/renderers/clustered.js @@ -13,9 +13,165 @@ export default class ClusteredRenderer { this._zSlices = zSlices; } + createPlane(v0, v1, v2) { + let norm = vec3.create(); + norm = vec3.cross(norm, v1 - v0, v2 - v0); + norm = vec3.normalize(norm, norm); + return norm; + } + + distanceFromPlane(n, p0, v0) { + return vec3.dot(n, p0 - v0) / vec3.length(n); + } + + // function for computing adjacent and opposite side lengths for a RIGHT triangle + computeComponents(dist) { + // adj = 1; opp = d; + let hyp = Math.sqrt(1 / (1 + dist*dist)); + return [hyp, dist*hyp]; // [cos,sin] // explaination in notes... todo: put an image in readme.. + } + + updateClustersOptimized(camera, viewMatrix, scene) { + //console.log("hi"); + for (let z = 0; z < this._zSlices; ++z) { + for (let y = 0; y < this._ySlices; ++y) { + for (let x = 0; x < this._xSlices; ++x) { + let i = x + y * this._xSlices + z * this._xSlices * this._ySlices; + // Reset the light count to 0 for every cluster + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(i, 0)] = 0; + } + } + } + + // LOOP OVER THE LIGHTS + // FIND THE NDC COORDS + // DIRECTLY GET THE X AND Y - USE VIEWPROJ MATRIX ?? + // GET Z BASED ON THE DEPTH VALUE OR FROM AN ARRAY FOR EXPONENTIAL - USE VIEW MATRIX ONLY ?? + // LOOP AROUND AND EXPAND THE RANGE BASED ON THE RADIUS + + let rad = Math.PI / 180; + let halfY = Math.tan((camera.fov / 2) * rad); + let halfX = camera.aspect * halfY; + + let stepY = 2 * halfY / this._ySlices; + let stepX = 2 * halfX / this._xSlices; + let stepZ = (camera.far-camera.near) / this._zSlices; // has nothing to do with FOV... + + + let lRad, lPos, lViewPos = vec4.create(); + + // RUN THREE SEPERATE LOOPS FOR X,Y,Z INSTEAD OF NESTED... + for(let l=0; l lViewPos[2] - lRad) { // search starts at NCP not origin... + zmin = i-1; + break; + } + } + if(zmin >= this._zSlices) { + continue; + } + + for(let i = zmin + 1; i < this._zSlices; i++) { + if (camera.near + i * stepZ > lViewPos[2] + lRad) { + zmax = i; + break; + } + } + if(zmax < 0) { + continue; + } + + // Y + for(let i = 0; i < this._ySlices; i++) { + let nor = this.computeComponents(i * stepY - halfY); + if (vec3.dot(lViewPos, vec3.fromValues(0, nor[0], -nor[1])) < lRad) { + ymin = i-1; + break; + } + } + if(ymin >= this._ySlices) { + continue; + } + + // X + for(let i = ymin + 1; i < this._ySlices; i++) { + let nor = this.computeComponents(i * stepY - halfY); + if (vec3.dot(lViewPos, vec3.fromValues(0, nor[0], -nor[1])) > lRad) { + ymax = i+1; + break; + } + } + if(ymax < 0) { + continue; + } + + for(let i = 0; i < this._xSlices; i++) { + let nor = this.computeComponents(i * stepX - halfX); + if (vec3.dot(lViewPos, vec3.fromValues(nor[0], 0, -nor[1])) < lRad) { + xmin = i-1; + break; + } + } + if(xmin >= this._xSlices) { + continue; + } + + for(let i = xmin + 1; i < this._xSlices; i++) { + let nor = this.computeComponents(i * stepX - halfX); + if (vec3.dot(lViewPos, vec3.fromValues(nor[0], 0, -nor[1])) > lRad) { + xmax = i+1; + break; + } + } + if(xmax < 0) { + continue; + } + + xmin = Math.max(0, xmin); + ymin = Math.max(0, ymin); + zmin = Math.max(0, zmin); + xmax = Math.min(this._xSlices, xmax); + ymax = Math.min(this._ySlices, ymax); + zmax = Math.min(this._zSlices, zmax); + + for (let z = zmin; z < zmax; z++) { + for (let y = ymin; y < ymax; y++) { + for (let x = xmin; x < xmax; x++) { + let idx = x + y * this._xSlices + z * this._xSlices * this._ySlices; + let numLights = ++this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, 0)]; + if(numLights > MAX_LIGHTS_PER_CLUSTER) { + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, 0)]--; + break; + } + let texIdx = Math.floor(numLights / 4); + let offset = numLights - texIdx * 4.0; + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, texIdx) + offset] = l; + } + } + } + } + + this._clusterTexture.update(); + } + 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... for (let z = 0; z < this._zSlices; ++z) { for (let y = 0; y < this._ySlices; ++y) { @@ -27,6 +183,102 @@ export default class ClusteredRenderer { } } + //let expZ = [0.1, 5.0, 6.8, 9.2, 12.6, 17.1, 23.2, 31.5, 42.9, 58.3, 79.2, 108, 146, 199, 271, 368, 500]; + let h = canvas.height; + let w = canvas.width; + + let v0, norm_1, norm_2, norm_3, norm_4; // variables representing planes.. + let v1_1, v1_2, v1_3, v1_4, v2_1, v2_2, v2_3, v2_4, yScaled, xScaled; // helper vars.. + v0 = camera.position; + + let zScale = 1000/this._zSlices; + for (let z = 0; z < this._zSlices; ++z) { + let z1 = z * zScale; //expZ[z]; + let z2 = z1 + zScale; //expZ[z + 1]; + for (let y = 0; y < this._ySlices; ++y) { + // LOWER PLANE + if (y === 0) { + yScaled = 0; + v1_1 = vec3.fromValues(0, yScaled, 1000); + v2_1 = vec3.fromValues(10, yScaled, 1000); + norm_1 = this.createPlane(v0, v1_1, v2_1); + } + else { + norm_1 = norm_2; // use from last iteration.. + } + + // UPPER PLANE + yScaled += h/this._ySlices; + v1_2 = vec3.fromValues(0, yScaled, 1000); + v2_2 = vec3.fromValues(10, yScaled, 1000); + norm_2 = this.createPlane(v0, v1_2, v2_2); + + for (let x = 0; x < this._xSlices; ++x) { + let i = x + y * this._xSlices + z * this._xSlices * this._ySlices; + + // LEFT PLANE + if (x === 0) { + xScaled = 0; + v1_3 = vec3.fromValues(xScaled, 0, 1000); + v2_3 = vec3.fromValues(xScaled, 10, 1000); + norm_3 = this.createPlane(v0, v1_3, v2_3); + } + else { + norm_3 = norm_4; + } + + // RIGHT PLANE + xScaled += w/this._xSlices; + v1_4 = vec3.fromValues(xScaled, 0, 1000); + v2_4 = vec3.fromValues(xScaled, 10, 1000); + norm_4 = this.createPlane(v0, v1_4, v2_4); + + // create 2 X planes + // loop and assign lights + for(let l=0; l z2) { + continue; + } + + // LOWER PLANE + let dist = this.distanceFromPlane(norm_1, p0, v0); + if (dist > scene.LIGHT_RADIUS) { + continue; + } + + // UPPER PLANE + dist = this.distanceFromPlane(norm_2, p0, v0); + if (dist > scene.LIGHT_RADIUS) { + continue; + } + + // lEFT PLANE + dist = this.distanceFromPlane(norm_3, p0, v0); + if (dist > scene.LIGHT_RADIUS) { + continue; + } + + // RIGHT PLANE + dist = this.distanceFromPlane(norm_4, p0, v0); + if (dist > scene.LIGHT_RADIUS) { + continue; + } + + let numLights = ++this._clusterTexture.buffer[this._clusterTexture.bufferIndex(i, 0)]; + let texIdx = Math.floor(numLights / 4.0); + let offset = numLights - texIdx * 4.0; + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(i, texIdx) + offset] = l; + } + } + } + } + this._clusterTexture.update(); } -} \ No newline at end of file + +} diff --git a/src/renderers/clusteredDeferred.js b/src/renderers/clusteredDeferred.js index 5e28e84..29b242d 100644 --- a/src/renderers/clusteredDeferred.js +++ b/src/renderers/clusteredDeferred.js @@ -2,6 +2,7 @@ import { gl, WEBGL_draw_buffers, canvas } from '../init'; import { mat4, vec4 } from 'gl-matrix'; import { loadShaderProgram, renderFullscreenQuad } from '../utils'; import { NUM_LIGHTS } from '../scene'; +import { MAX_LIGHTS_PER_CLUSTER } from './clustered'; import toTextureVert from '../shaders/deferredToTexture.vert.glsl'; import toTextureFrag from '../shaders/deferredToTexture.frag.glsl'; import QuadVertSource from '../shaders/quad.vert.glsl'; @@ -9,7 +10,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) { @@ -21,16 +22,18 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { this._lightTexture = new TextureBuffer(NUM_LIGHTS, 8); this._progCopy = loadShaderProgram(toTextureVert, toTextureFrag, { - uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap'], - attribs: ['a_position', 'a_normal', 'a_uv'], + uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_viewMatrix'], + attribs: ['a_position', 'a_normal', 'a_uv'] }); this._progShade = loadShaderProgram(QuadVertSource, fsSource({ numLights: NUM_LIGHTS, + maxLights: MAX_LIGHTS_PER_CLUSTER, numGBuffers: NUM_GBUFFERS, + xSlices: xSlices, ySlices: ySlices, zSlices: zSlices }), { - uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]'], - attribs: ['a_uv'], + uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_lightbuffer', 'u_clusterbuffer', 'u_viewMatrix', 'u_screenW', 'u_screenH', 'u_camN', 'u_camF', 'u_camPos'], + attribs: ['a_uv'] }); this._projectionMatrix = mat4.create(); @@ -124,6 +127,9 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { // Upload the camera matrix gl.uniformMatrix4fv(this._progCopy.u_viewProjectionMatrix, false, this._viewProjectionMatrix); + // view matrix + gl.uniformMatrix4fv(this._progCopy.u_viewMatrix, false, this._viewMatrix); + // Draw the scene. This function takes the shader program so that the model's textures can be bound to the right inputs scene.draw(this._progCopy); @@ -142,7 +148,7 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { this._lightTexture.update(); // Update the clusters for the frame - this.updateClusters(camera, this._viewMatrix, scene); + this.updateClustersOptimized(camera, this._viewMatrix, scene); // Bind the default null framebuffer which is the screen gl.bindFramebuffer(gl.FRAMEBUFFER, null); @@ -154,6 +160,12 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { gl.useProgram(this._progShade.glShaderProgram); // TODO: Bind any other shader inputs + gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix); + gl.uniform1f(this._progShade.u_screenW, canvas.width); + gl.uniform1f(this._progShade.u_screenH, canvas.height); + gl.uniform1f(this._progShade.u_camN, camera.near); + gl.uniform1f(this._progShade.u_camF, camera.far); + gl.uniform3f(this._progShade.u_camPos, camera.position.x, camera.position.y, camera.position.z); // Bind g-buffers const firstGBufferBinding = 0; // You may have to change this if you use other texture slots @@ -163,6 +175,17 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { gl.uniform1i(this._progShade[`u_gbuffers[${i}]`], i + firstGBufferBinding); } + // Bind the light and cluster textures... + // Set the light texture as a uniform input to the shader + gl.activeTexture(gl.TEXTURE3); + gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture); + gl.uniform1i(this._progShade.u_lightbuffer, 3); + + // Set the cluster texture as a uniform input to the shader + gl.activeTexture(gl.TEXTURE4); + gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture); + gl.uniform1i(this._progShade.u_clusterbuffer, 4); + renderFullscreenQuad(this._progShade); } }; diff --git a/src/renderers/clusteredForwardPlus.js b/src/renderers/clusteredForwardPlus.js index 9e8afbe..d2fb5a1 100644 --- a/src/renderers/clusteredForwardPlus.js +++ b/src/renderers/clusteredForwardPlus.js @@ -2,6 +2,7 @@ import { gl } from '../init'; import { mat4, vec4, vec3 } from 'gl-matrix'; import { loadShaderProgram } from '../utils'; import { NUM_LIGHTS } from '../scene'; +import { MAX_LIGHTS_PER_CLUSTER } from './clustered'; import vsSource from '../shaders/clusteredForward.vert.glsl'; import fsSource from '../shaders/clusteredForward.frag.glsl.js'; import TextureBuffer from './textureBuffer'; @@ -16,9 +17,13 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { this._shaderProgram = loadShaderProgram(vsSource, fsSource({ numLights: NUM_LIGHTS, + maxLights: MAX_LIGHTS_PER_CLUSTER, + xSlices: xSlices, + ySlices: ySlices, + zSlices: zSlices }), { - uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer'], - attribs: ['a_position', 'a_normal', 'a_uv'], + uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer', 'u_viewMatrix', 'u_screenW', 'u_screenH', 'u_camN', 'u_camF', 'u_camPos'], + attribs: ['a_position', 'a_normal', 'a_uv'] }); this._projectionMatrix = mat4.create(); @@ -34,7 +39,8 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { mat4.multiply(this._viewProjectionMatrix, this._projectionMatrix, this._viewMatrix); // Update cluster texture which maps from cluster index to light list - this.updateClusters(camera, this._viewMatrix, scene); + //this.updateClusters(camera, this._viewMatrix, scene); + this.updateClustersOptimized(camera, this._viewMatrix, scene); // Update the buffer used to populate the texture packed with light data for (let i = 0; i < NUM_LIGHTS; ++i) { @@ -76,8 +82,14 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { gl.uniform1i(this._shaderProgram.u_clusterbuffer, 3); // TODO: Bind any other shader inputs - + //this._sceneDim = ivec4.fromValues(canvas.width, canvas.height, camera.near, camera.far); + gl.uniformMatrix4fv(this._shaderProgram.u_viewMatrix, false, this._viewMatrix); + gl.uniform1f(this._shaderProgram.u_screenW, canvas.width); + gl.uniform1f(this._shaderProgram.u_screenH, canvas.height); + gl.uniform1f(this._shaderProgram.u_camN, camera.near); + gl.uniform1f(this._shaderProgram.u_camF, camera.far); + gl.uniform3f(this._shaderProgram.u_camPos, camera.position.x, camera.position.y, camera.position.z); // Draw the scene. This function takes the shader program so that the model's textures can be bound to the right inputs scene.draw(this._shaderProgram); } -}; \ No newline at end of file +}; diff --git a/src/renderers/forward.js b/src/renderers/forward.js index ac044f9..c949607 100644 --- a/src/renderers/forward.js +++ b/src/renderers/forward.js @@ -13,10 +13,10 @@ export default class ForwardRenderer { // Initialize a shader program. The fragment shader source is compiled based on the number of lights this._shaderProgram = loadShaderProgram(vsSource, fsSource({ - numLights: NUM_LIGHTS, + numLights: NUM_LIGHTS }), { uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer'], - attribs: ['a_position', 'a_normal', 'a_uv'], + attribs: ['a_position', 'a_normal', 'a_uv'] }); this._projectionMatrix = mat4.create(); diff --git a/src/shaders/clusteredForward.frag.glsl.js b/src/shaders/clusteredForward.frag.glsl.js index 022fda7..15e9760 100644 --- a/src/shaders/clusteredForward.frag.glsl.js +++ b/src/shaders/clusteredForward.frag.glsl.js @@ -1,7 +1,5 @@ export default function(params) { return ` - // TODO: This is pretty much just a clone of forward.frag.glsl.js - #version 100 precision highp float; @@ -12,6 +10,13 @@ export default function(params) { // TODO: Read this buffer to determine the lights influencing a cluster uniform sampler2D u_clusterbuffer; + uniform mat4 u_viewMatrix; + uniform float u_screenW; + uniform float u_screenH; + uniform float u_camN; + uniform float u_camF; + uniform vec3 u_camPos; + varying vec3 v_position; varying vec3 v_normal; varying vec2 v_uv; @@ -81,19 +86,68 @@ export default function(params) { vec3 fragColor = vec3(0.0); - for (int i = 0; i < ${params.numLights}; ++i) { - Light light = UnpackLight(i); + // Determine the cluster for a fragment + // Read in the lights in that cluster from the populated data + // Do shading for just those lights + + ivec3 clusterPos = ivec3( + int(gl_FragCoord.x / u_screenW * float(${params.xSlices})), + int(gl_FragCoord.y / u_screenH * float(${params.ySlices})), + int((-(u_viewMatrix * vec4(v_position,1.0)).z - u_camN) / (u_camF - u_camN) * float(${params.zSlices})) + ); + + // optimize z using non linear scale once linear works.. + // show perf. comparison.. + + // use UnpackLight() logic to read lightIdx, and then use UnpackLight() to read light from that idx.. + int clusterIdx = clusterPos.x + clusterPos.y * ${params.xSlices} + clusterPos.z * ${params.xSlices} * ${params.ySlices}; + int clusterWidth = ${params.xSlices} * ${params.ySlices} * ${params.zSlices}; + int clusterHeight = int(float(${params.maxLights}+1) / 4.0) + 1; + float clusterU = float(clusterIdx + 1) / float(clusterWidth + 1); // like u in UnpackLight().. + + int numLights = int(texture2D(u_clusterbuffer, vec2(clusterU, 0.0)).x); // clamp to max lights in scene if this misbehaves.. + + for (int i = 0; i < ${params.numLights}; i++) { + if(i >= numLights) { + break; + } + + int clusterPixel = int(float(i+1) / 4.0); // FIXED BUG: offset by 1 + float clusterV = float(clusterPixel+1) / float(clusterHeight+1); + vec4 texel = texture2D(u_clusterbuffer, vec2(clusterU, clusterV)); + int lightIdx; + int clusterPixelComponent = (i+1) - (clusterPixel * 4); + if (clusterPixelComponent == 0) { + lightIdx = int(texel[0]); + } else if (clusterPixelComponent == 1) { + lightIdx = int(texel[1]); + } else if (clusterPixelComponent == 2) { + lightIdx = int(texel[2]); + } else if (clusterPixelComponent == 3) { + lightIdx = int(texel[3]); + } + + // shading + Light light = UnpackLight(lightIdx); float lightDistance = distance(light.position, v_position); vec3 L = (light.position - v_position) / lightDistance; float lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius); - float lambertTerm = max(dot(L, normal), 0.0); + float lambertTerm = floor(max(dot(normalize(u_camPos-v_position), normal), 0.0) * 4.0) / 4.0; + //float lambertTerm = max(dot(L, normal), 0.0); + + float specular = 0.0; + // blinn-phong shading... https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_shading_model + // vec3 viewDir = normalize(u_camPos-v_position); + // vec3 halfDir = normalize(L + viewDir); + // float specAngle = max(dot(halfDir, normal), 0.0); + // specular = pow(specAngle, 100.0); // 100 -> shininess - fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity); + fragColor += (albedo + vec3(specular)) * lambertTerm * light.color * vec3(lightIntensity); } const vec3 ambientLight = vec3(0.025); - fragColor += albedo * ambientLight; + fragColor += albedo * ambientLight; // float(numLights) // vec3(float(u_slices.x)/2.0, float(u_slices.y)/2.0, float(u_slices.z)/2.0) gl_FragColor = vec4(fragColor, 1.0); } diff --git a/src/shaders/deferred.frag.glsl.js b/src/shaders/deferred.frag.glsl.js index 50f1e75..617a76b 100644 --- a/src/shaders/deferred.frag.glsl.js +++ b/src/shaders/deferred.frag.glsl.js @@ -2,19 +2,140 @@ export default function(params) { return ` #version 100 precision highp float; + + uniform sampler2D u_lightbuffer; uniform sampler2D u_gbuffers[${params.numGBuffers}]; varying vec2 v_uv; - + + uniform sampler2D u_clusterbuffer; + uniform mat4 u_viewMatrix; + uniform float u_screenW; + uniform float u_screenH; + uniform float u_camN; + uniform float u_camF; + uniform vec3 u_camPos; + + struct Light { + vec3 position; + float radius; + vec3 color; + }; + + 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]; + } + } + + Light UnpackLight(int index) { + Light light; + float u = float(index + 1) / float(${params.numLights + 1}); + vec4 v1 = texture2D(u_lightbuffer, vec2(u, 0.0)); + vec4 v2 = texture2D(u_lightbuffer, vec2(u, 0.5)); + light.position = v1.xyz; + light.radius = ExtractFloat(u_lightbuffer, ${params.numLights}, 2, index, 3); + light.color = v2.rgb; + return light; + } + + // Cubic approximation of gaussian curve so we falloff to exactly 0 at the light radius + 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; + } + } + void main() { - // TODO: extract data from g buffers and do lighting + // 2 COMPONENT NORMALS: // 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 gb3 = texture2D(u_gbuffers[3], v_uv); + // vec3 v_position = gb0.xyz; + // vec3 albedo = gb1.rgb; + // vec3 normal = vec3(gb0.w, gb1.w, sqrt(abs(1.0 - gb0.w * gb0.w - gb1.w * gb1.w)));// z2 = 1 - x2 - y2.. + + vec3 v_position = texture2D(u_gbuffers[0], v_uv).xyz; + vec3 albedo = texture2D(u_gbuffers[1], v_uv).xyz; + vec3 normal = texture2D(u_gbuffers[2], v_uv).xyz; + + vec3 fragColor = vec3(0.0); + + ivec3 clusterPos = ivec3( + int(gl_FragCoord.x / u_screenW * float(${params.xSlices})), + int(gl_FragCoord.y / u_screenH * float(${params.ySlices})), + int((-(u_viewMatrix * vec4(v_position,1.0)).z - u_camN) / (u_camF - u_camN) * float(${params.zSlices})) + ); + + // optimize z using non linear scale once linear works.. + // show perf. comparison.. + + // use UnpackLight() logic to read lightIdx, and then use UnpackLight() to read light from that idx.. + + int clusterIdx = clusterPos.x + clusterPos.y * ${params.xSlices} + clusterPos.z * ${params.xSlices} * ${params.ySlices}; + int clusterWidth = ${params.xSlices} * ${params.ySlices} * ${params.zSlices}; + int clusterHeight = int(float(${params.maxLights}+1) / 4.0) + 1; + float clusterU = float(clusterIdx + 1) / float(clusterWidth + 1); // like u in UnpackLight().. + + int numLights = int(texture2D(u_clusterbuffer, vec2(clusterU, 0.0)).x); // clamp to max lights in scene if this misbehaves.. + + for (int i = 0; i < ${params.numLights}; i++) { + if(i >= numLights) { + break; + } + + int clusterPixel = int(float(i+1) / 4.0); // FIXED BUG: offset by 1 + float clusterV = float(clusterPixel+1) / float(clusterHeight+1); + vec4 texel = texture2D(u_clusterbuffer, vec2(clusterU, clusterV)); + int lightIdx; + int clusterPixelComponent = (i+1) - (clusterPixel * 4); + if (clusterPixelComponent == 0) { + lightIdx = int(texel[0]); + } else if (clusterPixelComponent == 1) { + lightIdx = int(texel[1]); + } else if (clusterPixelComponent == 2) { + lightIdx = int(texel[2]); + } else if (clusterPixelComponent == 3) { + lightIdx = int(texel[3]); + } + + // shading + Light light = UnpackLight(lightIdx); + float lightDistance = distance(light.position, v_position); + vec3 L = (light.position - v_position) / lightDistance; + + float lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius); + float lambertTerm = max(dot(L, normal), 0.0); + + float specular = 0.0; + // blinn-phong shading... https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_shading_model + vec3 viewDir = normalize(u_camPos - v_position); + vec3 halfDir = normalize(L + viewDir); + float specAngle = max(dot(halfDir, normal), 0.0); + specular = pow(specAngle, 100.0); // 100 -> shininess + + fragColor += (albedo + vec3(specular)) * lambertTerm * light.color * vec3(lightIntensity); + } + + const vec3 ambientLight = vec3(0.025); + fragColor += albedo * ambientLight; - gl_FragColor = vec4(v_uv, 0.0, 1.0); + 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..e7c8704 100644 --- a/src/shaders/deferredToTexture.frag.glsl +++ b/src/shaders/deferredToTexture.frag.glsl @@ -5,6 +5,8 @@ precision highp float; uniform sampler2D u_colmap; uniform sampler2D u_normap; +uniform mat4 u_viewMatrix; + varying vec3 v_position; varying vec3 v_normal; varying vec2 v_uv; @@ -22,8 +24,13 @@ 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[3] = ?? -} \ No newline at end of file + + gl_FragData[0] = vec4(v_position, 1.0); + gl_FragData[1] = vec4(col, 1.0); + gl_FragData[2] = vec4(norm, 1.0); + + // save space using screen space normals + // https://computergraphics.stackexchange.com/questions/3942/screenspace-normals-creation-normal-maps-and-unpacking -> z = sqrt(1-x2-y2); + // gl_FragData[0] = vec4(v_position, norm.x); + // gl_FragData[1] = vec4(col, norm.y); +}