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: Mariano Merchante #15

Open
wants to merge 11 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
46 changes: 34 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,50 @@
WebGL Clustered Deferred and Forward+ Shading
======================

![](images/header.png)
**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)
* Mariano Merchante
* Tested on
* Microsoft Windows 10 Pro
* Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz, 2601 Mhz, 4 Core(s), 8 Logical Processor(s)
* 32.0 GB RAM
* NVIDIA GeForce GTX 1070 (mobile version)
* Chrome Version 61.0.3163.100 (Official Build) (64-bit)

### Live Online
## [Try it live!](https://mmerchante.github.io/Project5-WebGL-Clustered-Deferred-Forward-Plus/)

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

### Demo Video/GIF
A video can be found [here](https://vimeo.com/240225834)

[![](img/video.png)](TODO)
## Details

### (TODO: Your README)
This project implements both a forward+ and a clustered deferred rendering pipeline. The clustering is done in frustum space, by using the lights' bounding volumes to find the intersecting cluster cells.

*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.
![](images/grids.png)

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
It also implements a simple Phong shader in the deferred pipeline.

There's no optimization regarding light spheres and cluster collision, so there is a lot of unnecesary iterations -- this is the main probable reason for the following performance results:

![](images/perf.png)

Although different grid sizes may improve these results, the lack of intersection refinement makes each pipeline very iteration dependent, making the advantage that deferred rendering has over forward+ infinitesimal compared to the amount of iterations per pixel.

## G Buffer description

The deferred renderer uses only two buffers:

G1: [albedo | depth]

G2: [NormalXY | specularValue | specularExponent]

In the accumulation pass, the view space position is reconstructed from the depth, and the normal is also rebuilt from the encoded XY values. Initially, the texture format was RGBA8 but because of precision issues I reverted to FLOAT. Ideally, buffers should be RGBA8 and depth should be encoded in all four components of a separate buffer.

## Known issues

- There's an indexing bug that sometimes interferes with the cluster selection. It seems to be an off-by-one error.

### 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 images/grids.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 images/header.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 images/perf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 120 additions & 4 deletions src/renderers/clustered.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mat4, vec4, vec3 } from 'gl-matrix';
import { NUM_LIGHTS } from '../scene';
import TextureBuffer from './textureBuffer';

export const MAX_LIGHTS_PER_CLUSTER = 100;
export const MAX_LIGHTS_PER_CLUSTER = 511;

export default class ClusteredRenderer {
constructor(xSlices, ySlices, zSlices) {
Expand All @@ -13,10 +13,44 @@ export default class ClusteredRenderer {
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...
// returns the index as vec3
getPointClusterIndex(point, viewProjectionMatrix) {
var wsPos = vec4.create();
wsPos[0] = point[0];
wsPos[1] = point[1];
wsPos[2] = point[2];
wsPos[3] = 1.0;

var ndcPos = vec4.create();

// ndcPos = mul(viewProj, vec4(wsPos, 1.0));
vec4.transformMat4(ndcPos, wsPos, viewProjectionMatrix);

// Convert to actual ndc, [0,1]
var w = ndcPos[3];
var x = (ndcPos[0] / w) * .5 + .5;
var y = (ndcPos[1] / w) * .5 + .5;
var z = ndcPos[2] / w;

if(w < 1.0 / this._zSlices)
{
x = 0;
y = 0;
z = 0;
}
else
{
// Get cluster indices
x = x * this._xSlices;
y = y * this._ySlices;
z = z * this._zSlices;
}

return vec3.fromValues(x,y,z);
}

updateClusters(camera, viewProjectionMatrix, scene) {
// Reset the cluster texture
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 @@ -27,6 +61,88 @@ export default class ClusteredRenderer {
}
}

var aabbVectors = [];
aabbVectors.push(vec3.fromValues(1, 1, 1));
aabbVectors.push(vec3.fromValues(1, -1, -1));
aabbVectors.push(vec3.fromValues(1, 1, -1));
aabbVectors.push(vec3.fromValues(1, -1, 1));

aabbVectors.push(vec3.fromValues(-1, 1, 1));
aabbVectors.push(vec3.fromValues(-1, -1, -1));
aabbVectors.push(vec3.fromValues(-1, 1, -1));
aabbVectors.push(vec3.fromValues(-1, -1, 1));

var minBound = vec3.fromValues(0, 0, 0);
var maxBound = vec3.fromValues(this._xSlices - 1, this._ySlices - 1, this._zSlices - 1);

var viewMatrix = mat4.create();
mat4.invert(viewMatrix, camera.matrixWorld.elements);

for(let l = 0; l < scene.lights.length; ++l)
{
var lightIndex = l;
var light = scene.lights[l];
var wsLightPos = vec3.fromValues(light.position[0], light.position[1], light.position[2]);
var radius = light.radius;

var minIndex = vec3.clone(maxBound);
var maxIndex = vec3.clone(minBound);

var scaledAABB = vec3.create();
for(let v = 0; v < aabbVectors.length; v++)
{
var pos = vec3.create();
vec3.scale(scaledAABB, aabbVectors[v], radius);
vec3.add(pos, wsLightPos, scaledAABB);

var pointIndex = this.getPointClusterIndex(pos, viewProjectionMatrix);

vec3.min(minIndex, minIndex, pointIndex);
vec3.max(maxIndex, maxIndex, pointIndex);
}

// If the aabb is out of the frustum, ignore the light
if((maxIndex[0] < 0 || maxIndex[1] < 0 || maxIndex[2] < 0)
|| (minIndex[0] >= this._xSlices || minIndex[1] >= this._ySlices || minIndex[2] >= this._zSlices))
continue;

// Clamp min indices
vec3.min(minIndex, minIndex, maxBound);
vec3.max(minIndex, minIndex, minBound);
vec3.floor(minIndex, minIndex);

// Clamp max indices
vec3.min(maxIndex, maxIndex, maxBound);
vec3.max(maxIndex, maxIndex, minBound);
vec3.ceil(maxIndex, maxIndex);

for(var x = minIndex[0]; x <= maxIndex[0]; ++x)
{
for(var y = minIndex[1]; y <= maxIndex[1]; ++y)
{
for(var z = minIndex[2]; z <= maxIndex[2]; ++z)
{
var index = x + (y * this._xSlices) + (z * this._xSlices * this._ySlices);

// Get amount of lights, increment it
var clusterLightCount = this._clusterTexture.buffer[this._clusterTexture.bufferIndex(index, 0)];

if(clusterLightCount < MAX_LIGHTS_PER_CLUSTER)
{
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(index, 0)] = clusterLightCount + 1;

var clusterLightIndex = Math.floor(clusterLightCount + 1); // + 1 comes from the light count
var offset = Math.floor(clusterLightIndex / 4);
var component = Math.floor(clusterLightIndex % 4);

// Save the light index on the list
this._clusterTexture.buffer[this._clusterTexture.bufferIndex(index, offset) + component] = lightIndex;
}
}
}
}
}

this._clusterTexture.update();
}
}
39 changes: 33 additions & 6 deletions src/renderers/clusteredDeferred.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import QuadVertSource from '../shaders/quad.vert.glsl';
import fsSource from '../shaders/deferred.frag.glsl.js';
import TextureBuffer from './textureBuffer';
import ClusteredRenderer from './clustered';
import * as ClusteredConstants from './clustered';

export const NUM_GBUFFERS = 4;

Expand All @@ -21,15 +22,19 @@ 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,
numGBuffers: NUM_GBUFFERS,
xSlices: xSlices,
ySlices: ySlices,
zSlices: zSlices,
lightsPerCluster: ClusteredConstants.MAX_LIGHTS_PER_CLUSTER,
}), {
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_invProjectionMatrix'],
attribs: ['a_uv'],
});

