Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project 5: Joseph Klinger #31

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
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
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,46 @@ 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)
* Joseph Klinger
* Tested on: Windows 10, i5-7300HQ (4 CPUs) @ ~2.50GHz, GTX 1050 6030MB (Personal Machine)

### Live Online

[![](img/thumb.png)](http://TODO.github.io/Project5B-WebGL-Deferred-Shading)
[Demo](http://klingerj.github.io/Project5B-WebGL-Deferred-Shading)

### Demo Video/GIF

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

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

*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.
This week, I worked on implementing a clustered Forward+ and clustered deferred renderer. A quick rundown on how those rendering methods work:

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
Forward: simply, render each material with each light, using a nested pair of for loops. Not the most efficient way to do things.

Clustered Forward+: Only render each material with each light that is close enough to influence the object.

Clustered Deferred: Render geometry attributes to G-buffers, apply shading during a second pass, in screen space using the G-buffers. Also uses clusters to only compute lighting for nearby lights.

# Features

Clustered rendering involves dividing the view frustum up into "clusters". During each frame, each light computes which clusters it influences, and so when we go time we go to compute shading for each fragment,
we know that that fragment's shading computations will only take into account lights that are contributing to its color. Any additional computations are unnecessary, making forward rendering a most inefficient
means of rendering.

# Performance Analysis

Here is a graph displaying some stats regarding the performance of each method:

![](img/graph.png.PNG)

Based on the results, we will probably want to avoid forward rendering wherever possible. Clustered Forward+ provides immense improvements but deferred rendering
is even more efficient than that. This is likely due to the fact that we are only computing shading for fragments that are guaranteed to be visible.

For deferred rendering, by packing values into vec4s, I was able to reduce the G-buffers usage from 4 buffers to 2 which is a 50% memory comsumption reduction, which would certainly be a necessity at companies working
on either games or animation (minimizing memory consumption in a render farm is important, for example).

Among other features, I implemented a Blinn-Phong shader for the deferred renderer.

### Credits

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/clustered_Forward.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/graph.png.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ function setSize(width, height) {
canvas.width = width;
canvas.height = height;
camera.aspect = width / height;
camera.far = 50;
camera.near = 0.01;
camera.updateProjectionMatrix();
}

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

export const MAX_LIGHTS_PER_CLUSTER = 100;
Expand Down Expand Up @@ -27,6 +27,69 @@ export default class ClusteredRenderer {
}
}

// For each light, determine which clusters are within its radius of influence
// This computation makes the assumption that the slices are distributed evenly throughout the frustum
for (let i = 0; i < NUM_LIGHTS; ++i) {

// Transform light position into view space
let lightPosVec4 = vec4.fromValues(scene.lights[i].position[0], scene.lights[i].position[1], scene.lights[i].position[2], 1);
var lightPos = vec4.fromValues(0, 0, 0, 1);
vec4.transformMat4(lightPos, lightPosVec4, viewMatrix);

// Frustum width and height at this light's z-value
let halfFrustumHeight = Math.abs(Math.tan(camera.fov * 0.00872664625) * lightPos[2]); // that's pi / 360, since i'm using fov/2
let halfFrustumWidth = halfFrustumHeight * camera.aspect;

// Min and max clusters influenced in the x-direction
let clusterMinX = Math.floor((lightPos[0] - LIGHT_RADIUS + halfFrustumWidth) / (halfFrustumWidth * 2) * this._xSlices);
let clusterMaxX = Math.floor((lightPos[0] + LIGHT_RADIUS + halfFrustumWidth) / (halfFrustumWidth * 2) * this._xSlices);

// Min and max clusters influenced in the y-direction
let clusterMinY = Math.floor((lightPos[1] - LIGHT_RADIUS + halfFrustumHeight) / (halfFrustumHeight * 2) * this._ySlices);
let clusterMaxY = Math.floor((lightPos[1] + LIGHT_RADIUS + halfFrustumHeight) / (halfFrustumHeight * 2) * this._ySlices);

// Min and max clusters influenced in the z-direction
let clusterMinZ = Math.floor((-lightPos[2] - LIGHT_RADIUS - camera.near) / (camera.far - camera.near) * this._zSlices);
let clusterMaxZ = Math.floor((-lightPos[2] + LIGHT_RADIUS - camera.near) / (camera.far - camera.near) * this._zSlices);

// Cull lights not influencing the frustum
if((clusterMinX >= this._xSlices && clusterMaxX >= this._xSlices) || (clusterMaxX < 0 && clusterMinX < 0)) {
continue;
}

if((clusterMinY >= this._ySlices && clusterMaxY >= this._ySlices) || (clusterMaxY < 0 && clusterMinY < 0)) {
continue;
}

if((clusterMinZ >= this._zSlices && clusterMaxZ >= this._zSlices) || (clusterMaxZ < 0 && clusterMinZ < 0)) {
continue;
}

// Clamp cluster indices
clusterMinX = Math.min(Math.max(clusterMinX, 0), this._xSlices - 1);
clusterMaxX = Math.min(Math.max(clusterMaxX, 0), this._xSlices - 1);
clusterMinY = Math.min(Math.max(clusterMinY, 0), this._ySlices - 1);
clusterMaxY = Math.min(Math.max(clusterMaxY, 0), this._ySlices - 1);
clusterMinZ = Math.min(Math.max(clusterMinZ, 0), this._zSlices - 1);
clusterMaxZ = Math.min(Math.max(clusterMaxZ, 0), this._zSlices - 1);

// For each cluster in the range, add this light to its influencing lights
for(let z = clusterMinZ; z <= clusterMaxZ; ++z) {
for(let y = clusterMinY; y <= clusterMaxY; ++y) {
for(let x = clusterMinX; x <= clusterMaxX; ++x) {
let idx = x + y * this._xSlices + z * this._xSlices * this._ySlices;
let bufferIdx = this._clusterTexture.bufferIndex(idx, 0);
let numLightsInThisCluster = this._clusterTexture.buffer[bufferIdx];
if(numLightsInThisCluster < MAX_LIGHTS_PER_CLUSTER) {
this._clusterTexture.buffer[bufferIdx] = numLightsInThisCluster + 1;
let v = Math.floor((numLightsInThisCluster + 1) / 4);
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, v) + numLightsInThisCluster + 1 - v * 4] = i;
}
}
}
}
}

