diff --git a/README.md b/README.md index a903608..17f0e8d 100644 --- a/README.md +++ b/README.md @@ -3,26 +3,65 @@ 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) +* Ricky Rajani +* Tested on: **Google Chrome 62.0.3202** on + Windows 10, i5-6200U @ 2.30GHz, Intel(R) HD Graphics 520 4173MB (Personal Computer) + +This project implements Clustered Deferred and Forward+ Shading using WebGL. ### Live Online -[![](img/thumb.png)](http://TODO.github.io/Project5B-WebGL-Deferred-Shading) +- Num of Lights: 1500 +- Light Radius: 3.0 + +![](img/live.PNG) ### Demo Video/GIF -[![](img/video.png)](TODO) +[![Foo](img/videoScreenshot.PNG)](https://www.youtube.com/watch?v=vU8VylBNE9A&feature=youtu.be) + +### Features +- Clustered Forward+ +- Clustered Deferred +- Blinn-Phong shading +- Optimizations of g-buffers + +This project has implementations for three rendering methods for performance comparison reasons. +- Forward shading: Loop over all the lights in the scene for each geometry. +- Clustered shading: Divide the camera frustrum into 16 x 16 x 16 clusters. For shading, each cluster is assigned lights that affect the cluster. This provides for better worse case performance with large depth discontinuities. +- Forward+ shading: Forward shading with light culling for screen-space tiles. +- Deferred shading: Consists of two passes: G-buffer pass and lighting pass. All the shading occurs during the lighting pass using clustered shading. The primary advantage of deferred shading is the decoupling of scene geometry from lighting. Only one geometry pass is required and each light is only computed for those pixels that it actually affects. This gives the ability to render many lights in a scene without a significant performance-hit. + +### Performance Analysis + +Testing number of lights + - Light's radius : 3.0 + - Resolution : 1920 x 1080 + - Cluster Dimension : 16 x 16 x 16 + +![](img/numLightsGraph.PNG) + +Deferred shading is better for large number of lights. Deferred shading grabs its shading informationg from the closest fragment (from g-buffers), it is faster than Forward+. This advantage comes from deferred only having to shade one fragment per pixel rather than all the fragments associated to each pixel. + +While forward plus takes more time to do light calculation of geometry vertices that do not contribute to final rendering result, clustered deferred shading avoids the problem by first pass. -### (TODO: Your README) +Testing resolution + - Number of Lights : 1000 + - Light's radius : 3.0 + - Cluster Dimension : 16 x 16 x 16 -*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/resolutionGraph.PNG) -This assignment has a considerable amount of performance analysis compared -to implementation work. Complete the implementation early to leave time! +Deferred shading's performance depends more on screen resolution than scene complexity. Here you can see that its efficiency increases as the resolution increases in a scene with 1000 lights. + +Optimized g-buffer format: + - Used two rather than four g-buffers + - Use 2-component normals + - Reduce number of properties passed via g-buffer by reconstructing world space position + [![](img/numGBuffersGraph.PNG)] + +There isn't a significant improvement in performance when reducing the number of g-buffers used. The time it takes to grab the appropriate texels from the g-buffers is similar to the time of reconstructing world space position. However, there is clearly an improvement in memory allocation due to a decrease in the amount of g-buffers used. ### Credits diff --git a/desktop.ini b/desktop.ini new file mode 100644 index 0000000..abe0a04 --- /dev/null +++ b/desktop.ini @@ -0,0 +1,2 @@ +[LocalizedFileNames] +liveVideo.mp4=@liveVideo,0 diff --git a/img/desktop.ini b/img/desktop.ini new file mode 100644 index 0000000..abe0a04 --- /dev/null +++ b/img/desktop.ini @@ -0,0 +1,2 @@ +[LocalizedFileNames] +liveVideo.mp4=@liveVideo,0 diff --git a/img/live.PNG b/img/live.PNG new file mode 100644 index 0000000..ba201d3 Binary files /dev/null and b/img/live.PNG differ diff --git a/img/liveVideo.mp4 b/img/liveVideo.mp4 new file mode 100644 index 0000000..121e6b0 Binary files /dev/null and b/img/liveVideo.mp4 differ diff --git a/img/numGBuffersGraph.PNG b/img/numGBuffersGraph.PNG new file mode 100644 index 0000000..c55a4c4 Binary files /dev/null and b/img/numGBuffersGraph.PNG differ diff --git a/img/numLightsGraph.PNG b/img/numLightsGraph.PNG new file mode 100644 index 0000000..175c7d6 Binary files /dev/null and b/img/numLightsGraph.PNG differ diff --git a/img/resolutionGraph.PNG b/img/resolutionGraph.PNG new file mode 100644 index 0000000..5c79185 Binary files /dev/null and b/img/resolutionGraph.PNG differ diff --git a/img/videoScreenshot.PNG b/img/videoScreenshot.PNG new file mode 100644 index 0000000..4a2f55b Binary files /dev/null and b/img/videoScreenshot.PNG differ diff --git a/src/init.js b/src/init.js index 1b09377..f2ae14f 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'; @@ -41,6 +41,8 @@ for (let i = 0; i < requiredExtensions.length; ++i) { } } +gl.enable(gl.CULL_FACE); + // Get the maximum number of draw buffers gl.getExtension('OES_texture_float'); gl.getExtension('OES_texture_float_linear'); diff --git a/src/main.js b/src/main.js index 1cbbf9a..0e9f471 100644 --- a/src/main.js +++ b/src/main.js @@ -21,10 +21,10 @@ function setRenderer(renderer) { params._renderer = new ForwardRenderer(); break; case CLUSTERED_FORWARD_PLUS: - params._renderer = new ClusteredForwardPlusRenderer(15, 15, 15); + params._renderer = new ClusteredForwardPlusRenderer(16, 16, 16); break; case CLUSTERED_DEFFERED: - params._renderer = new ClusteredDeferredRenderer(15, 15, 15); + params._renderer = new ClusteredDeferredRenderer(16, 16, 16); break; } } diff --git a/src/renderers/clustered.js b/src/renderers/clustered.js index 9521fbd..ee99757 100644 --- a/src/renderers/clustered.js +++ b/src/renderers/clustered.js @@ -2,7 +2,35 @@ 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 = 2500; + +// Returns distance between light and X plane +function distanceToXPlane(lightPos, width) +{ + var x = lightPos[0]; + var z = lightPos[2]; + return (x - width * z) / Math.sqrt(width * width + 1.0); +} + +// Returns distance between light and Y plane +function distanceToYPlane(lightPos, height) +{ + var y = lightPos[1]; + var z = lightPos[2]; + return (y - height * z) / Math.sqrt(height * height + 1.0); +} + +function distanceToZPlane(z, slices, camera) +{ + if (z <= 1) { + return camera.near; + } + else + { + var n = (parseFloat(z) - 1.0) / (parseFloat(slices) - 1.0); + return Math.exp(n * Math.log(camera.far - camera.near + 1.0)) + camera.near - 1.0; + } +} export default class ClusteredRenderer { constructor(xSlices, ySlices, zSlices) { @@ -13,7 +41,8 @@ export default class ClusteredRenderer { this._zSlices = zSlices; } - updateClusters(camera, viewMatrix, scene) { + 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... @@ -26,7 +55,107 @@ export default class ClusteredRenderer { } } } + + var halfY = Math.tan(camera.fov * 0.5 * (Math.PI / 180.0)); + var yStride = (2.0 * halfY) / parseFloat(this._ySlices); + var halfX = camera.aspect * halfY; + var xStride = (2.0 * halfX) / parseFloat(this._xSlices); + + var lightPos = vec4.create(); + + // Loop through each light + for(let lightIndex = 0; lightIndex < NUM_LIGHTS; lightIndex++) + { + lightPos[0] = scene.lights[lightIndex].position[0]; + lightPos[1] = scene.lights[lightIndex].position[1]; + lightPos[2] = scene.lights[lightIndex].position[2]; + lightPos[3] = 1.0; + + // Transform light position from world space to view space + vec4.transformMat4(lightPos, lightPos, viewMatrix); + + // Make sure z is positive + lightPos[2] *= -1.0; + + let lightRadius = scene.lights[lightIndex].radius; + let minX; let minY; let minZ; + let maxX; let maxY; let maxZ; + + // AABB + for(minX = 0; minX <= this._xSlices; minX++) + { + let dist = distanceToXPlane(lightPos, xStride * (minX + 1 - this._xSlices * 0.5)); + if(dist <= lightRadius) + { + break; + } + } + for(maxX = this._xSlices; maxX >= minX; maxX--) + { + let dist = distanceToXPlane(lightPos, xStride * (maxX - 1 - this._xSlices * 0.5)) + if(-dist <= lightRadius) + { + maxX--; + break; + } + } + + for(minY = 0; minY <= this._ySlices; minY++) + { + let dist = distanceToYPlane(lightPos, yStride * (minY + 1 - this._ySlices * 0.5)); + if(dist <= lightRadius) + { + break; + } + } + for(maxY = this._ySlices; maxY >= minY; maxY--) + { + let dist = distanceToYPlane(lightPos, yStride * (maxY - 1 - this._ySlices * 0.5)); + if(-dist <= lightRadius) + { + maxY--; + break; + } + } + + for(minZ = 0; minZ <= this._zSlices; minZ++) + { + let zView = distanceToZPlane(minZ + 1, this._zSlices, camera); + if(zView > (lightPos[2] - lightRadius)) + { + break; + } + } + for(maxZ = this._zSlices; maxZ >= minZ; maxZ--) + { + let zView = distanceToZPlane(maxZ - 1, this._zSlices, camera); + if(zView <= (lightPos[2] + lightRadius)) + { + maxZ += 2; + break; + } + } + + // Add light indices to corresponding clusters and update light count + for(let x = minX; x <= maxX; x++) { + for(let y = minY; y <= maxY; y++) { + for(let z = minZ; z <= maxZ; z++) { + let i = x + y * this._xSlices + z * this._xSlices * this._ySlices; + let countIndex = this._clusterTexture.bufferIndex(i, 0); + let lightCount = this._clusterTexture.buffer[countIndex] + 1; + if (lightCount < MAX_LIGHTS_PER_CLUSTER) + { + this._clusterTexture.buffer[countIndex] = lightCount; + let texel = Math.floor(lightCount / 4.0); + let r = lightCount - texel * 4.0; + let texelIndex = this._clusterTexture.bufferIndex(i, texel); + this._clusterTexture.buffer[r + texelIndex] = lightIndex; + } + } + } + } + } this._clusterTexture.update(); } } \ No newline at end of file diff --git a/src/renderers/clusteredDeferred.js b/src/renderers/clusteredDeferred.js index 5e28e84..90cb825 100644 --- a/src/renderers/clusteredDeferred.js +++ b/src/renderers/clusteredDeferred.js @@ -8,8 +8,9 @@ import QuadVertSource from '../shaders/quad.vert.glsl'; import fsSource from '../shaders/deferred.frag.glsl.js'; import TextureBuffer from './textureBuffer'; import ClusteredRenderer from './clustered'; +import { MAX_LIGHTS_PER_CLUSTER } from './clustered'; -export const NUM_GBUFFERS = 4; +export const NUM_GBUFFERS = 2; export default class ClusteredDeferredRenderer extends ClusteredRenderer { constructor(xSlices, ySlices, zSlices) { @@ -21,21 +22,27 @@ 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'], + uniforms: ['u_viewProjectionMatrix', 'u_viewMatrix', 'u_colmap', 'u_normap'], attribs: ['a_position', 'a_normal', 'a_uv'], }); this._progShade = loadShaderProgram(QuadVertSource, fsSource({ numLights: NUM_LIGHTS, numGBuffers: NUM_GBUFFERS, + num_xSlices: xSlices, + num_ySlices: ySlices, + num_zSlices: zSlices, + num_maxLightsPerCluster: MAX_LIGHTS_PER_CLUSTER, }), { - uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]'], + uniforms: ['u_viewProjectionMatrix', 'u_viewMatrix', 'u_invProjectionMatrix', 'u_invViewProjectionMatrix', 'u_depthBuffer', 'u_gbuffers[0]', 'u_gbuffers[1]', 'u_lightbuffer', 'u_clusterbuffer', 'u_screenbuffer'], attribs: ['a_uv'], }); this._projectionMatrix = mat4.create(); this._viewMatrix = mat4.create(); this._viewProjectionMatrix = mat4.create(); + this._invProjectionMatrix = mat4.create(); + this._invViewProjectionMatrix = mat4.create(); } setupDrawBuffers(width, height) { @@ -108,6 +115,8 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { mat4.invert(this._viewMatrix, camera.matrixWorld.elements); mat4.copy(this._projectionMatrix, camera.projectionMatrix.elements); mat4.multiply(this._viewProjectionMatrix, this._projectionMatrix, this._viewMatrix); + mat4.invert(this._invProjectionMatrix, this._projectionMatrix); + mat4.invert(this._invViewProjectionMatrix, this._viewProjectionMatrix); // Render to the whole screen gl.viewport(0, 0, canvas.width, canvas.height); @@ -123,6 +132,7 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { // Upload the camera matrix gl.uniformMatrix4fv(this._progCopy.u_viewProjectionMatrix, false, this._viewProjectionMatrix); + 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); @@ -154,9 +164,29 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer { gl.useProgram(this._progShade.glShaderProgram); // TODO: Bind any other shader inputs + // Light Texture + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture); + gl.uniform1i(this._progShade.u_lightbuffer, 0); + + // Cluster Texture + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture); + gl.uniform1i(this._progShade.u_clusterbuffer, 1); + + // Depth Buffer + gl.activeTexture(gl.TEXTURE2); + gl.bindTexture(gl.TEXTURE_2D, this._depthTex); + gl.uniform1i(this._progShade.u_depthBuffer, 2); + + gl.uniformMatrix4fv(this._progShade.u_viewProjectionMatrix, false, this._viewProjectionMatrix); + gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix); + gl.uniformMatrix4fv(this._progShade.u_invProjectionMatrix, false, this._invProjectionMatrix); + gl.uniformMatrix4fv(this._progShade.u_invViewProjectionMatrix, false, this._invViewProjectionMatrix); + gl.uniform4f(this._progShade.u_screenbuffer, canvas.width, canvas.height, camera.near, camera.far); // Bind g-buffers - const firstGBufferBinding = 0; // You may have to change this if you use other texture slots + const firstGBufferBinding = 3; // 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]); diff --git a/src/renderers/clusteredForwardPlus.js b/src/renderers/clusteredForwardPlus.js index 9e8afbe..88a78e1 100644 --- a/src/renderers/clusteredForwardPlus.js +++ b/src/renderers/clusteredForwardPlus.js @@ -6,6 +6,7 @@ import vsSource from '../shaders/clusteredForward.vert.glsl'; import fsSource from '../shaders/clusteredForward.frag.glsl.js'; import TextureBuffer from './textureBuffer'; import ClusteredRenderer from './clustered'; +import { MAX_LIGHTS_PER_CLUSTER } from './clustered'; export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { constructor(xSlices, ySlices, zSlices) { @@ -16,14 +17,20 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { this._shaderProgram = loadShaderProgram(vsSource, fsSource({ numLights: NUM_LIGHTS, + num_xSlices: xSlices, + num_ySlices: ySlices, + num_zSlices: zSlices, + num_maxLightsPerCluster: MAX_LIGHTS_PER_CLUSTER, }), { - uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer'], + uniforms: ['u_viewProjectionMatrix', 'u_invProjectionMatrix', 'u_invViewMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer', 'u_screenbuffer'], attribs: ['a_position', 'a_normal', 'a_uv'], }); this._projectionMatrix = mat4.create(); this._viewMatrix = mat4.create(); this._viewProjectionMatrix = mat4.create(); + this._invprojectionMatrix = mat4.create(); + this._invViewMatrix = mat4.create(); } render(camera, scene) { @@ -32,10 +39,12 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { mat4.invert(this._viewMatrix, camera.matrixWorld.elements); mat4.copy(this._projectionMatrix, camera.projectionMatrix.elements); mat4.multiply(this._viewProjectionMatrix, this._projectionMatrix, this._viewMatrix); + mat4.invert(this._invprojectionMatrix, this._projectionMatrix); + mat4.copy(this._invViewMatrix, camera.matrixWorld.elements); // 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]; @@ -76,6 +85,9 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer { gl.uniform1i(this._shaderProgram.u_clusterbuffer, 3); // TODO: Bind any other shader inputs + gl.uniformMatrix4fv(this._shaderProgram.u_invProjectionMatrix, false, this._invprojectionMatrix); + gl.uniformMatrix4fv(this._shaderProgram.u_invViewMatrix, false, this._invViewMatrix); + gl.uniform4f(this._shaderProgram.u_screenbuffer, canvas.width, canvas.height, camera.near, camera.far); // 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); diff --git a/src/scene.js b/src/scene.js index 35f6700..4c2a44a 100644 --- a/src/scene.js +++ b/src/scene.js @@ -4,11 +4,11 @@ import { gl } from './init'; // TODO: Edit if you want to change the light initial positions export const LIGHT_MIN = [-14, 0, -6]; export const LIGHT_MAX = [14, 20, 6]; -export const LIGHT_RADIUS = 5.0; +export const LIGHT_RADIUS = 3.0; export const LIGHT_DT = -0.03; // TODO: This controls the number of lights -export const NUM_LIGHTS = 100; +export const NUM_LIGHTS = 1000; class Scene { constructor() { diff --git a/src/shaders/clusteredForward.frag.glsl.js b/src/shaders/clusteredForward.frag.glsl.js index 022fda7..e2855c2 100644 --- a/src/shaders/clusteredForward.frag.glsl.js +++ b/src/shaders/clusteredForward.frag.glsl.js @@ -8,10 +8,12 @@ export default function(params) { uniform sampler2D u_colmap; uniform sampler2D u_normap; uniform sampler2D u_lightbuffer; + uniform mat4 u_invProjectionMatrix; + uniform mat4 u_invViewMatrix; + uniform vec4 u_screenbuffer; // TODO: Read this buffer to determine the lights influencing a cluster uniform sampler2D u_clusterbuffer; - varying vec3 v_position; varying vec3 v_normal; varying vec2 v_uv; @@ -19,9 +21,12 @@ export default function(params) { vec3 applyNormalMap(vec3 geomnor, vec3 normap) { normap = normap * 2.0 - 1.0; vec3 up = normalize(vec3(0.001, 1, 0.001)); - vec3 surftan = normalize(cross(geomnor, up)); + if(abs(geomnor.y) >= 0.999) { + up = vec3(1.0, 0.0, 0.0); + } + vec3 surftan = normalize(cross(up, geomnor)); vec3 surfbinor = cross(geomnor, surftan); - return normap.y * surftan + normap.x * surfbinor + normap.z * geomnor; + return normalize(surftan * normap.x + surfbinor * normap.y + geomnor * normap.z); } struct Light { @@ -44,6 +49,8 @@ export default function(params) { return texel[2]; } else if (pixelComponent == 3) { return texel[3]; + } else { + return -1.0; } } @@ -73,23 +80,84 @@ export default function(params) { return 0.0; } } - + + const int num_xSlices = ${params.num_xSlices}; + const int num_ySlices = ${params.num_ySlices}; + const int num_zSlices = ${params.num_zSlices}; + const float num_maxLightsPerClust = float(${params.num_maxLightsPerCluster}); + const int num_lights = int(min(float(${params.numLights}), num_maxLightsPerClust)); + 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); - 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); + vec4 screenToView = vec4(gl_FragCoord.xyz, 1.0); + vec4 view = u_invProjectionMatrix * screenToView; + view /= view.w; + screenToView.xy /= u_screenbuffer.xy; + + int xSlice = int(screenToView.x * float(num_xSlices)); + int ySlice = int(screenToView.y * float(num_ySlices)); + int zSlice = 0; + + float near = u_screenbuffer.z; + floar far = u_screenbuffer.w; + + if(-view.z >= near) { + float n = log(-view.z - near + 1.0) / log(far - near + 1.0); + zSlice = int(n * float(num_zSlices - 1)) + 1; + } + + int numClusters = num_xSlices * num_ySlices * num_zSlices; + int clusterIndex = xSlice + ySlice * num_xSlices + zSlice * num_xSlices * num_ySlices; + float uCoord = float(clusterIndex + 1) / float(numClusters + 1); + int lightCount = int(texture2D(u_clusterbuffer, vec2(uCoord, 0.0))[0]); + + for (int lightIndex = 1; lightIndex <= num_lights; lightIndex++) { + if (lightCount < lightIndex) { + break; + } + + int texelIndex = lightIndex / 4; + float vCoord = float(texelIndex + 1) / ceil(float(${params.num_maxLightsPerCluster} + 1) / 4.0 + 1.0); + vec4 texel = texture2D(u_clusterbuffer, vec2(uCoord, vCoord)); + int r = lightIndex - 4 * texelIndex; + int index; + + if (r == 0) { + index = int(texel[0]); + } + else if (r == 1) { + index = int(texel[1]); + } + else if (r == 2) { + index = int(texel[2]); + } + else if (r == 3) { + index = int(texel[3]); + } + else { + continue; + } + + // Blinn-Phong shading (diffuse + specular) + Light currLight = UnpackLight(index); + float lightDistance = distance(currLight.position, v_position); + vec3 L = (currLight.position - v_position) / lightDistance; + + float lightIntensity = cubicGaussian(2.0 * lightDistance / currLight.radius); + float NdotL = max(dot(L, normal), 0.0); + + vec4 cameraWorldPos = u_invViewMatrix * vec4(0.0, 0.0, 0.0, 1.0); + vec3 V = normalize(cameraWorldPos.xyz - v_position); + vec3 H = normalize(L + V); + float NdotH = max(dot(H, normal), 0.0); + float specular = pow(NdotH, 100.0); + + fragColor += (albedo + vec3(specular)) * NdotL * lightIntensity * currLight.color; } const vec3 ambientLight = vec3(0.025); diff --git a/src/shaders/deferred.frag.glsl.js b/src/shaders/deferred.frag.glsl.js index 50f1e75..46abb1f 100644 --- a/src/shaders/deferred.frag.glsl.js +++ b/src/shaders/deferred.frag.glsl.js @@ -5,16 +5,163 @@ export default function(params) { uniform sampler2D u_gbuffers[${params.numGBuffers}]; + uniform sampler2D u_lightbuffer; + uniform sampler2D u_clusterbuffer; + uniform sampler2D u_depthBuffer; + uniform vec4 u_screenbuffer; + + uniform mat4 u_viewProjectionMatrix; + uniform mat4 u_viewMatrix; + uniform mat4 u_invProjectionMatrix; + uniform mat4 u_invViewProjectionMatrix; + varying vec2 v_uv; + 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]; + } else { + return -1.0; + } + } + + 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; + } + } + + const int num_xSlices = ${params.num_xSlices}; + const int num_ySlices = ${params.num_ySlices}; + const int num_zSlices = ${params.num_zSlices}; + const float num_maxLightsPerClust = float(${params.num_maxLightsPerCluster}); + const int num_lights = int(min(float(${params.numLights}), num_maxLightsPerClust)); + 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 albedo = texture2D(u_gbuffers[0], v_uv); // r : albedo.r g : albedo.g b : albedo.b a : depth + vec4 normal = texture2D(u_gbuffers[1], v_uv); // r : normal.x g : normal.y b : empty a : empty + + // Reconstructing world space position + float depthMap = texture2D(u_depthBuffer, v_uv).x; + vec4 screenPos; + if(depthMap == 1.0) { + screenPos = vec4(v_uv * 2.0 - vec2(1.0), depthMap, 1.0); + } + else { + screenPos = vec4(v_uv * 2.0 - vec2(1.0), albedo.w, 1.0); + } + vec4 worldSpacePos = u_invViewProjectionMatrix * screenPos; + worldSpacePos /= worldSpacePos.w; + normal.z = sqrt(1.0 - (normal.x * normal.x + normal.y * normal.y)); + vec4 view = u_viewMatrix * worldSpacePos; + + vec3 fragColor = vec3(0.0); + + int xSlice = int(v_uv.x * float(num_xSlices)); + int ySlice = int(v_uv.y * float(num_ySlices)); + int zSlice = 0; + + float near = u_screenbuffer.z; + float far = u_screenbuffer.w; + + if(-view.z >= near) { + float n = log(-view.z - near + 1.0) / log(far - near + 1.0); + zSlice = int(n * float(num_zSlices - 1)) + 1; + } + + int numClusters = num_xSlices * num_ySlices * num_zSlices; + int clusterIndex = xSlice + ySlice * num_xSlices + zSlice * num_xSlices * num_ySlices; + float uCoord = float(clusterIndex + 1) / float(numClusters + 1); + int lightCount = int(texture2D(u_clusterbuffer, vec2(uCoord, 0.0))[0]); + + for (int lightIndex = 1; lightIndex <= num_lights; lightIndex++) { + if (lightCount < lightIndex) { + break; + } + + int texelIndex = lightIndex / 4; + float vCoord = float(texelIndex + 1) / ceil(float(${params.num_maxLightsPerCluster} + 1) / 4.0 + 1.0); + vec4 texel = texture2D(u_clusterbuffer, vec2(uCoord, vCoord)); + int r = lightIndex - 4 * texelIndex; + int index; + + if (r == 0) { + index = int(texel[0]); + } + else if (r == 1) { + index = int(texel[1]); + } + else if (r == 2) { + index = int(texel[2]); + } + else if (r == 3) { + index = int(texel[3]); + } + else { + continue; + } + + // Blinn-Phong shading (diffuse + specular) + Light currLight = UnpackLight(index); + float lightDistance = distance(currLight.position, worldSpacePos.xyz); + vec3 L = (currLight.position - worldSpacePos.xyz) / lightDistance; + L = vec3(u_viewMatrix * vec4(L, 0.0)); + + float lightIntensity = cubicGaussian(2.0 * lightDistance / currLight.radius); + float NdotL = max(dot(L, normal.xyz), 0.0); + + vec4 viewSpacePos = u_viewMatrix * (vec4(worldSpacePos.xyz, 1.0)); + vec3 V = -normalize(vec3(viewSpacePos)); + vec3 H = normalize(L + V); + float NdotH = max(dot(H, normal.xyz), 0.0); + float specular = pow(NdotH, 100.0); + + fragColor += (albedo.xyz + vec3(specular)) * NdotL * lightIntensity * currLight.color; + } + + const vec3 ambientLight = vec3(0.025); + fragColor += albedo.xyz * 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..3f15775 100644 --- a/src/shaders/deferredToTexture.frag.glsl +++ b/src/shaders/deferredToTexture.frag.glsl @@ -9,21 +9,29 @@ varying vec3 v_position; varying vec3 v_normal; varying vec2 v_uv; +uniform mat4 u_viewMatrix; +uniform mat4 u_viewProjectionMatrix; + vec3 applyNormalMap(vec3 geomnor, vec3 normap) { normap = normap * 2.0 - 1.0; vec3 up = normalize(vec3(0.001, 1, 0.001)); - vec3 surftan = normalize(cross(geomnor, up)); + if(abs(geomnor.y) >= 0.999) { + up = vec3(1.0, 0.0, 0.0); + } + vec3 surftan = normalize(cross(up, geomnor)); vec3 surfbinor = cross(geomnor, surftan); - return normap.y * surftan + normap.x * surfbinor + normap.z * geomnor; + return normalize(surftan * normap.x + surfbinor * normap.y + geomnor * normap.z); } void main() { - vec3 norm = applyNormalMap(v_normal, vec3(texture2D(u_normap, v_uv))); + vec3 norm = vec3(u_viewMatrix * vec4(applyNormalMap(v_normal, vec3(texture2D(u_normap, v_uv))), 0.0)); vec3 col = vec3(texture2D(u_colmap, v_uv)); + + vec4 view = u_viewProjectionMatrix * vec4(v_position, 1.0); + view /= view.w; + float depth = view.z; // 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(col, depth); + gl_FragData[1] = vec4(norm.x, norm.y, 0.0, v_position.z); +} diff --git a/src/shaders/forward.frag.glsl.js b/src/shaders/forward.frag.glsl.js index 47f40a1..bc0f59a 100644 --- a/src/shaders/forward.frag.glsl.js +++ b/src/shaders/forward.frag.glsl.js @@ -39,6 +39,8 @@ export default function(params) { return texel[2]; } else if (pixelComponent == 3) { return texel[3]; + } else { + return texel[0]; } } @@ -82,9 +84,9 @@ export default function(params) { vec3 L = (light.position - v_position) / lightDistance; float lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius); - float lambertTerm = max(dot(L, normal), 0.0); + float NdotL = max(dot(L, normal), 0.0); - fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity); + fragColor += albedo * NdotL * light.color * vec3(lightIntensity); } const vec3 ambientLight = vec3(0.025);