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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
node_modules
node_modules

.DS_Store
src/.DS_Store
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,51 @@ 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)
* Mohamed Soudy
* Tested on: **Google Chrome 62.0.3202.75**
Windows 10, Intel(R) Xeon(R) CPU E5-2687W v3 @ 3.10GHz 32GB, GTX 980 8GB (SIG Lab)

### Live Online
## Live Online

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

### Demo Video/GIF
## Demo Video/GIF

[![](img/video.png)](TODO)
[![](img/video.gif)](TODO)

### (TODO: Your README)
## Overview

*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.
Implementations of forward, forward clustering, and deferred clustering with Blinn-Phong shading and G-buffer optimizations.

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
## Analysis

When comparing the performance of all three implementations, deferred clustering proves to be much faster than forward
and forward clustering as can be seen in the table and chart below. Forward crashes with 3000 plus lights because without
clustering all the lights are checked for every fragment. With forward clustering however, only the lights that are within
a cluster are checked which significantly improves performance.

### Credits
Clustered deferred is the fastest because it only deals with one fragment per pixel read from the G-buffer, whereas with
the other two implementations all the fragments of a specific pixel are shaded.

![](img/all_table.PNG)

![](img/all_chart.PNG)

The following chart compares the performance difference of using 2 vs 3 G-buffers in deferred shading. By compressing normals
to only store the x and y value, we can fit all the required data (position, color and normal) in only 2 G-buffers.
We can then reconstruct the normal in the shader to get the z value using the magnitude function. This is possible because we
know that the total sum of the components of a normal should be 1 and since we have the x and y we can easily derive the z value
of the normal.

The performance difference is not very significant but using 2 G-buffers instead of the 3 definitely improves performance as can
be seen in the chart below. I predict the difference will be larger when tested with a larger number of lights.


![](img/deferred_table.PNG)

![](img/deferred_chart.PNG)

## 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
Expand Down
2 changes: 2 additions & 0 deletions build/bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build/bundle.js.map

Large diffs are not rendered by default.

Binary file added img/all_chart.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/all_table.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/deferred_chart.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/deferred_table.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/thumb.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/video.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions 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 @@ -53,11 +53,11 @@ export const gui = new DAT.GUI();

// initialize statistics widget
const stats = new Stats();
stats.setMode(1); // 0: fps, 1: ms
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.body.appendChild(stats.domElement);
//document.body.appendChild(stats.domElement);

// Initialize camera
export const camera = new PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
Expand Down
69 changes: 68 additions & 1 deletion src/renderers/clustered.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,73 @@ export default class ClusteredRenderer {
}
}

var halfHeight = Math.tan(camera.fov / 2.0 * (Math.PI/180.0));
var halfWidth = halfHeight * camera.aspect;

for (let l = 0; l < NUM_LIGHTS; ++l) {
let radius = scene.lights[l].radius;
var lightPos = vec4.fromValues(scene.lights[l].position[0],
scene.lights[l].position[1], scene.lights[l].position[2], 1.0);
vec4.transformMat4(lightPos, lightPos, viewMatrix);
lightPos[2] *= -1.0;

let lightPosX = lightPos[0];
let lightPosY = lightPos[1];
let lightPosZ = lightPos[2];

let sliceYHalfHeight = halfHeight * lightPosZ;
let sliceYHeight = sliceYHalfHeight * 2.0;
let sliceYstride = sliceYHeight / this._ySlices;

let startY = Math.floor((lightPosY - radius + sliceYHalfHeight) / sliceYstride);
let endY = Math.floor((lightPosY + radius + sliceYHalfHeight) / sliceYstride);

let sliceXHalfHeight = halfWidth * lightPosZ;
let sliceXHeight = sliceXHalfHeight * 2.0;
let sliceXstride = sliceXHeight / this._xSlices;

let startX = Math.floor((lightPosX - radius + sliceXHalfHeight) / sliceXstride) - 1;
let endX = Math.floor((lightPosX + radius + sliceXHalfHeight) / sliceXstride) + 1;

let sliceZHeight = (camera.far - camera.near);
let sliceZstride = sliceZHeight / this._zSlices;

let startZ = Math.floor((lightPosZ - radius) / sliceZstride);
let endZ = Math.floor((lightPosZ + radius) / sliceZstride);

if((startZ < 0 && endZ < 0) || (startZ >= this._zSlices && endZ >= this._zSlices)) continue;
if((startY < 0 && endY < 0) || (startY >= this._ySlices && endY >= this._ySlices)) continue;
if((startX < 0 && endX < 0) || (startX >= this._xSlices && endX >= this._xSlices)) continue;

startX = startX.clamp(0, this._xSlices-1);
endX = endX.clamp(0, this._xSlices-1);
startY = startY.clamp(0, this._ySlices-1);
endY = endY.clamp(0, this._ySlices-1);
startZ = startZ.clamp(0, this._zSlices-1);
endZ = endZ.clamp(0, this._zSlices-1);

for (let z = startZ; z <= endZ; z++) {
for (let y = startY; y <= endY; y++) {
for (let x = startX; x <= endX; x++) {
let i = x + y*this._xSlices + z *this._ySlices*this._xSlices;
let lightIndex = this._clusterTexture.bufferIndex(i, 0);
let numLights = 1 + this._clusterTexture.buffer[lightIndex];

if (numLights <= MAX_LIGHTS_PER_CLUSTER) {
let col = Math.floor(numLights / 4);
let row = Math.floor(numLights % 4);
this._clusterTexture.buffer[lightIndex] = numLights;
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(i, col) + row] = l;
}
}
}
}
}

