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
56 changes: 44 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,56 @@ WebGL Clustered Deferred and Forward+ Shading

**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5**

* (TODO) YOUR NAME HERE
* Tested on: (TODO) **Google Chrome 222.2** on
Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab)

* William Ho
* Tested on: **Google Chrome** on
OS X El Capitan, MacBook Pro 2013, 2.4, GHz Intel Core i5, Intel Iris 1536 MB
### Live Online

[![](img/thumb.png)](http://TODO.github.io/Project5B-WebGL-Deferred-Shading)
[![](img/thumb.jpg)](https://williamkho.github.io/Project5-WebGL-Clustered-Deferred-Forward-Plus/)

### Overview

This project consists of implementations of a **Clustered Forward+** renderer and a **Clustered Deferred** renderer, compared against a general case **Forward** renderer.

A forward renderer performs brute force lighting by shading fragments against every possible light in the scene. The unnecessary work is in the negligible effects a given light has on the majority of fragments. A solution to this is to divide our rendered area up into "clusters", where the fragments in a given cluster need only be shaded for the lights that affect that specific cluster. This involves a CPU side preprocessing of light information to map clusters to their relevant lights. This is what **Clustered Forward+** performs.

To further decrease the amount of unnecessary work done, a further solution is to defer lighting to a second stage after fragment attributes have been computed. On a first pass, the scene is processed and attributes are passed to a g-buffer, which is then passed to a second pass render (in which we can again use our clustering technique). We are able to leverage the fact that the first render pass discards all unnecessary fragments (such as those occluded). This is what **Clustered Deferred** performs.

### Comparisons of Implementation

| Forward Renderer | Clustered Forward+ | Clustered Deferred (w/ Blinn-Phong)|
|:----:|:----:|:----:|
| ![](img/forward.gif) | ![](img/clusteredforwardplus.gif) | ![](img/clustereddeferred.gif) |

### Analysis

![](img/chart1.png)

We can see here that as we increase the number of lights in the scene, our Clustered implementations are able to render at a higher frame rate than the basic Forward implementation, and our Deferred shader grants us extra wins on time. Our Clustered Forward+ manages to eliminate the unnecessary lighting work by culling redundant lights from fragments. The Deferred renderer culls unnecessary fragments from being shaded.

It should be noted that utilizing the clustering technique has other costs. Clustering the scene must be done before the scene is processed by the vertex and fragment shaders, and this CPU-side preprocessing step can have potential downsides. If for, instance, clustering did not effectively cull lights, such as in a case where relevant lights span all clusters, clustering could prove detrimental. There is also the added overhead of generating clustering data.

Deferred shading similarly has an extra step that effectively culls unnecessary fragments, but in cases where such fragments are not as common, such as scenes with little occlusion, it could be ineffective. Additionally, deferred shading requires the ability to specify multiple render targets, which is not supported in all cases. The other drawback to deferred shading is the increased bandwidth requirements of passing the g-buffer from one render pass to the next.

## Potential Improvements

There are several improvements to these implementations that are worth exploring:

* Improved cluster testing: my implementation currently calculates a conservative bounding frustrum per light to calculate its relevant clusters. It is possible that clusters could be more effectively culled from the light.

* Optimized g-buffer: better packing of data in the g-buffer reduces bandwidth requirements.

### Demo Video/GIF

[![](img/video.png)](TODO)
### Debug Views

### (TODO: Your README)
![](img/clusterDebug01.png)
Debugging view of clustering in screen space.

*DO NOT* leave the README to the last minute! It is a crucial part of the
project, and we will not be able to grade you without a good README.
![](img/clusterDebug02.png)
Debugging view to illustrate cluster plane calculation.

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
![](img/clusterDebug03.png)
Debugging view of frozen clustering frustrum to visualize fragment cluster placement.


### Credits
Expand All @@ -31,3 +62,4 @@ to implementation work. Complete the implementation early to leave time!
* [webgl-debug](https://github.com/KhronosGroup/WebGLDeveloperTools) by Khronos Group Inc.
* [glMatrix](https://github.com/toji/gl-matrix) by [@toji](https://github.com/toji) and contributors
* [minimal-gltf-loader](https://github.com/shrekshao/minimal-gltf-loader) by [@shrekshao](https://github.com/shrekshao)
* [Practical Clustered Shading](http://www.humus.name/Articles/PracticalClusteredShading.pdf) by Emil Persson, Head of Research, Avalanche Studios
Binary file added img/chart1.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/clusterDebug01.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/clusterDebug02.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/clusterDebug03.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/clustereddeferred.gif
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/clusteredforwardplus.gif
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/forward.gif
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/thumb.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 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
2 changes: 1 addition & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const CLUSTERED_FORWARD_PLUS = 'Clustered Forward+';
const CLUSTERED_DEFFERED = 'Clustered Deferred';

const params = {
renderer: CLUSTERED_FORWARD_PLUS,
renderer: CLUSTERED_DEFFERED,
_renderer: null,
};

Expand Down
179 changes: 178 additions & 1 deletion src/renderers/clustered.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mat4, vec4, vec3 } from 'gl-matrix';
import { NUM_LIGHTS } from '../scene';
import TextureBuffer from './textureBuffer';

export const MAX_LIGHTS_PER_CLUSTER = 100;
export const MAX_LIGHTS_PER_CLUSTER = 250;

export default class ClusteredRenderer {
constructor(xSlices, ySlices, zSlices) {
Expand All @@ -13,10 +13,113 @@ export default class ClusteredRenderer {
this._zSlices = zSlices;
}

//Helper function to convert degrees to radians
getTanDeg(deg) {
var rad = deg * Math.PI/180;
return Math.tan(rad);

}

getContainingZPlane(posZ) {
if (posZ > -5) {
return 0;
} else {
let logPosZ = Math.log2(Math.abs(posZ) - 5.0);
if (logPosZ < 0.0) return 1;
return Math.floor(logPosZ) + 1.0;
}
}
/*
Options parameter includes:
stride : distance between clusters
strideMultiple : how many strides to take for the given plane
axis : which axis of cluster we are using
output : output vec3
*/
calculatePlaneNormal(options) {
if (options.axis === 'horizontal') {
let d = options.stride * options.strideMultiple;
let x = 1 / Math.sqrt(1 + d * d);
let z = d / Math.sqrt(1 + d * d);
options.output.x = x;
options.output.y = 0;
options.output.z = z;
} else {
let d = options.stride * options.strideMultiple;
let y = 1 / Math.sqrt(1 + d * d);
let z = d / Math.sqrt(1 + d * d);
options.output.x = 0;
options.output.y = y;
options.output.z = z;
}
}

//Take in light position in view space FOR
intersectsPlane(planeNormal, planeOrigin, position, radius) {
let relativePosition = vec3.create();
vec3.subtract(relativePosition, position, planeOrigin);
return radius > Math.abs(vec3.dot(planeNormal, relativePosition));
}

clamp(a, b, c) {
return Math.max(b, Math.min(c, a));
}

//Calculate the normalized frustrum space coordinates given a position
getClusterUVD(options) {
let absZ = Math.abs(options.position[2]);
let radius = options.radius;

let height = options.fovTan * absZ * 2;
let pHeight = (options.position[1] + height / 2);

let pv1 = (pHeight - radius) / height;
let pv2 = (pHeight + radius) / height;

let width = height * options.aspect;
let pWidth = (options.position[0] + width / 2);

let pu1 = (pWidth - radius) / width;
let pu2 = (pWidth + radius) / width;

let pd1 = ((absZ - radius) - options.near) / (options.far - options.near);
let pd2 = ((absZ + radius) - options.near) / (options.far - options.near);

return {
x1 : pu1,
y1 : pv1,
z1 : pd1,
x2 : pu2,
y2 : pv2,
z2 : pd2
};

//vec3.set(options.output, pu, pv, pd);
}

updateClusters(camera, viewMatrix, scene) {
// TODO: Update the cluster texture with the count and indices of the lights in each cluster
// This will take some time. The math is nontrivial...

var fovTan = this.getTanDeg(camera.fov / 2);
var minClusterUVDScratch = vec3.create();
var maxClusterUVDScratch = vec3.create();


//scratch variables for normals
/*
var origin = vec3.create();
var z1OriginScratch = vec3.create();
var z2OriginScratch = vec3.create();
var x1NormalScratch = vec3.create();
var x2NormalScratch = vec3.create();
var y1NormalScratch = vec3.create();
var y2NormalScratch = vec3.create();
var zNormal = vec3.create();
zNormal.z = 1;
*/
var lightPositionScratch = vec3.create();

for (let z = 0; z < this._zSlices; ++z) {
for (let y = 0; y < this._ySlices; ++y) {
for (let x = 0; x < this._xSlices; ++x) {
Expand All @@ -27,6 +130,80 @@ export default class ClusteredRenderer {
}
}

for (let l = 0; l < scene.lights.length; ++l) {
let light = scene.lights[l];
let radius = light.radius * 1.4;
let outOfView = false;
vec3.copy(lightPositionScratch, light.position);
vec3.transformMat4(lightPositionScratch, lightPositionScratch, viewMatrix);

let boundingBox = this.getClusterUVD({
radius : radius,
position : lightPositionScratch,
fovTan : fovTan,
aspect : camera.aspect,
near : camera.near,
far : camera.far
});

if (boundingBox.x1 > 0.999 && boundingBox.x2 > 0.999) {
outOfView = true;
} else if (boundingBox.y1 > 0.999 && boundingBox.y2 > 0.999) {
outOfView = true;
} else if (boundingBox.z1 > 0.999 && boundingBox.z2 > 0.999) {
outOfView = true;
} else if (boundingBox.x1 < 0 && boundingBox.x2 < 0) {
outOfView = true;
} else if (boundingBox.y1 < 0 && boundingBox.y2 < 0) {
outOfView = true;
} else if (boundingBox.z1 < 0 && boundingBox.z2 < 0) {
outOfView = true;
}

if (!outOfView) {
let pu1 = this.clamp(boundingBox.x1, 0, 0.999);
let pu2 = this.clamp(boundingBox.x2, 0, 0.999);

let minX = Math.floor(pu1 * this._xSlices);
let maxX = Math.floor(pu2 * this._xSlices);

let pv1 = this.clamp(boundingBox.y1, 0, 0.999);
let pv2 = this.clamp(boundingBox.y2, 0, 0.999);

let minY = Math.floor(pv1 * this._ySlices);
let maxY = Math.floor(pv2 * this._ySlices);

//Exponential zplanes
let pd1 = this.clamp(boundingBox.z1, 0, 0.999);
let pd2 = this.clamp(boundingBox.z2, 0, 0.999);

pd1 = pd1 * pd1 * (3.0 - 2.0 * pd1);
Math.pow(pd1, 0.25);

pd2 = pd2 * pd2 * (3.0 - 2.0 * pd2);
Math.pow(pd2, 0.25);

let minZ = Math.floor(pd1 * this._zSlices);
let maxZ = Math.floor(pd2 * this._zSlices);


// loop through bounding frustrum
for (let cz = minZ; cz < maxZ + 1; ++cz) {
for (let cy = minY; cy < maxY + 1; ++cy) {
for (let cx = minX; cx < maxX + 1; ++cx) {
let clusterId = cx + cy * this._xSlices + cz * this._xSlices * this._ySlices;
let numLights = this._clusterTexture.buffer[this._clusterTexture.bufferIndex(clusterId, 0)];
++numLights;
let lidInBuffer = this._clusterTexture.bufferIndex(clusterId, Math.floor(numLights / 4));
lidInBuffer += numLights % 4;
this._clusterTexture.buffer[lidInBuffer] = l;
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(clusterId, 0)] = numLights;
}
}
}
}
}

this._clusterTexture.update();
}
}
28 changes: 26 additions & 2 deletions src/renderers/clusteredDeferred.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import fsSource from '../shaders/deferred.frag.glsl.js';
import TextureBuffer from './textureBuffer';
import ClusteredRenderer from './clustered';

export const NUM_GBUFFERS = 4;
export const NUM_GBUFFERS = 3;

export default class ClusteredDeferredRenderer extends ClusteredRenderer {
constructor(xSlices, ySlices, zSlices) {
Expand All @@ -28,8 +28,12 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {
this._progShade = loadShaderProgram(QuadVertSource, fsSource({
numLights: NUM_LIGHTS,
numGBuffers: NUM_GBUFFERS,
xSliceNum : xSlices,
ySliceNum : ySlices,
zSliceNum : zSlices
}), {
uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]'],
uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]', 'u_lightbuffer', 'u_clusterbuffer',
'u_frustrumRatios', 'u_nearFar', 'u_viewMatrix'],
attribs: ['a_uv'],
});

Expand Down Expand Up @@ -155,6 +159,18 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {

// TODO: Bind any other shader inputs

//upload the width and height ratios
let heightRatio = Math.tan((camera.fov / 2) * Math.PI/180) * 2;
let widthRatio = camera.aspect * heightRatio;
gl.uniform2f(this._progShade.u_frustrumRatios, widthRatio, heightRatio);

//upload near and far clipping planes
gl.uniform2f(this._progShade.u_nearFar, camera.near, camera.far);

//bind view matrix
gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix);


// Bind g-buffers
const firstGBufferBinding = 0; // You may have to change this if you use other texture slots
for (let i = 0; i < NUM_GBUFFERS; i++) {
Expand All @@ -163,6 +179,14 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {
gl.uniform1i(this._progShade[`u_gbuffers[${i}]`], i + firstGBufferBinding);
}

gl.activeTexture(gl[`TEXTURE${NUM_GBUFFERS}`]);
gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture);
gl.uniform1i(this._progShade.u_lightbuffer, NUM_GBUFFERS);

gl.activeTexture(gl[`TEXTURE${NUM_GBUFFERS + 1}`]);
gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture);
gl.uniform1i(this._progShade.u_clusterbuffer, NUM_GBUFFERS + 1);

renderFullscreenQuad(this._progShade);
}
};
Loading