Expand Down Expand Up @@ -124,14 +129,21 @@ 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);

// Update the buffer used to populate the texture packed with light data
for (let i = 0; i < NUM_LIGHTS; ++i) {
this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 0] = scene.lights[i].position[0];
this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 1] = scene.lights[i].position[1];
this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 2] = scene.lights[i].position[2];

// Light positions in viewspace to simplify code later
var vsPos = vec4.fromValues(scene.lights[i].position[0], scene.lights[i].position[1], scene.lights[i].position[2], 1);
vec4.transformMat4(vsPos, vsPos, this._viewMatrix);

this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 0] = vsPos[0];
this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 1] = vsPos[1];
this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 2] = vsPos[2];
this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 0) + 3] = scene.lights[i].radius;

this._lightTexture.buffer[this._lightTexture.bufferIndex(i, 1) + 0] = scene.lights[i].color[0];
Expand All @@ -142,7 +154,7 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {
this._lightTexture.update();

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

// Bind the default null framebuffer which is the screen
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
Expand All @@ -163,6 +175,21 @@ export default class ClusteredDeferredRenderer extends ClusteredRenderer {
gl.uniform1i(this._progShade[`u_gbuffers[${i}]`], i + firstGBufferBinding);
}

var invProj = mat4.create();
invProj = mat4.invert(invProj, this._projectionMatrix);
gl.uniformMatrix4fv(this._progShade.u_invProjectionMatrix, false, invProj);

// 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);


renderFullscreenQuad(this._progShade);
}
};
9 changes: 7 additions & 2 deletions src/renderers/clusteredForwardPlus.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ import vsSource from '../shaders/clusteredForward.vert.glsl';
import fsSource from '../shaders/clusteredForward.frag.glsl.js';
import TextureBuffer from './textureBuffer';
import ClusteredRenderer from './clustered';
import * as ClusteredConstants from './clustered';