this._clusterTexture.update();
}
}
}

Number.prototype.clamp = function(min, max) {
return Math.min(Math.max(this, min), max);
};
36 changes: 31 additions & 5 deletions src/renderers/clusteredDeferred.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { gl, WEBGL_draw_buffers, canvas } from '../init';
import { mat4, vec4 } from 'gl-matrix';
import { loadShaderProgram, renderFullscreenQuad } from '../utils';
import { NUM_LIGHTS } from '../scene';
import { MAX_LIGHTS_PER_CLUSTER } from './clustered';
import toTextureVert from '../shaders/deferredToTexture.vert.glsl';
import toTextureFrag from '../shaders/deferredToTexture.frag.glsl';
import QuadVertSource from '../shaders/quad.vert.glsl';
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 = 2;

export default class ClusteredDeferredRenderer extends ClusteredRenderer {
constructor(xSlices, ySlices, zSlices) {
Expand All @@ -21,21 +22,25 @@ 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,
numLights: NUM_LIGHTS, xSlices: xSlices, ySlices: ySlices, zSlices: zSlices,
maxLightsPerCluster: MAX_LIGHTS_PER_CLUSTER,
numGBuffers: NUM_GBUFFERS,
}), {
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_viewProjectionMatrix', 'u_viewMatrix', 'u_inverseViewMatrix', 'u_lightbuffer', 'u_clusterbuffer',
'u_near', 'u_far', 'u_screenWidth', 'u_screenHeight'],
attribs: ['a_uv'],
});

this._projectionMatrix = mat4.create();
this._viewMatrix = mat4.create();
this._viewProjectionMatrix = mat4.create();
this._inverseViewMatrix = mat4.create();
}

setupDrawBuffers(width, height) {
Expand Down Expand Up @@ -108,6 +113,7 @@ 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._inverseViewMatrix, this._viewMatrix);

// Render to the whole screen
gl.viewport(0, 0, canvas.width, canvas.height);
Expand All @@ -123,6 +129,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 @@ -153,10 +160,29 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {
// Use this shader program
gl.useProgram(this._progShade.glShaderProgram);

// Upload the camera matrix
gl.uniformMatrix4fv(this._progShade.u_viewProjectionMatrix, false, this._viewProjectionMatrix);
gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix);
gl.uniformMatrix4fv(this._progShade.u_inverseViewMatrix, false, this._inverseViewMatrix);

// 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._progShade.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._progShade.u_clusterbuffer, 3);

// TODO: Bind any other shader inputs
gl.uniform1f(this._progShade.u_screenWidth, canvas.width);
gl.uniform1f(this._progShade.u_screenHeight, canvas.height);
gl.uniform1f(this._progShade.u_near, camera.near);
gl.uniform1f(this._progShade.u_far, camera.far);

