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- Ryan Tong #3

Open
wants to merge 12 commits into
base: main
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
31 changes: 21 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,37 @@ WebGL Forward+ and Clustered Deferred 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)
* Ryan Tong
* Tested on: **Google Chrome 160.0** on
Windows 10, i7-8750H @ 2.20GHz 16GB, GeForce GTX 1060 6144MB (Personal Laptop)

### Live Online

[![](img/thumb.png)](http://TODO.github.io/Project5-WebGL-Forward-Plus-and-Clustered-Deferred)
[![](img/title.jpg)](http://ryanryantong.github.io/Project5-WebGL-Forward-Plus-and-Clustered-Deferred)

### Demo Video/GIF

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

### (TODO: Your README)
### Project Description
This project implements forward+ and clustered deferred rendering. Forward+ rendering optimizes a forward rendering pipeline by dividing the scene into frustums. The lights in the scene are treated as spheres and are then clustered into frustum buckets based on position. This allows the shader to only need to check the lights closeby that are within the frustum instead of looping through all lights which . Blinn-phong shading is also implemented.

*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.
### Performance Analysis
The key advantage of the forward+ and clustered deferred pipeline is the optimization to only check relevant light sources. Therefore, performance gains are especially noticeable in scenes with a large number of lights. Clustered deferred rendering is also implemented. Clustered deferred rendering further improves the forward+ pipeline by decoupling the calculation of vertex attributes and shading. This is especially good for scenes with complex or large numbers of objects. Note that the max number of lights allowed in a cluster are increased each time to match the total number of lights. Below is a plot of delay based on the number of lights which shows these performance gains.

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
### Performance
![performance](img/performance.png)
From these results we can clearly see that clustered deferred is fastest, followed by forward+, then regular forward rendering. This is expected because clustered deferred builds on the optimizations in forward+.

### Optimizations
Unfortunately, I did not have time to implement further optimization but instead provide some theoretical analysis. Specifically with GBuffers that each hold 4 floats to pass 3 vec3s (9 values). If we were able to condense one of the vec3s into 2 values, we would only have 8 values meaning we can save a Gbuffer. This would reduce the number of different global memory locations read meaning better performance. This could be reduced even further if the position vector is not passed and instead calculated using the camera and X/Y/depth. Although a comparison would need to be made to see if the computations outweigh the delay improvement.

### Blinn-Phong Shading
I implemented blinn-phong shading to improve the visual quality of the render. This is a standard technique that has negligible impact. The results can be seen below.
### Regular
![regular](img/regular.jpg)
### Blinn-Phong
![blinn](img/blinn.jpg)

### Credits

Expand Down
Binary file added img/blinn.jpg
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/performance.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/regular.jpg
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/title.jpg
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/vid.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/vid.mp4
Binary file not shown.
14 changes: 7 additions & 7 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,29 @@ scene.loadGLTF('models/sponza/sponza.gltf');
// It lets you draw arbitrary lines in the scene.
// This may be helpful for visualizing your frustum clusters so you can make
// sure that they are in the right place.
const wireframe = new Wireframe();

var segmentStart = [-14.0, 0.0, -6.0];
var segmentEnd = [14.0, 20.0, 6.0];
var segmentColor = [1.0, 0.0, 0.0];
wireframe.addLineSegment(segmentStart, segmentEnd, segmentColor);
wireframe.addLineSegment([-14.0, 1.0, -6.0], [14.0, 21.0, 6.0], [0.0, 1.0, 0.0]);
//wireframe.addLineSegment(segmentStart, segmentEnd, segmentColor);
//wireframe.addLineSegment([-14.0, 1.0, -6.0], [14.0, 21.0, 6.0], [0.0, 1.0, 0.0]);

camera.position.set(-10, 8, 0);
cameraControls.target.set(0, 2, 0);
gl.enable(gl.DEPTH_TEST);

function render() {
const wireframe = new Wireframe();
scene.update();
params._renderer.render(camera, scene);
params._renderer.render(camera, scene, wireframe);

// LOOK: Render wireframe "in front" of everything else.
// If you would like the wireframe to render behind and in front
// of objects based on relative depths in the scene, comment out /
//the gl.disable(gl.DEPTH_TEST) and gl.enable(gl.DEPTH_TEST) lines.
gl.disable(gl.DEPTH_TEST);
wireframe.render(camera);
gl.enable(gl.DEPTH_TEST);
// gl.disable(gl.DEPTH_TEST);
// wireframe.render(camera);
// gl.enable(gl.DEPTH_TEST);
}

makeRenderLoop(render)();
185 changes: 182 additions & 3 deletions src/renderers/base.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { vec3, vec4 } from 'gl-matrix';
import { NUM_LIGHTS } from '../scene';
import TextureBuffer from './textureBuffer';

export const MAX_LIGHTS_PER_CLUSTER = 100;
Expand All @@ -10,11 +12,10 @@ export default class BaseRenderer {
this._ySlices = ySlices;
this._zSlices = zSlices;
}

updateClusters(camera, viewMatrix, scene) {
updateClusters(camera, viewMatrix, scene, wireframe) {
// 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...

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 @@ -25,6 +26,184 @@ export default class BaseRenderer {
}
}

// Project to camera space, z slices are centered at 0,0
// For min, max point on sphere, calc min, max cluster indicies
// Get w,h of Z slice of view frustrum using:
// h = 2 * tan(fovRadians/ 2) * zDist; w = h * aspectRatio;
// Compute which x,y bucket/cluster
// Compute which z bucket/cluster
// Cache ratio to avoid repeated computations
let heightRatio = 2 * Math.tan(camera.fov / 2 * Math.PI / 180);
let widthRatio = heightRatio * camera.aspect;

//Loop over all lights
for (let i = 0; i < NUM_LIGHTS; ++i) {
let light = scene.lights[i];
let lightPos = vec4.fromValues(light.position[0], light.position[1], light.position[2], 1);
// Do radius and camera.near offset before projection since they are in world position?
// Not as efficent bc more lights in range now :/ but not sure why flickering was occuring
// 7 was found experimentally, removes ~most flickering
let radius = light.radius + 7; //This offset increases the reach of the light to reduce flickering
let lightMin = vec4.fromValues(light.position[0] - radius, light.position[1] - radius, light.position[2] - camera.near - radius, 1);
let lightMax = vec4.fromValues(light.position[0] + radius, light.position[1] + radius, light.position[2] - camera.near + radius, 1);
//let cameraPos = vec4.fromValues(camera.position.x, camera.position.y, camera.position.z, 1);

//Project to camera space
vec4.transformMat4(lightMin, lightMin, viewMatrix);
vec4.transformMat4(lightMax, lightMax, viewMatrix);
vec4.transformMat4(lightPos, lightPos, viewMatrix);


// NOTE: sign of z becomes flipped after viewMatrix transform
// clamp light min so that zmin is not neg?
var tempMin = [lightMin[0], lightMin[1], lightMin[2], 1];
var tempMax = [lightMax[0], lightMax[1], lightMax[2], 1];
// if (lightMin[2] > 0) {
// lightMin[2] = 0;
// }
// if (lightMax[2] > 0) {
// lightMax[2] = 0;
// }
lightMin[2] *= -1;
lightMax[2] *= -1;
lightPos[2] *= -1;
// if (lightMin[2] < 0 || lightMax[2] < 0 || lightMin[2] > lightMax[2]) {
// // console.log(tempMin);
// // console.log(tempMax);
// // debugger;
// continue;
// }
// if (lightMin[0] > lightMax[0] || lightMin[1] > lightMax[1]) {
// debugger;
// }

// Calc h, w based on Z
let totalHeightNear = heightRatio * lightMin[2];
let totalWidthNear = widthRatio * lightMin[2];
let totalHeightFar = heightRatio * lightMax[2];
let totalWidthFar = widthRatio * lightMax[2];

//Half height
let halfHeightNear = totalHeightNear / 2.0;
let halfWidthNear = totalWidthNear / 2.0;
let halfHeightFar = totalHeightFar / 2.0;
let halfWidthFar = totalWidthFar / 2.0;

//Strides
let yStrideNear = totalHeightNear / this._ySlices;
let xStrideNear = totalWidthNear / this._xSlices;
let yStrideFar = totalHeightFar / this._ySlices;
let xStrideFar = totalWidthFar / this._xSlices;
let zStride = (camera.far - camera.near) / this._zSlices;

// Add h,w / 2 to be relative to [0, h,w] instead of [-h/2,-w/2, h/2,w/2] to avoid negative flooring issues
//Camera space pos after being shifted
let yMinPos = (lightMin[1] + halfHeightNear);
let yMaxPos = (lightMax[1] + halfHeightFar);
let xMinPos = (lightMin[0] + halfWidthNear);
let xMaxPos = (lightMax[0] + halfWidthFar);

// if (yMinPos < 0 || yMaxPos < 0 || xMinPos < 0 || xMaxPos < 0) {
// debugger;
// }
// if (yStrideNear < 0 || xStrideNear < 0 || yStrideFar < 0 || xStrideFar < 0) {
// debugger;
// }

let totalWidth = lightPos[2] * widthRatio;
let totalHeight = lightPos[2] * heightRatio;
let yStride = totalHeight / this._ySlices;
let xStride = totalWidth / this._xSlices;

// x, y bucketing
// clamp out of bounds
let yMax = Math.min(Math.max(Math.floor(yMaxPos / yStrideFar), 0), this._ySlices - 1);
let yMin = Math.min(Math.max(Math.floor(yMinPos / yStrideNear), 0), this._ySlices - 1);
let xMin = Math.min(Math.max(Math.floor(xMinPos / xStrideNear), 0), this._xSlices - 1);
let xMax = Math.min(Math.max(Math.floor(xMaxPos / xStrideFar), 0), this._xSlices - 1);

// z bucketing, z ranges from camera.near -> camera.far
// Subtract camera.near to get [0, camera.far-camera.near]
//Something is wrong here maybe? Z is never > 0 bc zStride is so big
let zMin = Math.min(Math.max(Math.floor(lightMin[2] / zStride), 0), this._zSlices - 1);
let zMax = Math.min(Math.max(Math.floor(lightMax[2] / zStride), 0), this._zSlices - 1);
//console.log("(%d, %d), (%d, %d), (%d,%d)", xMin, xMax, yMin, yMax, zMin, zMax);

// Loop through all canidate frustrums
for (let z = zMin; z <= zMax; ++z) {
for (let y = yMin; y <= yMax; ++y) {
for (let x = xMin; x <= xMax; ++x) {
let idx = x + y * this._xSlices + z * this._xSlices * this._ySlices;
let numLights = this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, 0)];
if (numLights > MAX_LIGHTS_PER_CLUSTER) {
continue;
}
++this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, 0)];
++numLights;

let debug = 1;

if (debug == 0) {
// DEBUG CODE
// NOTE: Flip zs back around to be negative for transformation
let startZ = z * zStride + camera.near;
let frontBL = vec4.fromValues(x * xStrideNear - halfWidthNear, y * yStrideNear - halfHeightNear, -startZ, 1);
let frontBR = vec4.fromValues(frontBL[0] + xStrideNear, frontBL[1], frontBL[2], 1);
let frontTL = vec4.fromValues(frontBL[0], frontBL[1] + yStrideNear, frontBL[2], 1);
let frontTR = vec4.fromValues(frontBL[0] + xStrideNear, frontBL[1] + yStrideNear, frontBL[2], 1);

let backBL = vec4.fromValues(x * xStrideFar - halfWidthFar, y * yStrideFar - halfHeightFar, -(startZ + zStride), 1);
let backBR = vec4.fromValues(backBL[0] + xStrideFar, backBL[1], backBL[2], 1);
let backTL = vec4.fromValues(backBL[0], backBL[1] + yStrideFar, backBL[2], 1);
let backTR = vec4.fromValues(backBL[0] + xStrideFar, backBL[1] + yStrideFar, backBL[2], 1);

// Transform back to world space
let tempFrontBL = vec4.create();
let tempFrontBR = vec4.create();
let tempFrontTL = vec4.create();
let tempFrontTR = vec4.create();

vec4.transformMat4(tempFrontBL, frontBL, camera.matrixWorld.toArray());
vec4.transformMat4(tempFrontBR, frontBR, camera.matrixWorld.toArray());
vec4.transformMat4(tempFrontTL, frontTL, camera.matrixWorld.toArray());
vec4.transformMat4(tempFrontTR, frontTR, camera.matrixWorld.toArray());

vec4.transformMat4(backBL, backBL, camera.matrixWorld.toArray());
vec4.transformMat4(backBR, backBR, camera.matrixWorld.toArray());
vec4.transformMat4(backTL, backTL, camera.matrixWorld.toArray());
vec4.transformMat4(backTR, backTR, camera.matrixWorld.toArray());

// Draw Lines
var red = [1.0, 0.0, 0.0];
var green = [0.0, 1.0, 0.0];
var blue = [0.0, 0.0, 1.0];
// Front face
wireframe.addLineSegment([frontBL[0], frontBL[1], frontBL[2]], [frontTL[0], frontTL[1], frontTL[2]], red);
wireframe.addLineSegment([frontBL[0], frontBL[1], frontBL[2]], [frontBR[0], frontBR[1], frontBR[2]], red);
wireframe.addLineSegment([frontBR[0], frontBR[1], frontBR[2]], [frontTR[0], frontTR[1], frontTR[2]], red);
wireframe.addLineSegment([frontTR[0], frontTR[1], frontTR[2]], [frontTL[0], frontTL[1], frontTL[2]], red);

// Back face
wireframe.addLineSegment([backBL[0], backBL[1], backBL[2]], [backTL[0], backTL[1], backTL[2]], blue);
wireframe.addLineSegment([backBL[0], backBL[1], backBL[2]], [backBR[0], backBR[1], backBR[2]], blue);
wireframe.addLineSegment([backBR[0], backBR[1], backBR[2]], [backTR[0], backTR[1], backTR[2]], blue);
wireframe.addLineSegment([backTR[0], backTR[1], backTR[2]], [backTL[0], backTL[1], backTL[2]], blue);

//Connecting Lines
wireframe.addLineSegment([backBL[0], backBL[1], backBL[2]], [frontBL[0], frontBL[1], frontBL[2]], green);
wireframe.addLineSegment([backBR[0], backBR[1], backBR[2]], [frontBR[0], frontBR[1], frontBR[2]], green);
wireframe.addLineSegment([backTR[0], backTR[1], backTR[2]], [frontTR[0], frontTR[1], frontTR[2]], green);
wireframe.addLineSegment([backTL[0], backTL[1], backTL[2]], [frontTL[0], frontTL[1], frontTL[2]], green);
}

// Set light index
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, Math.floor(numLights / 4)) + Math.floor(numLights % 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
Expand Up @@ -29,7 +29,9 @@ export default class ClusteredDeferredRenderer extends BaseRenderer {
numLights: NUM_LIGHTS,
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_lightbuffer', 'u_clusterbuffer', 'u_viewMatrix', 'u_slices', 'u_height', 'u_width',
'u_near', 'u_far', 'u_clipDist'],
attribs: ['a_uv'],
});

Expand Down Expand Up @@ -98,7 +100,7 @@ export default class ClusteredDeferredRenderer extends BaseRenderer {
gl.bindTexture(gl.TEXTURE_2D, null);
}

render(camera, scene) {
render(camera, scene, wireframe) {
if (canvas.width != this._width || canvas.height != this._height) {
this.resize(canvas.width, canvas.height);
}
Expand Down Expand Up @@ -142,7 +144,7 @@ export default class ClusteredDeferredRenderer extends BaseRenderer {
this._lightTexture.update();

// Update the clusters for the frame
this.updateClusters(camera, this._viewMatrix, scene);
this.updateClusters(camera, this._viewMatrix, scene, wireframe);

// Bind the default null framebuffer which is the screen
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
Expand All @@ -153,8 +155,6 @@ export default class ClusteredDeferredRenderer extends BaseRenderer {
// Use this shader program
gl.useProgram(this._progShade.glShaderProgram);

// TODO: Bind any other shader inputs

// 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 +163,24 @@ export default class ClusteredDeferredRenderer extends BaseRenderer {
gl.uniform1i(this._progShade[`u_gbuffers[${i}]`], i + firstGBufferBinding);
}

// TODO: Bind any other shader inputs
// Set the light texture as a uniform input to the shader
gl.activeTexture(gl[`TEXTURE${NUM_GBUFFERS}`]);
gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture);
gl.uniform1i(this._progShade.u_lightbuffer, NUM_GBUFFERS);

// Set the cluster texture as a uniform input to the shader
gl.activeTexture(gl[`TEXTURE${NUM_GBUFFERS + 1}`]);
gl.bindTexture(gl.TEXTURE_2D, this._clusterTexture.glTexture);
gl.uniform1i(this._progShade.u_clusterbuffer, NUM_GBUFFERS+1);

gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix);
gl.uniform3f(this._progShade.u_slices, this._xSlices, this._ySlices, this._zSlices);
gl.uniform1f(this._progShade.u_height, canvas.height);
gl.uniform1f(this._progShade.u_width, canvas.width);
gl.uniform1f(this._progShade.u_near, camera.near);
gl.uniform1f(this._progShade.u_far, camera.far);

renderFullscreenQuad(this._progShade);
}
};
2 changes: 1 addition & 1 deletion src/renderers/forward.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default class ForwardRenderer {
this._viewProjectionMatrix = mat4.create();
}

render(camera, scene) {
render(camera, scene, wireframe) {
// Update the camera matrices
camera.updateMatrixWorld();
mat4.invert(this._viewMatrix, camera.matrixWorld.elements);
Expand Down
Loading