diff --git a/README.md b/README.md index ec3c73b..f4a12be 100755 --- a/README.md +++ b/README.md @@ -3,29 +3,70 @@ WebGL Clustered 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) +* Ziad Ben Hadj-Alouane + * [LinkedIn](https://www.linkedin.com/in/ziadbha/), [personal website](https://www.seas.upenn.edu/~ziadb/) +* Tested on: Google Chrome Version 70.0.3538.77 (WebGL), Windows 10, i7-8750H @ 2.20GHz, 16GB, GTX 1060 -### Live Online +# Video Demo + [](https://www.youtube.com/watch?v=J1Pvi4GN62o) -[![](img/thumb.png)](http://TODO.github.io/Project5B-WebGL-Deferred-Shading) +# Deferred Shading Intro +This project showcases an implementation of Forward shading, with extensions: Forward+ and Clustered/Deferred shading. + * For **Forward** shading, we supply the graphics card the geometry data, which is then projected, broken into vertices, and split into fragments. Each fragment then gets the final lighting treatment before they are passed onto the screen. + * For **Forward+** shading, we do the same thing except that we break our viewing frustum into pieces, and compute the lights that overlap these pieces. As such, we can determine which section (i.e cluster) a fragment is in, and iterate over a select few number of lights. + * For **Clustered** shading (deferred shading), we do the same thing except that the rendering is deferred a little bit until all of the geometries have passed down many stages. The final image is then obtained by doing lighting calculations at the end. This essentially requires more passes on the scene. + +This project is fully implemented with Javascript and WebGL. See a live demo above. -### Demo Video/GIF +## Implementing the Cluster Datastructure +To implement clustering, we must be able to break up our viewing frustum into sectors in every dimension (X, Y, and Z). Since the Z dimension is simple (from near clip to far clip), it is simple to test intersections of spheres with it. However, for the X and Y dimensions, our view grows in a cone shape. The image below helps visualize the test we do for each frustum section and each spherical light. -[![](img/video.png)](TODO) +

-### (TODO: Your README) +We essentially represent each section of the frustum by a right triangle with sides 1 and d, d being the distance from 0. We then represent it with a normalized vector (the light blue one). Now if we dot that vector with the position vector of the sphere (the brown one), it will result in measuring the red distance outlined in the image above. We compare that red distance with the radius to get a sense of where the sphere is with respect to our frustum. -*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. +## Scene +### Sponza +

-This assignment has a considerable amount of performance analysis compared -to implementation work. Complete the implementation early to leave time! +#### Views +| Depth View | Normals View | Non-Debug View | +| ------------- | ----------- | ----------- | +|

|

|

| +#### Blinn-Phong Effect +

-### Credits +Blinn-Phong is a shading effect achieved by adding a specular component to the albedo color: + +~~~~ +vec3 halfDirection = lightDirection + viewDirection +float angle = dot(halfDirection, normal); +float spec = pow(angle, exponent); +albedo += spec; +~~~~ + +Adding Blinn-Phong virtually has no performance impact. It is an extra 3 instructions per lighting computation. + +# Performance +## Forward vs. Forward+ vs. Clustered/Deferred +The graph below shows performance differences for the different shading techniques. Overall, the more lights we have, the better Clustering is. However, if we have a low number of lights (say 100), then clustering does more work than needed, hurting performance. +

+ +This is easily explained by the lost benefit of creating the clustering data-structure in the forward+ cases: the less lights you have, the less iterations you would have done anyways. + +## Packing Normals in the Position and Color G-Buffers +To reduce the amount of G-Buffers we use, I pack the normals' x and y values in the w coordinate of the position and color vectors. I then retrieve the z coordinate since the normal vector is normalized. The sign value is not lost, since I also multiply the Red color channel by -1 if the z coordinate is negative. There are still some artifacts as showcased below: +| With Packing | Without Packing | +| ------------- | ----------- | +|

|

| + +As for performance, packing clearly wins because we do less global memory reads (at the expense of slightly more computation) +

+ + +### Credits * [Three.js](https://github.com/mrdoob/three.js) by [@mrdoob](https://github.com/mrdoob) and contributors * [stats.js](https://github.com/mrdoob/stats.js) by [@mrdoob](https://github.com/mrdoob) and contributors * [webgl-debug](https://github.com/KhronosGroup/WebGLDeveloperTools) by Khronos Group Inc. diff --git a/imgs/blinn.jpg b/imgs/blinn.jpg new file mode 100644 index 0000000..a9e1e32 Binary files /dev/null and b/imgs/blinn.jpg differ diff --git a/imgs/debug_none.jpg b/imgs/debug_none.jpg new file mode 100644 index 0000000..495185d Binary files /dev/null and b/imgs/debug_none.jpg differ diff --git a/imgs/debug_norm.jpg b/imgs/debug_norm.jpg new file mode 100644 index 0000000..3d70bca Binary files /dev/null and b/imgs/debug_norm.jpg differ diff --git a/imgs/debug_z.png b/imgs/debug_z.png new file mode 100644 index 0000000..3feac09 Binary files /dev/null and b/imgs/debug_z.png differ diff --git a/imgs/packed.png b/imgs/packed.png new file mode 100644 index 0000000..aea4f9b Binary files /dev/null and b/imgs/packed.png differ diff --git a/imgs/performance_diff.png b/imgs/performance_diff.png new file mode 100644 index 0000000..3c75a7b Binary files /dev/null and b/imgs/performance_diff.png differ diff --git a/imgs/performance_packing.png b/imgs/performance_packing.png new file mode 100644 index 0000000..0768a12 Binary files /dev/null and b/imgs/performance_packing.png differ diff --git a/imgs/test.png b/imgs/test.png new file mode 100644 index 0000000..e7d2e14 Binary files /dev/null and b/imgs/test.png differ diff --git a/imgs/thumb.jpg b/imgs/thumb.jpg new file mode 100644 index 0000000..024693f Binary files /dev/null and b/imgs/thumb.jpg differ diff --git a/imgs/top.gif b/imgs/top.gif new file mode 100644 index 0000000..823e620 Binary files /dev/null and b/imgs/top.gif differ diff --git a/imgs/unpacked.png b/imgs/unpacked.png new file mode 100644 index 0000000..da1464e Binary files /dev/null and b/imgs/unpacked.png differ diff --git a/src/renderers/base.js b/src/renderers/base.js index 8a975b9..9ff5c78 100755 --- a/src/renderers/base.js +++ b/src/renderers/base.js @@ -1,30 +1,203 @@ import TextureBuffer from './textureBuffer'; +import {vec4, vec3} from 'gl-matrix'; +import {NUM_LIGHTS} from "../scene"; export const MAX_LIGHTS_PER_CLUSTER = 100; +/** + * Converts an angle in degrees to radians + */ +function toRadian(deg) { + return deg * Math.PI / 180; +} + +/** + * Returns an array containing the cosine and sine of a right triangle with sides 1 and opposite + */ +function getNormalForTriangle(opposite) { + let hypothenuse = Math.sqrt(1 + opposite * opposite); + return [1 / hypothenuse, opposite / hypothenuse]; +} + +/** + * Explained in detail in the README. Does frustum/sphere intersection test by approximating the frustum section + * as a right triangle with sides 1 and currentAxisDistance + */ +function getDotForFrustumCheck(currentAxisDistance, camSpaceLightPos, axis) { + // get the info for the frustum vector + let normalCoords = getNormalForTriangle(currentAxisDistance); + + // flip the vector by around the cosine axis + let normal; + if (axis === "x") { + // X plane + normal = vec3.fromValues(normalCoords[0], 0, -normalCoords[1]); + } else { + // Y plane + normal = vec3.fromValues(0, normalCoords[0], -normalCoords[1]); + } + + + // return the dot product of the frustum vector with the light position vector + return vec3.dot(camSpaceLightPos, normal); +} + export default class BaseRenderer { - constructor(xSlices, ySlices, zSlices) { - // Create a texture to store cluster data. Each cluster stores the number of lights followed by the light indices - this._clusterTexture = new TextureBuffer(xSlices * ySlices * zSlices, MAX_LIGHTS_PER_CLUSTER + 1); - this._xSlices = xSlices; - this._ySlices = ySlices; - this._zSlices = zSlices; - } - - 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) { - 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; - } - } + constructor(xSlices, ySlices, zSlices) { + // Create a texture to store cluster data. Each cluster stores the number of lights followed by the light indices + this._clusterTexture = new TextureBuffer(xSlices * ySlices * zSlices, MAX_LIGHTS_PER_CLUSTER + 1); + this._xSlices = xSlices; + this._ySlices = ySlices; + this._zSlices = zSlices; } - this._clusterTexture.update(); - } + updateClusters(camera, viewMatrix, scene) { + /** Init light counter to 0 **/ + 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; + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(i, 0)] = 0; + } + } + } + + /** Get Frustum Info ***/ + // tangent of half of vertical fov gives us the half the vertical camera frustum + let verticalStretch = 2.0 * Math.tan(toRadian(camera.fov / 2)); + let xSliceLength = verticalStretch * camera.aspect / this._xSlices; + let ySliceLength = verticalStretch / this._ySlices; + let depthStretch = camera.far - camera.near; + let zSliceLength = depthStretch / this._zSlices; + + /** For every light, compute the clusters**/ + for (let lightIdx = 0; lightIdx < NUM_LIGHTS; lightIdx++) { + // get light info: radius and world position + let lightRad = scene.lights[lightIdx].radius; + let lightPos = vec4.create(); + lightPos[0] = scene.lights[lightIdx].position[0]; + lightPos[1] = scene.lights[lightIdx].position[1]; + lightPos[2] = scene.lights[lightIdx].position[2]; + lightPos[3] = 1; + + // transform world position to camera space. Cam is positioned at 0 origin in this space + lightPos = vec4.transformMat4(lightPos, lightPos, viewMatrix); + lightPos[2] = -lightPos[2]; + + // vec3 version of light pos, adjusted for -z + let lightPos3 = vec3.fromValues(lightPos[0], lightPos[1], lightPos[2]); + + /*************** X DIMENSION ********************/ + // For the X & Y dimensions, we use a trigonometric method of computing + // sphere + frustum section intersection. See README for details + let minX = this._xSlices; + let maxX = this._xSlices; + for (let i = 0; i < this._xSlices; i++) { + // start at -half of X axis + let dot = getDotForFrustumCheck(i * xSliceLength - ((verticalStretch / 2.0) * camera.aspect), + lightPos3, "x"); + if (dot < lightRad) { + // if a section of the frustum is inside the sphere, then take previous section + minX = i - 1; // clamp this later just in case == -1 + break; + } + } + if (minX >= this._xSlices) { + continue; + } + + for (let i = minX + 1; i < this._xSlices; i++) { + let dot = getDotForFrustumCheck(i * xSliceLength - ((verticalStretch / 2.0) * camera.aspect), + lightPos3, "x"); + if (dot > lightRad) { + // if a section of the frustum becomes outside the sphere, then we have it + maxX = i; + break; + } + } + + /*************** Y DIMENSION ********************/ + let minY = this._ySlices; + let maxY = this._ySlices; + for (let i = 0; i < this._ySlices; i++) { + // start at -half of Y axis + let dot = getDotForFrustumCheck(i * ySliceLength - (verticalStretch / 2.0), lightPos3, "y"); + if (dot < lightRad) { + minY = i - 1; + break; + } + } + if (minY >= this._ySlices) { + continue; + } + + for (let i = minY + 1; i < this._ySlices; i++) { + // start at -half of Y axis + let dot = getDotForFrustumCheck(i * ySliceLength - (verticalStretch / 2.0), lightPos3, "y"); + if (dot > lightRad) { + maxY = i; + break; + } + } + + /*************** Z DIMENSION ********************/ + // Z dimension is purely linear (from near clip to far clip) + let minZ = this._zSlices; + let maxZ = -1; + for (let i = 0; i < this._zSlices; i++) { + let currZ = i * zSliceLength + camera.near; + if (currZ > lightPos[2] - lightRad) { + minZ = i - 1; // take the previous one because we still need to be < pos - rad + break; + } + } + if (minZ >= this._zSlices) { + continue; + } + + for (let i = minZ + 1; i < this._zSlices; i++) { + let currZ = i * zSliceLength + camera.near; + if (currZ > lightPos[2] + lightRad) { + maxZ = i; // take the current one because it has the most recent change + break; + } + } + if (maxZ <= -1) { + continue; + } + + /********* CLAMPING THE BOUNDS ************/ + // clamp cluster bounds + maxZ = Math.min(maxZ, this._zSlices - 1); + maxY = Math.min(maxY, this._ySlices - 1); + maxX = Math.min(maxX, this._xSlices - 1); + + minZ = Math.max(minZ, 0); + minY = Math.max(minY, 0); + minX = Math.max(minX, 0); + + /************ UPDATING AFFECTED CLUSTERS ************/ + // iterate over the cluster bounds and fill the buffers + // optimized loop goes z y then x + for (let z = minZ; z <= maxZ; z++) { + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x++) { + // get current num lights, check if == num lights, and update if not + let clusterIdx = x + y * this._xSlices + z * this._ySlices * this._xSlices; + let nbLights = this._clusterTexture.buffer[this._clusterTexture.bufferIndex(clusterIdx, 0)]; + if (nbLights >= MAX_LIGHTS_PER_CLUSTER) { + break; + } + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(clusterIdx, 0)] = nbLights + 1; + + // update to store light index + let row = Math.floor((nbLights + 1) / 4); + let component = nbLights + 1 - row * 4; + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(clusterIdx, row) + component] = lightIdx; + } + } + } + } + this._clusterTexture.update(); + } } \ No newline at end of file diff --git a/src/renderers/clustered.js b/src/renderers/clustered.js index 46b8278..e08ea76 100755 --- a/src/renderers/clustered.js +++ b/src/renderers/clustered.js @@ -1,168 +1,187 @@ -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 {gl, WEBGL_draw_buffers, canvas} from '../init'; +import {mat4, vec4} from 'gl-matrix'; +import {loadShaderProgram, renderFullscreenQuad} from '../utils'; +import {NUM_LIGHTS} from '../scene'; import toTextureVert from '../shaders/deferredToTexture.vert.glsl'; import toTextureFrag from '../shaders/deferredToTexture.frag.glsl'; import QuadVertSource from '../shaders/quad.vert.glsl'; import fsSource from '../shaders/deferred.frag.glsl.js'; import TextureBuffer from './textureBuffer'; -import BaseRenderer from './base'; +import BaseRenderer, {MAX_LIGHTS_PER_CLUSTER} from './base'; -export const NUM_GBUFFERS = 4; +export const NUM_GBUFFERS = 2; export default class ClusteredRenderer extends BaseRenderer { - constructor(xSlices, ySlices, zSlices) { - super(xSlices, ySlices, zSlices); - - this.setupDrawBuffers(canvas.width, canvas.height); - - // Create a texture to store light data - 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'], - }); - - this._progShade = loadShaderProgram(QuadVertSource, fsSource({ - numLights: NUM_LIGHTS, - numGBuffers: NUM_GBUFFERS, - }), { - uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]'], - attribs: ['a_uv'], - }); - - this._projectionMatrix = mat4.create(); - this._viewMatrix = mat4.create(); - this._viewProjectionMatrix = mat4.create(); - } - - setupDrawBuffers(width, height) { - this._width = width; - this._height = height; - - this._fbo = gl.createFramebuffer(); - - //Create, bind, and store a depth target texture for the FBO - this._depthTex = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this._depthTex); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null); - gl.bindTexture(gl.TEXTURE_2D, null); - - gl.bindFramebuffer(gl.FRAMEBUFFER, this._fbo); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this._depthTex, 0); - - // Create, bind, and store "color" target textures for the FBO - this._gbuffers = new Array(NUM_GBUFFERS); - let attachments = new Array(NUM_GBUFFERS); - for (let i = 0; i < NUM_GBUFFERS; i++) { - attachments[i] = WEBGL_draw_buffers[`COLOR_ATTACHMENT${i}_WEBGL`]; - this._gbuffers[i] = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, this._gbuffers[i]); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null); - gl.bindTexture(gl.TEXTURE_2D, null); - - gl.framebufferTexture2D(gl.FRAMEBUFFER, attachments[i], gl.TEXTURE_2D, this._gbuffers[i], 0); + constructor(xSlices, ySlices, zSlices) { + super(xSlices, ySlices, zSlices); + + this.setupDrawBuffers(canvas.width, canvas.height); + + // Create a texture to store light data + 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'], + }); + + this._progShade = loadShaderProgram(QuadVertSource, fsSource({ + numLights: NUM_LIGHTS, + numGBuffers: NUM_GBUFFERS, + xSlices: xSlices, ySlices: ySlices, zSlices: zSlices, + maxLights: MAX_LIGHTS_PER_CLUSTER, + }), { + uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]', + 'u_lightbuffer', 'u_clusterbuffer', 'u_viewMat', + 'u_nearClip', 'u_farClip', 'u_width', 'u_height'], + attribs: ['a_uv'], + }); + + this._projectionMatrix = mat4.create(); + this._viewMatrix = mat4.create(); + this._viewProjectionMatrix = mat4.create(); } - if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) { - throw "Framebuffer incomplete"; + setupDrawBuffers(width, height) { + this._width = width; + this._height = height; + + this._fbo = gl.createFramebuffer(); + + //Create, bind, and store a depth target texture for the FBO + this._depthTex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this._depthTex); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null); + gl.bindTexture(gl.TEXTURE_2D, null); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this._fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this._depthTex, 0); + + // Create, bind, and store "color" target textures for the FBO + this._gbuffers = new Array(NUM_GBUFFERS); + let attachments = new Array(NUM_GBUFFERS); + for (let i = 0; i < NUM_GBUFFERS; i++) { + attachments[i] = WEBGL_draw_buffers[`COLOR_ATTACHMENT${i}_WEBGL`]; + this._gbuffers[i] = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this._gbuffers[i]); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null); + gl.bindTexture(gl.TEXTURE_2D, null); + + gl.framebufferTexture2D(gl.FRAMEBUFFER, attachments[i], gl.TEXTURE_2D, this._gbuffers[i], 0); + } + + if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) { + throw "Framebuffer incomplete"; + } + + // Tell the WEBGL_draw_buffers extension which FBO attachments are + // being used. (This extension allows for multiple render targets.) + WEBGL_draw_buffers.drawBuffersWEBGL(attachments); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); } - // Tell the WEBGL_draw_buffers extension which FBO attachments are - // being used. (This extension allows for multiple render targets.) - WEBGL_draw_buffers.drawBuffersWEBGL(attachments); - - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - } - - resize(width, height) { - this._width = width; - this._height = height; - - gl.bindTexture(gl.TEXTURE_2D, this._depthTex); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null); - for (let i = 0; i < NUM_GBUFFERS; i++) { - gl.bindTexture(gl.TEXTURE_2D, this._gbuffers[i]); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null); - } - gl.bindTexture(gl.TEXTURE_2D, null); - } - - render(camera, scene) { - if (canvas.width != this._width || canvas.height != this._height) { - this.resize(canvas.width, canvas.height); - } - - // Update the camera matrices - camera.updateMatrixWorld(); - mat4.invert(this._viewMatrix, camera.matrixWorld.elements); - mat4.copy(this._projectionMatrix, camera.projectionMatrix.elements); - mat4.multiply(this._viewProjectionMatrix, this._projectionMatrix, this._viewMatrix); - - // Render to the whole screen - gl.viewport(0, 0, canvas.width, canvas.height); - - // Bind the framebuffer - gl.bindFramebuffer(gl.FRAMEBUFFER, this._fbo); - - // Clear the frame - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - // Use the shader program to copy to the draw buffers - gl.useProgram(this._progCopy.glShaderProgram); - - // Upload the camera matrix - gl.uniformMatrix4fv(this._progCopy.u_viewProjectionMatrix, false, this._viewProjectionMatrix); - - // 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); - - // Update the buffer used to populate the texture packed with light data - for (let i = 0; i < NUM_LIGHTS; ++i) { - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 0] = scene.lights[i].position[0]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 1] = scene.lights[i].position[1]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 2] = scene.lights[i].position[2]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 3] = scene.lights[i].radius; - - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 0] = scene.lights[i].color[0]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 1] = scene.lights[i].color[1]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 2] = scene.lights[i].color[2]; + resize(width, height) { + this._width = width; + this._height = height; + + gl.bindTexture(gl.TEXTURE_2D, this._depthTex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null); + for (let i = 0; i < NUM_GBUFFERS; i++) { + gl.bindTexture(gl.TEXTURE_2D, this._gbuffers[i]); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.FLOAT, null); + } + gl.bindTexture(gl.TEXTURE_2D, null); } - // Update the light texture - this._lightTexture.update(); - - // Update the clusters for the frame - this.updateClusters(camera, this._viewMatrix, scene); - // Bind the default null framebuffer which is the screen - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - - // Clear the frame - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - // Use this shader program - gl.useProgram(this._progShade.glShaderProgram); - - // TODO: Bind any other shader inputs - - // 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++) { - gl.activeTexture(gl[`TEXTURE${i + firstGBufferBinding}`]); - gl.bindTexture(gl.TEXTURE_2D, this._gbuffers[i]); - gl.uniform1i(this._progShade[`u_gbuffers[${i}]`], i + firstGBufferBinding); + render(camera, scene) { + if (canvas.width != this._width || canvas.height != this._height) { + this.resize(canvas.width, canvas.height); + } + + // Update the camera matrices + camera.updateMatrixWorld(); + mat4.invert(this._viewMatrix, camera.matrixWorld.elements); + mat4.copy(this._projectionMatrix, camera.projectionMatrix.elements); + mat4.multiply(this._viewProjectionMatrix, this._projectionMatrix, this._viewMatrix); + + // Render to the whole screen + gl.viewport(0, 0, canvas.width, canvas.height); + + // Bind the framebuffer + gl.bindFramebuffer(gl.FRAMEBUFFER, this._fbo); + + // Clear the frame + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + // Use the shader program to copy to the draw buffers + gl.useProgram(this._progCopy.glShaderProgram); + + // Upload the camera matrix + gl.uniformMatrix4fv(this._progCopy.u_viewProjectionMatrix, false, this._viewProjectionMatrix); + + // 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); + + // Update the buffer used to populate the texture packed with light data + for (let i = 0; i < NUM_LIGHTS; ++i) { + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 0] = scene.lights[i].position[0]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 1] = scene.lights[i].position[1]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 2] = scene.lights[i].position[2]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 3] = scene.lights[i].radius; + + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 0] = scene.lights[i].color[0]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 1] = scene.lights[i].color[1]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 2] = scene.lights[i].color[2]; + } + // Update the light texture + this._lightTexture.update(); + + // Update the clusters for the frame + this.updateClusters(camera, this._viewMatrix, scene); + + // Bind the default null framebuffer which is the screen + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + // Clear the frame + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + // Use this shader program + gl.useProgram(this._progShade.glShaderProgram); + + // Bind camera & canvas stuff + gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix); + gl.uniform1f(this._progShade.u_nearClip, camera.near); + gl.uniform1f(this._progShade.u_farClip, camera.far); + gl.uniform1f(this._progShade.u_width, canvas.width); + gl.uniform1f(this._progShade.u_height, canvas.height); + + // Set the light texture as a uniform input to the shader + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture); + gl.uniform1i(this._progShade.u_lightbuffer, 0); + + // Set the cluster texture as a uniform input to the shader + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture); + gl.uniform1i(this._progShade.u_clusterbuffer, 1); + + // Bind g-buffers + const firstGBufferBinding = 2; // You may have to change this if you use other texture slots + for (let i = 0; i < NUM_GBUFFERS; i++) { + gl.activeTexture(gl[`TEXTURE${i + firstGBufferBinding}`]); + gl.bindTexture(gl.TEXTURE_2D, this._gbuffers[i]); + gl.uniform1i(this._progShade[`u_gbuffers[${i}]`], i + firstGBufferBinding); + } + + renderFullscreenQuad(this._progShade); } - - renderFullscreenQuad(this._progShade); - } }; diff --git a/src/renderers/forwardPlus.js b/src/renderers/forwardPlus.js index a02649c..309cad9 100755 --- a/src/renderers/forwardPlus.js +++ b/src/renderers/forwardPlus.js @@ -1,83 +1,91 @@ -import { gl } from '../init'; -import { mat4, vec4, vec3 } from 'gl-matrix'; -import { loadShaderProgram } from '../utils'; -import { NUM_LIGHTS } from '../scene'; +import {gl} from '../init'; +import {mat4, vec4, vec3} from 'gl-matrix'; +import {loadShaderProgram} from '../utils'; +import {NUM_LIGHTS} from '../scene'; import vsSource from '../shaders/forwardPlus.vert.glsl'; import fsSource from '../shaders/forwardPlus.frag.glsl.js'; import TextureBuffer from './textureBuffer'; -import BaseRenderer from './base'; +import BaseRenderer, {MAX_LIGHTS_PER_CLUSTER} from './base'; export default class ForwardPlusRenderer extends BaseRenderer { - constructor(xSlices, ySlices, zSlices) { - super(xSlices, ySlices, zSlices); - - // Create a texture to store light data - this._lightTexture = new TextureBuffer(NUM_LIGHTS, 8); - - this._shaderProgram = loadShaderProgram(vsSource, fsSource({ - numLights: NUM_LIGHTS, - }), { - uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer'], - attribs: ['a_position', 'a_normal', 'a_uv'], - }); - - this._projectionMatrix = mat4.create(); - this._viewMatrix = mat4.create(); - this._viewProjectionMatrix = mat4.create(); - } - - render(camera, scene) { - // Update the camera matrices - camera.updateMatrixWorld(); - mat4.invert(this._viewMatrix, camera.matrixWorld.elements); - mat4.copy(this._projectionMatrix, camera.projectionMatrix.elements); - 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); - - // Update the buffer used to populate the texture packed with light data - for (let i = 0; i < NUM_LIGHTS; ++i) { - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 0] = scene.lights[i].position[0]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 1] = scene.lights[i].position[1]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 2] = scene.lights[i].position[2]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 3] = scene.lights[i].radius; - - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 0] = scene.lights[i].color[0]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 1] = scene.lights[i].color[1]; - this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 2] = scene.lights[i].color[2]; + constructor(xSlices, ySlices, zSlices) { + super(xSlices, ySlices, zSlices); + + // Create a texture to store light data + this._lightTexture = new TextureBuffer(NUM_LIGHTS, 8); + + this._shaderProgram = loadShaderProgram(vsSource, fsSource({ + numLights: NUM_LIGHTS, xSlices: xSlices, ySlices: ySlices, zSlices: zSlices, + maxLights: MAX_LIGHTS_PER_CLUSTER, + }), { + uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer', + 'u_viewMat', 'u_nearClip', 'u_farClip', 'u_width', 'u_height', 'u_cameraPos'], + attribs: ['a_position', 'a_normal', 'a_uv'], + }); + + this._projectionMatrix = mat4.create(); + this._viewMatrix = mat4.create(); + this._viewProjectionMatrix = mat4.create(); } - // Update the light texture - this._lightTexture.update(); - // Bind the default null framebuffer which is the screen - gl.bindFramebuffer(gl.FRAMEBUFFER, null); + render(camera, scene) { + // Update the camera matrices + camera.updateMatrixWorld(); + mat4.invert(this._viewMatrix, camera.matrixWorld.elements); + mat4.copy(this._projectionMatrix, camera.projectionMatrix.elements); + 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); - // Render to the whole screen - gl.viewport(0, 0, canvas.width, canvas.height); + // Update the buffer used to populate the texture packed with light data + for (let i = 0; i < NUM_LIGHTS; ++i) { + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 0] = scene.lights[i].position[0]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 1] = scene.lights[i].position[1]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 2] = scene.lights[i].position[2]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 3] = scene.lights[i].radius; - // Clear the frame - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 0] = scene.lights[i].color[0]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 1] = scene.lights[i].color[1]; + this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 2] = scene.lights[i].color[2]; + } + // Update the light texture + this._lightTexture.update(); - // Use this shader program - gl.useProgram(this._shaderProgram.glShaderProgram); + // Bind the default null framebuffer which is the screen + gl.bindFramebuffer(gl.FRAMEBUFFER, null); - // Upload the camera matrix - gl.uniformMatrix4fv(this._shaderProgram.u_viewProjectionMatrix, false, this._viewProjectionMatrix); + // Render to the whole screen + gl.viewport(0, 0, canvas.width, canvas.height); - // Set the light texture as a uniform input to the shader - gl.activeTexture(gl.TEXTURE2); - gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture); - gl.uniform1i(this._shaderProgram.u_lightbuffer, 2); + // Clear the frame + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - // Set the cluster texture as a uniform input to the shader - gl.activeTexture(gl.TEXTURE3); - gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture); - gl.uniform1i(this._shaderProgram.u_clusterbuffer, 3); + // Use this shader program + gl.useProgram(this._shaderProgram.glShaderProgram); - // TODO: Bind any other shader inputs + // Upload the camera matrix + gl.uniformMatrix4fv(this._shaderProgram.u_viewProjectionMatrix, false, this._viewProjectionMatrix); - // 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); - } + // Set the light texture as a uniform input to the shader + gl.activeTexture(gl.TEXTURE2); + gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture); + gl.uniform1i(this._shaderProgram.u_lightbuffer, 2); + + // Set the cluster texture as a uniform input to the shader + gl.activeTexture(gl.TEXTURE3); + gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture); + gl.uniform1i(this._shaderProgram.u_clusterbuffer, 3); + + // Bind camera & canvas stuff + gl.uniformMatrix4fv(this._shaderProgram.u_viewMatrix, false, this._viewMatrix); + gl.uniform1f(this._shaderProgram.u_nearClip, camera.near); + gl.uniform1f(this._shaderProgram.u_farClip, camera.far); + gl.uniform1f(this._shaderProgram.u_width, canvas.width); + gl.uniform1f(this._shaderProgram.u_height, canvas.height); + gl.uniform3f(this._shaderProgram.u_cameraPos, 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/shaders/deferred.frag.glsl.js b/src/shaders/deferred.frag.glsl.js index 50f1e75..e047ee1 100644 --- a/src/shaders/deferred.frag.glsl.js +++ b/src/shaders/deferred.frag.glsl.js @@ -1,20 +1,134 @@ -export default function(params) { - return ` +export default function (params) { + return ` #version 100 precision highp float; + varying vec2 v_uv; + uniform sampler2D u_gbuffers[${params.numGBuffers}]; + uniform sampler2D u_lightbuffer; - varying vec2 v_uv; + // Camera uniforms + uniform mat4 u_viewMat; + uniform float u_nearClip; + uniform float u_farClip; + + // Screen unifroms + uniform float u_width; + uniform float u_height; + + uniform sampler2D u_clusterbuffer; + 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]; + } + } + + // Unpack number of lights for a specific cluster + int UnpackNbLights(int clusterIdx, float u) { + return int(texture2D(u_clusterbuffer, vec2(u, 0.0)).x); + } + + 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; + } + + // 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 - // 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); + vec4 fullPos = texture2D(u_gbuffers[0], v_uv); + vec4 fullCol = texture2D(u_gbuffers[1], v_uv); + vec3 albedo = fullCol.rgb; + vec3 v_position = fullPos.xyz; + float normZ = sqrt(1.0 - pow(fullPos.w, 2.0) - pow(fullCol.w, 2.0)); + + // restore sign of norm z + if (albedo.r < 0.0) { + albedo.r = -albedo.r; + normZ *= -1.0; + } + vec3 normal = vec3(fullPos.w, fullCol.w, normZ); + + vec3 fragColor = vec3(0.0); + + // Cluster identification + vec4 camSpacePos = u_viewMat * vec4(v_position, 1.0); + int clusterX = int(gl_FragCoord.x / u_width * float(${params.xSlices})); + int clusterY = int(gl_FragCoord.y / u_height * float(${params.ySlices})); + int clusterZ = int((-camSpacePos.z - u_nearClip) / (u_farClip - u_nearClip) * float(${params.zSlices})); + ivec3 cluster = ivec3(clusterX, clusterY, clusterZ); + + // Get indexing info + int clusterIdx = cluster.x + cluster.y * ${params.xSlices} + cluster.z * ${params.xSlices} * ${params.ySlices}; + int clusterTexWidth = ${params.xSlices} * ${params.ySlices} * ${params.zSlices}; + int clusterTexHeight = int(float(${params.maxLights} + 1) / 4.0) + 1; + + // Get # Lights for this texture + int nbLights = int(ExtractFloat(u_clusterbuffer, clusterTexWidth, clusterTexHeight, clusterIdx, 0)); + + // Iterate over all lights because of loop unrolling + for (int i = 0; i < ${params.numLights}; i++) { + if (i >= nbLights) { + break; + } + + // get the wanted light idx by extracting from texture + int lightIdx = int(ExtractFloat(u_clusterbuffer, clusterTexWidth, clusterTexHeight, clusterIdx, i + 1)); + + // Shade the object with the light + 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); + + fragColor += albedo * 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..7d2caaa 100644 --- a/src/shaders/deferredToTexture.frag.glsl +++ b/src/shaders/deferredToTexture.frag.glsl @@ -21,9 +21,17 @@ void main() { vec3 norm = applyNormalMap(v_normal, vec3(texture2D(u_normap, v_uv))); 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] = ?? + // normals are packed within color & position's w value. Decode by doing: + // sqrt norm.z = 1 - pos.w squared - col.w squared + norm = normalize(norm); + // pack the sign of norm z into col.r + if (norm.z < 0.0) { + if (col.r == 0.0) { + col.r = 0.001; + } + col.r = -col.r; + } + + gl_FragData[0] = vec4(v_position, norm.x); + gl_FragData[1] = vec4(col, norm.y); } \ No newline at end of file diff --git a/src/shaders/forward.frag.glsl.js b/src/shaders/forward.frag.glsl.js index 47f40a1..b4a5be6 100644 --- a/src/shaders/forward.frag.glsl.js +++ b/src/shaders/forward.frag.glsl.js @@ -27,7 +27,9 @@ export default function(params) { float ExtractFloat(sampler2D texture, int textureWidth, int textureHeight, int index, int component) { float u = float(index + 1) / float(textureWidth + 1); - int pixel = component / 4; + int pixel = component / 4; // pixel is 0 or 1 + + // in our example, pixel will always be 0 or 1. v will therefore be: 1/3 = 0, or 2/3 = 0 float v = float(pixel + 1) / float(textureHeight + 1); vec4 texel = texture2D(texture, vec2(u, v)); int pixelComponent = component - pixel * 4; @@ -52,6 +54,8 @@ export default function(params) { // 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 + + // extract float: given a light buffer, the width, the height, the ith light idx, and the component to extract light.radius = ExtractFloat(u_lightbuffer, ${params.numLights}, 2, index, 3); light.color = v2.rgb; diff --git a/src/shaders/forwardPlus.frag.glsl.js b/src/shaders/forwardPlus.frag.glsl.js index 022fda7..782caab 100644 --- a/src/shaders/forwardPlus.frag.glsl.js +++ b/src/shaders/forwardPlus.frag.glsl.js @@ -1,5 +1,5 @@ -export default function(params) { - return ` +export default function (params) { + return ` // TODO: This is pretty much just a clone of forward.frag.glsl.js #version 100 @@ -8,6 +8,16 @@ export default function(params) { uniform sampler2D u_colmap; uniform sampler2D u_normap; uniform sampler2D u_lightbuffer; + + // Camera uniforms + uniform mat4 u_viewMat; + uniform float u_nearClip; + uniform float u_farClip; + uniform vec3 u_cameraPos; + + // Screen uniforms + uniform float u_width; + uniform float u_height; // TODO: Read this buffer to determine the lights influencing a cluster uniform sampler2D u_clusterbuffer; @@ -46,6 +56,11 @@ export default function(params) { return texel[3]; } } + + // Unpack number of lights for a specific cluster + int UnpackNbLights(int clusterIdx, float u) { + return int(texture2D(u_clusterbuffer, vec2(u, 0.0)).x); + } Light UnpackLight(int index) { Light light; @@ -80,21 +95,53 @@ export default function(params) { vec3 normal = applyNormalMap(v_normal, normap); vec3 fragColor = vec3(0.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 lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius); - float lambertTerm = max(dot(L, normal), 0.0); - - fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity); + + // Cluster identification + vec4 camSpacePos = u_viewMat * vec4(v_position, 1.0); + int clusterX = int(gl_FragCoord.x / u_width * float(${params.xSlices})); + int clusterY = int(gl_FragCoord.y / u_height * float(${params.ySlices})); + int clusterZ = int((-camSpacePos.z - u_nearClip) / (u_farClip - u_nearClip) * float(${params.zSlices})); + ivec3 cluster = ivec3(clusterX, clusterY, clusterZ); + + // Get indexing info + int clusterIdx = cluster.x + cluster.y * ${params.xSlices} + cluster.z * ${params.xSlices} * ${params.ySlices}; + int clusterTexWidth = ${params.xSlices} * ${params.ySlices} * ${params.zSlices}; + int clusterTexHeight = int(float(${params.maxLights} + 1) / 4.0) + 1; + + // Get # Lights for this texture + int nbLights = int(ExtractFloat(u_clusterbuffer, clusterTexWidth, clusterTexHeight, clusterIdx, 0)); + + // Iterate over all lights because of loop unrolling + for (int i = 0; i < ${params.numLights}; i++) { + if (i >= nbLights) { + break; + } + + // get the wanted light idx by extracting from texture + int lightIdx = int(ExtractFloat(u_clusterbuffer, clusterTexWidth, clusterTexHeight, clusterIdx, i + 1)); + + // Shade the object with the light + 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); + + vec3 viewDir = normalize(u_cameraPos - v_position); + vec3 halfDir = normalize(L + viewDir); + float angle = max(dot(halfDir, normal), 0.0); + float specExp = 500.0; + float spec = pow(angle, specExp); + albedo += spec; + + fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity); } const vec3 ambientLight = vec3(0.025); fragColor += albedo * ambientLight; + float depth = (gl_FragCoord.z - 0.9) * 5.0; gl_FragColor = vec4(fragColor, 1.0); } `;