Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions desktop.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[LocalizedFileNames]
liveVideo.mp4=@liveVideo,0
2 changes: 2 additions & 0 deletions img/desktop.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[LocalizedFileNames]
liveVideo.mp4=@liveVideo,0
Binary file added img/live.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/liveVideo.mp4
Binary file not shown.
Binary file added img/numGBuffersGraph.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/numLightsGraph.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/resolutionGraph.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/videoScreenshot.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/init.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
133 changes: 131 additions & 2 deletions src/renderers/clustered.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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...

Expand All @@ -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();
}
}
38 changes: 34 additions & 4 deletions src/renderers/clusteredDeferred.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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]);
Expand Down
Loading