// Bind g-buffers
const firstGBufferBinding = 0; // You may have to change this if you use other texture slots
const firstGBufferBinding = 4; // 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
13 changes: 11 additions & 2 deletions src/renderers/clusteredForwardPlus.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { gl } from '../init';
import { mat4, vec4, vec3 } from 'gl-matrix';
import { loadShaderProgram } from '../utils';
import { NUM_LIGHTS } from '../scene';
import { MAX_LIGHTS_PER_CLUSTER } from './clustered';
import vsSource from '../shaders/clusteredForward.vert.glsl';
import fsSource from '../shaders/clusteredForward.frag.glsl.js';
import TextureBuffer from './textureBuffer';
Expand All @@ -15,9 +16,12 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer {
this._lightTexture = new TextureBuffer(NUM_LIGHTS, 8);

this._shaderProgram = loadShaderProgram(vsSource, fsSource({
numLights: NUM_LIGHTS,
numLights: NUM_LIGHTS, xSlices: xSlices, ySlices: ySlices, zSlices: zSlices,
maxLightsPerCluster: MAX_LIGHTS_PER_CLUSTER,
}), {
uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer'],
uniforms: ['u_viewProjectionMatrix', 'u_viewMatrix', 'u_colmap', 'u_normap',
'u_lightbuffer', 'u_clusterbuffer', 'u_near', 'u_far',
'u_screenWidth', 'u_screenHeight'],
attribs: ['a_position', 'a_normal', 'a_uv'],
});

Expand Down Expand Up @@ -64,6 +68,7 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer {

// Upload the camera matrix
gl.uniformMatrix4fv(this._shaderProgram.u_viewProjectionMatrix, false, this._viewProjectionMatrix);
gl.uniformMatrix4fv(this._shaderProgram.u_viewMatrix, false, this._viewMatrix);

// Set the light texture as a uniform input to the shader
gl.activeTexture(gl.TEXTURE2);
Expand All @@ -76,6 +81,10 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer {
gl.uniform1i(this._shaderProgram.u_clusterbuffer, 3);

// TODO: Bind any other shader inputs
gl.uniform1f(this._shaderProgram.u_screenWidth, canvas.width);
gl.uniform1f(this._shaderProgram.u_screenHeight, canvas.height);
gl.uniform1f(this._shaderProgram.u_near, camera.near);
gl.uniform1f(this._shaderProgram.u_far, 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);
Expand Down
27 changes: 26 additions & 1 deletion src/shaders/clusteredForward.frag.glsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export default function(params) {
// TODO: Read this buffer to determine the lights influencing a cluster
uniform sampler2D u_clusterbuffer;

uniform mat4 u_viewMatrix;
uniform float u_screenWidth;
uniform float u_screenHeight;
uniform float u_near;
uniform float u_far;

varying vec3 v_position;
varying vec3 v_normal;
varying vec2 v_uv;
Expand Down Expand Up @@ -79,10 +85,29 @@ export default function(params) {
vec3 normap = texture2D(u_normap, v_uv).xyz;
vec3 normal = applyNormalMap(v_normal, normap);

vec4 viewPos = u_viewMatrix * vec4(v_position, 1.0);
viewPos.z = -viewPos.z;

float clusterXStride = float(u_screenWidth) / float(${params.xSlices});
float clusterYStride = float(u_screenHeight) / float(${params.ySlices});
float clusterZStride = float(u_far - u_near) / float(${params.zSlices});

int clusterX = int(gl_FragCoord.x / clusterXStride);
int clusterY = int(gl_FragCoord.y / clusterYStride);
int clusterZ = int( (viewPos.z - u_near) / clusterZStride);

int clusterIndex = clusterX + clusterY * ${params.xSlices} + clusterZ * ${params.xSlices} * ${params.ySlices};

int numClusters = ${params.xSlices} * ${params.ySlices} * ${params.zSlices};
float u = float(clusterIndex+1)/float(numClusters+1);
int textureHeight = int(ceil(float(${params.maxLightsPerCluster}+1) / 4.0));
int clusterNumLights = int(texture2D(u_clusterbuffer, vec2(u,0)).r);

vec3 fragColor = vec3(0.0);

for (int i = 0; i < ${params.numLights}; ++i) {
Light light = UnpackLight(i);
if (i >= clusterNumLights) break;
Light light = UnpackLight(int(ExtractFloat(u_clusterbuffer, numClusters, textureHeight, clusterIndex, i+1)));
float lightDistance = distance(light.position, v_position);
vec3 L = (light.position - v_position) / lightDistance;

Expand Down
Loading