this._clusterTexture.update();
}
}
28 changes: 23 additions & 5 deletions src/renderers/clusteredDeferred.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,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 { NUM_LIGHTS, LIGHT_RADIUS } 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 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 @@ -27,9 +27,10 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {

this._progShade = loadShaderProgram(QuadVertSource, fsSource({
numLights: NUM_LIGHTS,
lightRadius: LIGHT_RADIUS,
numGBuffers: NUM_GBUFFERS,
}), {
uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]'],
uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_lightbuffer', 'u_viewMatrix', 'u_clusterbuffer', 'u_camera_fov', 'u_camera_aspect', 'u_camera_near', 'u_camera_far'],
attribs: ['a_uv'],
});

Expand Down Expand Up @@ -71,7 +72,7 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {
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);
gl.framebufferTexture2D(gl.FRAMEBUFFER, attachments[i], gl.TEXTURE_2D, this._gbuffers[i], 0);
}

if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
Expand Down Expand Up @@ -153,7 +154,24 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {
// Use this shader program
gl.useProgram(this._progShade.glShaderProgram);

// TODO: Bind any other shader inputs
// Upload the view matrix
gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix);

// Set the light texture as a uniform input to the shader
gl.activeTexture(gl.TEXTURE3);
gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture);
gl.uniform1i(this._progShade.u_lightbuffer, 3);

// Set the cluster texture as a uniform input to the shader
gl.activeTexture(gl.TEXTURE4);
gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture);
gl.uniform1i(this._progShade.u_clusterbuffer, 4);

// Set the camera parameters as a uniform input to the shader
gl.uniform1f(this._progShade.u_camera_fov, camera.fov * 0.00872664625); // pi / 360
gl.uniform1f(this._progShade.u_camera_aspect, camera.aspect);
gl.uniform1f(this._progShade.u_camera_near, camera.near);
gl.uniform1f(this._progShade.u_camera_far, camera.far);

