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

Project5: Yu Sun #3

Open
wants to merge 4 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
60 changes: 48 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,62 @@ WebGL Clustered 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)
* Yu Sun
* [LinkedIn](https://www.linkedin.com/in/yusun3/)
* Tested on: Tested on: Windows 10 , i7-6700HQ CPU @ 2.60GHz × 8 , GeForce GTX 960M/PCIe/SSE2, 7.7GB Memory (Personal Laptop)
## Introduction
In the previous project, a basic shading method was implemented. However, we notice that the implementation was not
very efficient since we were doing multiple computation such as vertex transformation and rasterization many times for
each object even if not all lights would affect a single project. Therefore, in this project, more efficient shading methods such as
forward plus shading and clustered deferred shading is implemented and compared.

### Live Online
```
* Clustered forward plus shading
* clustered deferred shading with Blinn-Phong effect
* G-buffer usage optimization with 2-component normal
```

[![](img/thumb.png)](http://TODO.github.io/Project5B-WebGL-Deferred-Shading)
### Clusters
Both the forward plus shading and deferred shading take advantage of the concept of clusters. The idea is that
computing shading for each object with each light is too computation expensive. To save the amount of computation needed,
we divide the whole frame into grids, and define a radius of influence for the lights. Thus, we save the amount of
computation by only computing lights that would have an influence on the object.

### Forward Shading and Deferred Shading
For forward shading, we are doing computation one step by one step, and ends up computing many times for objects that are actually
occluded in the scene. This is not very efficient when there are a huge number of lights.

For deferred shading, we store the depth, normals, colors and other information we needed into a g-buffer, and do the
shading for only objects that's visible in the scene. However, deferred shading does suffer from limitation of memory
bandwidth since it reads from g-buffer for each light.

### Demo Video/GIF
## Live Online

Wait to be added.
[![](img/thumb.png)](http://TODO.github.io/Project5B-WebGL-Deferred-Shading)

[![](img/video.png)](TODO)
## Demo Video/GIF

### (TODO: Your README)
![](img/demo.gif)

*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.
## Analysis
The chart belows shows the amount of time taken to render each frame (averaged across 10 iterations) for the three shading
methods introduced.
![](img/pa.png)
It is clear that when the number of lights in the scene are small, all three methods don't have a significant difference
in terms of performance. However, as the number of lights increase, the time required for the naive forward shading method
increases significantly. While the clustered deferred shading keeps about the same performance.

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
I also use 2-component normal to reduce the g-buffer size. Therefore, reducing the time that would require to read from
memory. However, I only have the implementation for two component g-buffer so I don't have the exact number of the amount
of time it saves. However, rationally I would think the gain over time wouldn't be too significant. Since the g-buffer
are stored in continuous memory, and when the program reads it can make use of cache as well. The significance however,
lies in the amount of memory that we can save from this optimization. This is significant when we have huge amount of data
that we need to store.

### Known Issue
The rendering using forward plus and clustered deferred led to some artifacts when the number of lights increase.
I don't have the time to investigate deeply but I believe this is caused by the scene division and value rounding.

### Credits

Expand Down
Binary file added img/.DS_Store
Binary file not shown.
Binary file added img/demo.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/pa.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: 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
15 changes: 14 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const FORWARD_PLUS = 'Forward+';
const CLUSTERED = 'Clustered';

const params = {
renderer: FORWARD_PLUS,
renderer: CLUSTERED,
_renderer: null,
};

Expand Down Expand Up @@ -38,9 +38,22 @@ camera.position.set(-10, 8, 0);
cameraControls.target.set(0, 2, 0);
gl.enable(gl.DEPTH_TEST);

var count = 0;
var time = 0;
var prev_time = 0;
function render() {
count++;
if (count >= 10) count = 0;
scene.update();
params._renderer.render(camera, scene);
let t1 = performance.now();
time += (t1 - prev_time);
prev_time = t1;
if (count === 0 ){
console.log("Rendering took " + time/10 + " milliseconds.");
time = 0;
}

}

makeRenderLoop(render)();
124 changes: 102 additions & 22 deletions src/renderers/base.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,110 @@
import TextureBuffer from './textureBuffer';
import {NUM_LIGHTS} from "../scene";
import { mat4, vec3, vec4 } from 'gl-matrix';

export const MAX_LIGHTS_PER_CLUSTER = 100;

function getDistanceX(lightPos, x){
let xNormal = vec3.fromValues(1.0 / Math.sqrt(x * x + 1), 0 , -x / Math.sqrt(x * x + 1));
return vec3.dot(vec3.fromValues(lightPos[0], lightPos[1], lightPos[2]), xNormal);
}

function getDistanceY(lightPos, y){
let yNormal = vec3.fromValues(0, 1.0 / Math.sqrt(y * y + 1), -y / Math.sqrt(y * y + 1));
return vec3.dot(vec3.fromValues(lightPos[0], lightPos[1], lightPos[2]), yNormal);
}

export default class BaseRenderer {
constructor(xSlices, ySlices, zSlices) {
// Create a texture to store cluster data. Each cluster stores the number of lights followed by the light indices
this._clusterTexture = new TextureBuffer(xSlices * ySlices * zSlices, MAX_LIGHTS_PER_CLUSTER + 1);
this._xSlices = xSlices;
this._ySlices = ySlices;
this._zSlices = zSlices;
}

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...

for (let z = 0; z < this._zSlices; ++z) {
for (let y = 0; y < this._ySlices; ++y) {
for (let x = 0; x < this._xSlices; ++x) {
let i = x + y * this._xSlices + z * this._xSlices * this._ySlices;
// Reset the light count to 0 for every cluster
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(i, 0)] = 0;
}
}
constructor(xSlices, ySlices, zSlices) {
// Create a texture to store cluster data. Each cluster stores the number of lights followed by the light indices
this._clusterTexture = new TextureBuffer(xSlices * ySlices * zSlices, MAX_LIGHTS_PER_CLUSTER + 1);
this._xSlices = xSlices;
this._ySlices = ySlices;
this._zSlices = zSlices;
}

this._clusterTexture.update();
}
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...

for (let z = 0; z < this._zSlices; ++z) {
for (let y = 0; y < this._ySlices; ++y) {
for (let x = 0; x < this._xSlices; ++x) {
let i = x + y * this._xSlices + z * this._xSlices * this._ySlices;
// Reset the light count to 0 for every cluster
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(i, 0)] = 0;
}
}
}

// with 'z-normalized'
const y_half = Math.tan(camera.fov * 0.5 * Math.PI /180.0);
const x_half = camera.aspect * y_half;
const y_step = y_half * 2.0 / this._ySlices;
const x_step = x_half * 2.0 / this._xSlices;
const z_step = (camera.far - camera.near) / this._zSlices;


for (let i = 0; i < NUM_LIGHTS; i++){

let xMin, xMax, yMin, yMax, zMin, zMax;
let lightPos = vec4.fromValues(scene.lights[i].position[0], scene.lights[i].position[1],
scene.lights[i].position[2], 1.0);
vec4.transformMat4(lightPos, lightPos, viewMatrix);
lightPos[2] *= -1.0;

let lightRadius = scene.lights[i].radius;
let z = lightPos[2] - camera.near;

zMin = Math.floor((z - lightRadius) / z_step);
zMax = Math.floor((z + lightRadius) / z_step);

if (zMin >= this._zSlices || zMax < 0) continue;
zMin = Math.max(0, zMin);
zMax = Math.min(zMax, this._zSlices - 1);


for (xMin = 0; xMin <= this._xSlices; xMin++) {
let xSeg = -x_half + xMin * x_step;
let dist = getDistanceX(lightPos, xSeg);
if (Math.abs(dist) < lightRadius) break;
}

for (xMax = this._xSlices; xMax >= xMin; xMax--){
let xSeg = -x_half + xMax * x_step;
let dist = getDistanceX(lightPos, xSeg);
if (Math.abs(dist) < lightRadius) break;
}

for (yMin = 0; yMin <= this._ySlices; yMin++){
let ySeg = -y_half + yMin * y_step;
let dist = getDistanceY(lightPos, ySeg);
if (Math.abs(dist) < lightRadius) break;
}

for (yMax = this._ySlices; yMax >= yMin; yMax--){
let ySeg = -y_half + yMax * y_step;
let dist = getDistanceY(lightPos, ySeg);
if (Math.abs(dist) < lightRadius) break;
}

for (let x = xMin; x < xMax; x++){
for(let y = yMin; y < yMax; y++){
for(let z = zMin; z <= zMax; z++){
let pixel = x + y * this._xSlices + z * this._xSlices * this._ySlices;
let num_light = this._clusterTexture.buffer[this._clusterTexture.bufferIndex(pixel, 0)] + 1;
if (num_light > MAX_LIGHTS_PER_CLUSTER) break;
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(pixel, 0)] += 1;
let texel = this._clusterTexture.bufferIndex(pixel, Math.floor(num_light / 4.0));
let offset = num_light - Math.floor(num_light / 4.0) * 4;
this._clusterTexture.buffer[texel + offset] = i;
}
}
}


}

this._clusterTexture.update();
}
}
31 changes: 25 additions & 6 deletions src/renderers/clustered.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 { mat4, vec4, vec3 } from 'gl-matrix';
import { loadShaderProgram, renderFullscreenQuad } from '../utils';
import { NUM_LIGHTS } 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 BaseRenderer from './base';
import BaseRenderer, {MAX_LIGHTS_PER_CLUSTER} from './base';