export default class ClusteredForwardPlusRenderer extends ClusteredRenderer {
constructor(xSlices, ySlices, zSlices) {
super(xSlices, ySlices, zSlices);

// Create a texture to store light data
this._lightTexture = new TextureBuffer(NUM_LIGHTS, 8);

this._shaderProgram = loadShaderProgram(vsSource, fsSource({
numLights: NUM_LIGHTS,
xSlices: xSlices,
ySlices: ySlices,
zSlices: zSlices,
lightsPerCluster: ClusteredConstants.MAX_LIGHTS_PER_CLUSTER,
}), {
uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer'],
attribs: ['a_position', 'a_normal', 'a_uv'],
Expand All @@ -34,7 +39,7 @@ export default class ClusteredForwardPlusRenderer extends ClusteredRenderer {
mat4.multiply(this._viewProjectionMatrix, this._projectionMatrix, this._viewMatrix);

// Update cluster texture which maps from cluster index to light list
this.updateClusters(camera, this._viewMatrix, scene);
this.updateClusters(camera, this._viewProjectionMatrix, scene);

// Update the buffer used to populate the texture packed with light data
for (let i = 0; i < NUM_LIGHTS; ++i) {
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/textureBuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default class TextureBuffer {
* @param {*} component The ith float of an element is located in the (i/4)th pixel
*/
bufferIndex(index, component) {
return 4 * index + 4 * component * this._elementCount;
return 4 * (component * this._elementCount + index); // seriously?
}

/**
Expand Down
8 changes: 3 additions & 5 deletions src/scene.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
const MinimalGLTFLoader = require('../lib/minimal-gltf-loader');
import { gl } from './init';

// TODO: Edit if you want to change the light initial positions
export const LIGHT_MIN = [-14, 0, -6];
export const LIGHT_MIN = [-14, -5, -6];
export const LIGHT_MAX = [14, 20, 6];
export const LIGHT_RADIUS = 5.0;
export const LIGHT_DT = -0.03;
export const LIGHT_DT = -0.01;

// TODO: This controls the number of lights
export const NUM_LIGHTS = 100;
export const NUM_LIGHTS = 128;

class Scene {
constructor() {
Expand Down
Loading