// Bind g-buffers
const firstGBufferBinding = 0; // You may have to change this if you use other texture slots
Expand Down
14 changes: 11 additions & 3 deletions src/renderers/clusteredForwardPlus.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { gl } from '../init';
import { mat4, vec4, vec3 } from 'gl-matrix';
import { loadShaderProgram } from '../utils';
import { NUM_LIGHTS } from '../scene';
import { NUM_LIGHTS, LIGHT_RADIUS } from '../scene';
import vsSource from '../shaders/clusteredForward.vert.glsl';
import fsSource from '../shaders/clusteredForward.frag.glsl.js';
import TextureBuffer from './textureBuffer';
Expand All @@ -16,8 +16,9 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer {

this._shaderProgram = loadShaderProgram(vsSource, fsSource({
numLights: NUM_LIGHTS,
lightRadius: LIGHT_RADIUS,
}), {
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_camera_fov', 'u_camera_aspect', 'u_camera_near', 'u_camera_far'],
attribs: ['a_position', 'a_normal', 'a_uv'],
});

Expand Down Expand Up @@ -65,6 +66,9 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer {
// Upload the camera matrix
gl.uniformMatrix4fv(this._shaderProgram.u_viewProjectionMatrix, false, this._viewProjectionMatrix);

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

// Set the light texture as a uniform input to the shader
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture);
Expand All @@ -75,7 +79,11 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer {
gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture);
gl.uniform1i(this._shaderProgram.u_clusterbuffer, 3);

// TODO: Bind any other shader inputs
// Set the camera parameters as a uniform input to the shader
gl.uniform1f(this._shaderProgram.u_camera_fov, camera.fov * 0.00872664625); // pi / 360
gl.uniform1f(this._shaderProgram.u_camera_aspect, camera.aspect);
gl.uniform1f(this._shaderProgram.u_camera_near, camera.near);
gl.uniform1f(this._shaderProgram.u_camera_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
2 changes: 1 addition & 1 deletion src/renderers/textureBuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { gl } from '../init';

export default class TextureBuffer {
/**
* This class represents a buffer in a shader. Unforunately we can't bind arbitrary buffers so we need to pack the data as a texture
* This class represents a buffer in a shader. Unfortunately we can't bind arbitrary buffers so we need to pack the data as a texture
* @param {Number} elementCount The number of items in the buffer
* @param {Number} elementSize The number of values in each item of the buffer
*/
Expand Down
77 changes: 67 additions & 10 deletions src/shaders/clusteredForward.frag.glsl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default function(params) {
return `
// TODO: This is pretty much just a clone of forward.frag.glsl.js
// This is pretty much just a clone of forward.frag.glsl.js

#version 100
precision highp float;
Expand All @@ -9,12 +9,21 @@ export default function(params) {
uniform sampler2D u_normap;
uniform sampler2D u_lightbuffer;

// TODO: Read this buffer to determine the lights influencing a cluster
// Read this buffer to determine the lights influencing a cluster
uniform sampler2D u_clusterbuffer;

uniform mat4 u_viewMatrix;

// Camera parameters
uniform float u_camera_fov; // already in radians
uniform float u_camera_aspect;
uniform float u_camera_near;
uniform float u_camera_far;

varying vec3 v_position;
varying vec3 v_normal;
varying vec2 v_uv;
varying vec3 v_positionVC; // position in view space

vec3 applyNormalMap(vec3 geomnor, vec3 normap) {
normap = normap * 2.0 - 1.0;
Expand Down Expand Up @@ -79,17 +88,65 @@ export default function(params) {
vec3 normap = texture2D(u_normap, v_uv).xyz;
vec3 normal = applyNormalMap(v_normal, normap);

vec3 fragColor = vec3(0.0);
// Compute the same stuff we computed on the CPU for each light
const float lightRadius = float(${params.lightRadius});
const float numClusters = 15.0;

// Frustum width and height at this light's z-value
float halfFrustumHeight = abs(tan(u_camera_fov) * -v_positionVC.z);
float halfFrustumWidth = halfFrustumHeight * u_camera_aspect;
float denom = 1.0 / (2.0 * halfFrustumHeight);
float denomX = 1.0 / (2.0 * halfFrustumWidth);

// Min and max clusters influenced in the x-direction
float clusterX = floor(((v_positionVC.x + halfFrustumWidth) * denomX) * numClusters);

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;
// Min and max clusters influenced in the y-direction
float clusterY = floor(((v_positionVC.y + halfFrustumHeight) * denom) * numClusters);

float lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius);
float lambertTerm = max(dot(L, normal), 0.0);
// Min and max clusters influenced in the z-direction
float clusterZ = floor(((-v_positionVC.z - u_camera_near) / (u_camera_far - u_camera_near)) * numClusters);

fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity);
// Now read from the cluster texture to find out what lights are in the same cluster as this fragment
vec2 uv_cluster = vec2(0.0, 0.0);

float clusterIndex = clusterX + clusterY * (numClusters) + clusterZ * (numClusters) * (numClusters);
uv_cluster.x = clusterIndex / (15.0 * 15.0 * 15.0);

int numLightsInThisCluster = int(texture2D(u_clusterbuffer, uv_cluster).r);

const float MAX_LIGHTS_PER_CLUSTER_RATIO = 1.0 / ceil(100.0 / 4.0);
const int MAX_LIGHTS_PER_CLUSTER = int(ceil(100.0 / 4.0)); // max number of rows in the clusterbuffer texture

vec3 fragColor = vec3(0.0);

for(int i = 0; i <= ${params.numLights}; i += 4) {
if(i > numLightsInThisCluster) {
break;
}
uv_cluster.y = floor(float(i) / 4.0) * MAX_LIGHTS_PER_CLUSTER_RATIO;

vec4 lightIds = texture2D(u_clusterbuffer, uv_cluster);

// Shade using each light in this cluster
for(int l = 0; l < 4; ++l) {
if(l + i == 0) {
continue;
}
if(l + i > numLightsInThisCluster) {
break;
}

Light light = UnpackLight(int(lightIds[l]));
vec3 lightPos = (u_viewMatrix * vec4(light.position, 1.0)).xyz;
float lightDistance = distance(lightPos, v_positionVC);
vec3 L = (lightPos - v_positionVC) / 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);
Expand Down
3 changes: 3 additions & 0 deletions src/shaders/clusteredForward.vert.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
precision highp float;

uniform mat4 u_viewProjectionMatrix;
uniform mat4 u_viewMatrix;

attribute vec3 a_position;
attribute vec3 a_normal;
Expand All @@ -10,10 +11,12 @@ attribute vec2 a_uv;
varying vec3 v_position;
varying vec3 v_normal;
varying vec2 v_uv;
varying vec3 v_positionVC; // position in view space

void main() {
gl_Position = u_viewProjectionMatrix * vec4(a_position, 1.0);
v_position = a_position;
v_normal = a_normal;
v_uv = a_uv;
v_positionVC = (u_viewMatrix * vec4(a_position, 1.0)).xyz;
}
Loading