export const NUM_GBUFFERS = 4;
export const NUM_GBUFFERS = 2;

export default class ClusteredRenderer extends BaseRenderer {
constructor(xSlices, ySlices, zSlices) {
Expand All @@ -28,9 +28,14 @@ export default class ClusteredRenderer extends BaseRenderer {
this._progShade = loadShaderProgram(QuadVertSource, fsSource({
numLights: NUM_LIGHTS,
numGBuffers: NUM_GBUFFERS,
xSlices: xSlices,
ySlices: ySlices,
zSlices: zSlices,
maxLightPerCluster: MAX_LIGHTS_PER_CLUSTER, width:canvas.width, height:canvas.height
}), {
uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]'],
attribs: ['a_uv'],
uniforms: ['u_gbuffers[0]', 'u_gbuffers[1]', 'u_gbuffers[2]', 'u_gbuffers[3]','u_lightbuffer', 'u_clusterbuffer',
'u_viewMatrix', 'u_cameraNear','u_cameraFar', 'u_cameraPos'],
attribs: ['a_position', 'a_uv'],
});

this._projectionMatrix = mat4.create();
Expand Down Expand Up @@ -154,9 +159,23 @@ export default class ClusteredRenderer extends BaseRenderer {
gl.useProgram(this._progShade.glShaderProgram);

// TODO: Bind any other shader inputs
gl.uniform1f(this._progShade.u_cameraFar, camera.far);
gl.uniform1f(this._progShade.u_cameraNear, camera.near);
gl.uniform3f(this._progShade.u_cameraPos, camera.position.x, camera.position.y, camera.position.z);
gl.uniformMatrix4fv(this._progShade.u_viewMatrix, false, this._viewMatrix);

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

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

// Bind g-buffers
const firstGBufferBinding = 0; // You may have to change this if you use other texture slots
const firstGBufferBinding = 2; // 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
11 changes: 8 additions & 3 deletions src/renderers/forwardPlus.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NUM_LIGHTS } from '../scene';
import vsSource from '../shaders/forwardPlus.vert.glsl';
import fsSource from '../shaders/forwardPlus.frag.glsl.js';
import TextureBuffer from './textureBuffer';
import BaseRenderer from './base';
import BaseRenderer, {MAX_LIGHTS_PER_CLUSTER} from './base';

export default class ForwardPlusRenderer extends BaseRenderer {
constructor(xSlices, ySlices, zSlices) {
Expand All @@ -15,9 +15,11 @@ export default class ForwardPlusRenderer extends BaseRenderer {
this._lightTexture = new TextureBuffer(NUM_LIGHTS, 8);

this._shaderProgram = loadShaderProgram(vsSource, fsSource({
numLights: NUM_LIGHTS,
numLights: NUM_LIGHTS, xSlices:xSlices, ySlices:ySlices, zSlices:zSlices,
maxLightPerCluster:MAX_LIGHTS_PER_CLUSTER, width:canvas.width, height:canvas.height,
}), {
uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer'],
uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer',
'u_viewMatrix', 'u_cameraNear','u_cameraFar'],
attribs: ['a_position', 'a_normal', 'a_uv'],
});

Expand Down Expand Up @@ -64,6 +66,7 @@ export default class ForwardPlusRenderer extends BaseRenderer {

// 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 +79,8 @@ export default class ForwardPlusRenderer extends BaseRenderer {
gl.uniform1i(this._shaderProgram.u_clusterbuffer, 3);

// TODO: Bind any other shader inputs
gl.uniform1f(this._shaderProgram.u_cameraFar, camera.far);
gl.uniform1f(this._shaderProgram.u_cameraNear, camera.near);

// 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
Loading