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
45 changes: 32 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,50 @@ 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)
* Xuanyi Zhou
* Tested on: **Google Chrome 86.0.4240.111 with `--enable-webgl2-compute-context --use-angle=gl --use-cmd-decoder=passthrough`** on
Windows 10, i7-9750H @ 2.60GHz 32GB, RTX 2060 6GB

### Live Online
## Demo

[![](img/thumb.png)](http://TODO.github.io/Project5-WebGL-Forward-Plus-and-Clustered-Deferred)
The scene contains 1000 lights. Click to open live demo in browser (**Note that you need to enable `webgl2-compute` in order to run the demo**).

### Demo Video/GIF
[![](img/demo.gif)](https://lukedan.github.io/Project5-WebGL-Forward-Plus-and-Clustered-Deferred/)

[![](img/video.png)](TODO)
Different debug views and Toon shading.

### (TODO: Your README)
![](img/debug.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.
## Features

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
- Proper forward+ and clustered deferred rendering using WebGL2 compute shaders
- Special command line flags are required to enable `webgl2-compte` contexts for chrome. Refer to https://github.com/9ballsyndrome/WebGL_Compute_shader/blob/master/README.md for how to enable `webgl2-compute` and to test if it's properly enabled.
- Debug views
- Debug view 1: Depth/cluster visualization
- Debug view 2: Visualization of number of lights shaded per pixel
- Debug view 3: (Only for clustered deferred) Toon shaded view
- Toon shading, using sobel filters for edge detection
- GBuffer optimization:
- `float16` for normal, `uint8` for albedo.
- World position is reconstructed from depth. This is implemented from the very beginning, so no performance comparison is given.

## Performace Analysis

### Credits
![](img/fps-numLights.png)

The major factor that impacts rendering performance is the number of lights - see the graph above for details, or `data.xlsx` for raw data. Specifically, the number of frames per second scales linearly with the number of lights. Since the scene is relatively small, any increase in the number of lights will also cause the number of lights that affect a specific pixel to increase accordingly.

In general, the clustered-deferred algorithm performs better than the forward+ algorithm, which in turn performs better than the pure forward algorithm. It can be seen from the debug view that both the cluster-deferred algorithm and the forward+ algorithm effectively reducese the number of lights that need to be visited during computation. To achieve the same memory footprint, the clustered-deferred algorithm uses a far coraser grid which results in increased number lights being visited for a large number of pixels compared to forward+. However, the clustered-deferred algorithm is able to differentiate between pixels in the same cluster with different depths and use different sets of lights, thus avoiding the worse-case scenario for forward+ around the edges of foreground objects, which is a bottleneck for the algorithm. The clustered-deferred algorithm can handle 1600 point lights while still maintaining a framerate above 60 FPS.

One G-buffer optimization I implemented is changing the albedo texture to `uint8` and the normal texture to `float16`. Since the rendering process is compute-bound, this change has very little impact on performance, as shown in the graph. However, it does save a considerable amount of memory.

I've also implemented Toon shading. The shader quantizes the final color, and filters normal and depth using Sobel filters for edge detection. Although this requires lots of texel fetches, in the end its performance impact is still minimal due to the algorithm being compute-bound. Since the performance difference is negligible, detailed analysis is omitted.

## 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
* [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)
* https://github.com/9ballsyndrome/WebGL_Compute_shader
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 data.xlsx
Binary file not shown.
Binary file added img/debug.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/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/fps-numLights.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 29 additions & 19 deletions src/init.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// TODO: Change this to enable / disable debug mode
export const DEBUG = true && process.env.NODE_ENV === 'development';
export const DEBUG = false;
export const SHOW_SPECTOR = true;

import DAT from 'dat.gui';
import WebGLDebug from 'webgl-debug';
import Stats from 'stats-js';
import { PerspectiveCamera } from 'three';
import { GeometryIdCount, PerspectiveCamera } from 'three';
import OrbitControls from 'three-orbitcontrols';
import { Spector } from 'spectorjs';

Expand All @@ -18,36 +19,45 @@ export function abort(message) {
export const canvas = document.getElementById('canvas');

// Initialize the WebGL context
const glContext = canvas.getContext('webgl');
const glContext = canvas.getContext('webgl2-compute');
if (!glContext) {
document.body = document.createElement('body');
const warning = document.createElement('p');
warning.innerText = 'Failed to create webgl2-compute context.';
warning.style.background = 'black';
warning.style.color = 'white';
document.body.appendChild(warning);
abort();
}

// Get a debug context
export const gl = DEBUG ? WebGLDebug.makeDebugContext(glContext, (err, funcName, args) => {
abort(WebGLDebug.glEnumToString(err) + ' was caused by call to: ' + funcName);
}) : glContext;

const supportedExtensions = gl.getSupportedExtensions();
const requiredExtensions = [
'OES_texture_float',
'OES_texture_float_linear',
'OES_element_index_uint',
'WEBGL_depth_texture',
'WEBGL_draw_buffers',
];
const requiredExtensions = [ 'EXT_color_buffer_float' ];

// Check that all required extensions are supported
for (let i = 0; i < requiredExtensions.length; ++i) {
if (supportedExtensions.indexOf(requiredExtensions[i]) < 0) {
throw 'Unable to load extension ' + requiredExtensions[i];
}
gl.getExtension(requiredExtensions[i])
}

// Get the maximum number of draw buffers
gl.getExtension('OES_texture_float');
gl.getExtension('OES_texture_float_linear');
gl.getExtension('OES_element_index_uint');
gl.getExtension('WEBGL_depth_texture');
export const WEBGL_draw_buffers = gl.getExtension('WEBGL_draw_buffers');
export const MAX_DRAW_BUFFERS_WEBGL = gl.getParameter(WEBGL_draw_buffers.MAX_DRAW_BUFFERS_WEBGL);
export const FORWARD = 'Forward';
export const FORWARD_PLUS = 'Forward+';
export const CLUSTERED = 'Clustered Deferred';

export const globalParams = {
renderer: FORWARD_PLUS,
_renderer: null,

updateLights: true,
debugMode: 0,
debugModeParam: 1
};

export const gui = new DAT.GUI();

Expand All @@ -60,7 +70,7 @@ stats.domElement.style.top = '0px';
document.body.appendChild(stats.domElement);

// Initialize camera
export const camera = new PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
export const camera = new PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 60);

// Initialize camera controls
export const cameraControls = new OrbitControls(camera, canvas);
Expand All @@ -80,7 +90,7 @@ function setSize(width, height) {
setSize(canvas.clientWidth, canvas.clientHeight);
window.addEventListener('resize', () => setSize(canvas.clientWidth, canvas.clientHeight));

if (DEBUG) {
if (SHOW_SPECTOR) {
const spector = new Spector();
spector.displayUI();
}
Expand Down
71 changes: 25 additions & 46 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,46 @@
import { makeRenderLoop, camera, cameraControls, gui, gl } from './init';
import {
makeRenderLoop, camera, cameraControls,
FORWARD, FORWARD_PLUS, CLUSTERED, globalParams, gui, gl
} from './init';
import ForwardRenderer from './renderers/forward';
import ForwardPlusRenderer from './renderers/forwardPlus';
import ClusteredDeferredRenderer from './renderers/clusteredDeferred';
import Scene from './scene';
import Wireframe from './wireframe';

const FORWARD = 'Forward';
const FORWARD_PLUS = 'Forward+';
const CLUSTERED = 'Clustered Deferred';

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

setRenderer(params.renderer);
setRenderer(globalParams.renderer);

function setRenderer(renderer) {
switch(renderer) {
case FORWARD:
params._renderer = new ForwardRenderer();
break;
case FORWARD_PLUS:
params._renderer = new ForwardPlusRenderer(15, 15, 15);
break;
case CLUSTERED:
params._renderer = new ClusteredDeferredRenderer(15, 15, 15);
break;
}
switch(renderer) {
case FORWARD:
globalParams._renderer = new ForwardRenderer();
break;
case FORWARD_PLUS:
globalParams._renderer = new ForwardPlusRenderer(192, 108, 15);
break;
case CLUSTERED:
globalParams._renderer = new ClusteredDeferredRenderer(48, 27, 16);
break;
}
}

gui.add(params, 'renderer', [FORWARD, FORWARD_PLUS, CLUSTERED]).onChange(setRenderer);
gui.add(globalParams, 'renderer', [FORWARD, FORWARD_PLUS, CLUSTERED]).onChange(setRenderer);
gui.add(globalParams, 'updateLights');
gui.add(globalParams, 'debugMode', 0, 5, 1);
gui.add(globalParams, 'debugModeParam', 0, 1);

const scene = new Scene();
scene.loadGLTF('models/sponza/sponza.gltf');

// LOOK: The Wireframe class is for debugging.
// 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]);

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

function render() {
scene.update();
params._renderer.render(camera, scene);

// 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);
if (globalParams.updateLights) {
scene.update();
}
globalParams._renderer.render(camera, scene);
}

makeRenderLoop(render)();
makeRenderLoop(render)();
31 changes: 6 additions & 25 deletions src/renderers/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,9 @@ import TextureBuffer from './textureBuffer';
export const MAX_LIGHTS_PER_CLUSTER = 100;

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;
}
}
}

this._clusterTexture.update();
}
}
constructor(xSlices, ySlices, zSlices) {
this._xSlices = xSlices;
this._ySlices = ySlices;
this._zSlices = zSlices;
}
}
Loading