diff --git a/README.md b/README.md index 94de884..93acfe8 100755 --- a/README.md +++ b/README.md @@ -3,25 +3,35 @@ 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) +* Haoyu Sui + * [LinkedIn](http://linkedin.com/in/haoyu-sui-721284192) +* Tested on: Windows 10, i5-9600K @ 3.70GHz 16GB, RTX 2070 SUPER 8GB +* SM:7.5 + +| Forward+ | Clustered Deferred and Blinn-Phong | +| ------------------------ | ----------------------- | +| ![](img/forwardplus.png) | ![](img/deferred.png) | + ### Live Online -[![](img/thumb.png)](http://TODO.github.io/Project5-WebGL-Forward-Plus-and-Clustered-Deferred) +[Live Demo](https://haoyusui.github.io/Project5-WebGL-Forward-Plus-and-Clustered-Deferred/) + +### Demo Video + +[Demo Video on Youtube](https://youtu.be/e2mTMiIlSO8) + +### Performance Analysis -### Demo Video/GIF +![](img/A1.jpg) -[![](img/video.png)](TODO) +From the figure we can see that forward+ and clustered deferred can improve performance, and as the total number of lights increases, the performance improvement becomes more obvious -### (TODO: Your README) +**2-component normals** -*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. +![](img/A2.jpg) -This assignment has a considerable amount of performance analysis compared -to implementation work. Complete the implementation early to leave time! +According to the chart, 2-component normal does not improve performance to a large extent. I think the reason may be that although 2-component normal optimizes the use of G-Buffer and reduces its space, it also adds additional computational overhead. ### Credits diff --git a/img/A1.jpg b/img/A1.jpg new file mode 100644 index 0000000..fab96fe Binary files /dev/null and b/img/A1.jpg differ diff --git a/img/A2.jpg b/img/A2.jpg new file mode 100644 index 0000000..7a3dab8 Binary files /dev/null and b/img/A2.jpg differ diff --git a/img/deferred.png b/img/deferred.png new file mode 100644 index 0000000..c9800da Binary files /dev/null and b/img/deferred.png differ diff --git a/img/forwardplus.png b/img/forwardplus.png new file mode 100644 index 0000000..734892b Binary files /dev/null and b/img/forwardplus.png differ diff --git a/src/main.js b/src/main.js index d688fde..038cf23 100755 --- a/src/main.js +++ b/src/main.js @@ -44,8 +44,8 @@ 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); diff --git a/src/renderers/base.js b/src/renderers/base.js index 8a975b9..2bc0f09 100755 --- a/src/renderers/base.js +++ b/src/renderers/base.js @@ -1,6 +1,8 @@ import TextureBuffer from './textureBuffer'; +import { NUM_LIGHTS } from '../scene.js'; +import { vec3, vec4, mat4 } from 'gl-matrix'; -export const MAX_LIGHTS_PER_CLUSTER = 100; +export const MAX_LIGHTS_PER_CLUSTER = 200; export default class BaseRenderer { constructor(xSlices, ySlices, zSlices) { @@ -25,6 +27,82 @@ export default class BaseRenderer { } } + // compute height, width, depth of frustum + let frustumDepth = camera.far - camera.near; + let frustumHeight = 2 * Math.tan(camera.fov * 0.5 * Math.PI / 180); + let frustumWidth = frustumHeight * camera.aspect; + + // compute height, width, depth of each cell + let xStride = frustumWidth / this._xSlices; + let yStride = frustumHeight / this._ySlices; + let zStride = frustumDepth / this._zSlices; + + // for each light, get the position and radius, + for (let i = 0; i < NUM_LIGHTS; ++i) { + let light = scene.lights[i]; + let lightPosTmp = vec4.fromValues(light.position[0], light.position[1], light.position[2], 1.0); + let radius = light.radius; + + let lightPos = vec4.create(); + vec4.transformMat4(lightPos, lightPosTmp, viewMatrix); + + let offset = radius * 0.25; + + let xMax = lightPos[0] + radius + offset; + let xMin = lightPos[0] - radius - offset; + let yMax = lightPos[1] + radius + offset; + let yMin = lightPos[1] - radius - offset; + let zMax = lightPos[2] + radius + offset; + let zMin = lightPos[2] - radius - offset; + + let depth = Math.max(zMin, camera.near); + let height = 2 * depth * Math.tan(camera.fov * 0.5 * Math.PI / 180); + let width = height * camera.aspect; + + // for Z + let zMinIdx = Math.floor((zMin - camera.near) / zStride); + zMinIdx = Math.max(zMinIdx, 0); + zMinIdx = Math.min(zMinIdx, this._zSlices - 1); + + let zMaxIdx = Math.floor((zMax - camera.near) / zStride); + zMaxIdx = Math.max(zMaxIdx, 0); + zMaxIdx = Math.min(zMaxIdx, this._zSlices - 1); + + // for y + let yMinIdx = Math.floor((yMin - (-1 * 0.5 * height)) / yStride); + yMinIdx = Math.max(yMinIdx, 0); + yMinIdx = Math.min(yMinIdx, this._ySlices - 1); + + let yMaxIdx = Math.floor((yMax - (-1 * 0.5 * height)) / yStride); + yMaxIdx = Math.max(yMaxIdx, 0); + yMaxIdx = Math.min(yMaxIdx, this._ySlices - 1); + + // for x + let xMinIdx = Math.floor((xMin - (-1 * 0.5 * width)) / xStride); + xMinIdx = Math.max(xMinIdx, 0); + xMinIdx = Math.min(xMinIdx, this._xSlices - 1); + + let xMaxIdx = Math.floor((xMax - (-1 * 0.5 * width)) / xStride); + xMaxIdx = Math.max(xMaxIdx, 0); + xMaxIdx = Math.min(xMaxIdx, this._xSlices - 1); + + + for (let z = zMinIdx; z <= zMaxIdx; ++z) { + for (let y = yMinIdx; y <= yMaxIdx; ++y) { + for (let x = xMinIdx; x <= xMaxIdx; ++x) { + let idx = x + y * this._xSlices + z * this._xSlices * this._ySlices; + let lightCount = this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, 0)]; + + if(lightCount < MAX_LIGHTS_PER_CLUSTER) { + lightCount++; + let offset = lightCount % 4; + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, 0)] = lightCount; + this._clusterTexture.buffer[this._clusterTexture.bufferIndex(idx, Math.floor(lightCount / 4)) + offset] = i; + } + } + } + } + } this._clusterTexture.update(); } -} \ No newline at end of file +} diff --git a/src/renderers/clusteredDeferred.js b/src/renderers/clusteredDeferred.js index f9ae494..016fdbb 100644 --- a/src/renderers/clusteredDeferred.js +++ b/src/renderers/clusteredDeferred.js @@ -8,8 +8,9 @@ import QuadVertSource from '../shaders/quad.vert.glsl'; import fsSource from '../shaders/deferred.frag.glsl.js'; import TextureBuffer from './textureBuffer'; import BaseRenderer from './base'; +import { MAX_LIGHTS_PER_CLUSTER } from './base.js'; -export const NUM_GBUFFERS = 4; +export const NUM_GBUFFERS = 3; export default class ClusteredDeferredRenderer extends BaseRenderer { constructor(xSlices, ySlices, zSlices) { @@ -28,8 +29,13 @@ export default class ClusteredDeferredRenderer extends BaseRenderer { this._progShade = loadShaderProgram(QuadVertSource, fsSource({ numLights: NUM_LIGHTS, numGBuffers: NUM_GBUFFERS, + numLightsMax: MAX_LIGHTS_PER_CLUSTER, + xSlices: xSlices, + ySlices: ySlices, + zSlices: zSlices, }), { - 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_nearFarPlane', 'u_canvasSize', 'u_cameraPosition'], attribs: ['a_uv'], }); @@ -154,9 +160,23 @@ export default class ClusteredDeferredRenderer extends BaseRenderer { gl.useProgram(this._progShade.glShaderProgram); // TODO: Bind any other shader inputs + gl.uniform2f(this._progShade.u_canvasSize, canvas.width, canvas.height); + + gl.uniform2f(this._progShade.u_nearFarPlane, camera.near, camera.far); + + gl.uniform3f(this._progShade.u_cameraPosition, camera.position.x, camera.position.y, camera.position.z); + + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this._lightTexture.glTexture); + gl.uniform1i(this._progShade.u_lightbuffer, 0); + + 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]); diff --git a/src/renderers/forwardPlus.js b/src/renderers/forwardPlus.js index a02649c..c55c267 100755 --- a/src/renderers/forwardPlus.js +++ b/src/renderers/forwardPlus.js @@ -1,4 +1,4 @@ -import { gl } from '../init'; +import { canvas, gl } from '../init'; import { mat4, vec4, vec3 } from 'gl-matrix'; import { loadShaderProgram } from '../utils'; import { NUM_LIGHTS } from '../scene'; @@ -6,6 +6,7 @@ import vsSource from '../shaders/forwardPlus.vert.glsl'; import fsSource from '../shaders/forwardPlus.frag.glsl.js'; import TextureBuffer from './textureBuffer'; import BaseRenderer from './base'; +import { MAX_LIGHTS_PER_CLUSTER } from './base.js'; export default class ForwardPlusRenderer extends BaseRenderer { constructor(xSlices, ySlices, zSlices) { @@ -16,8 +17,13 @@ export default class ForwardPlusRenderer extends BaseRenderer { this._shaderProgram = loadShaderProgram(vsSource, fsSource({ numLights: NUM_LIGHTS, + numLightsMax: MAX_LIGHTS_PER_CLUSTER, + xSlices: xSlices, + ySlices: ySlices, + zSlices: zSlices, + }), { - uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer'], + uniforms: ['u_viewProjectionMatrix', 'u_colmap', 'u_normap', 'u_lightbuffer', 'u_clusterbuffer', 'u_nearFarPlane', 'u_canvasSize'], attribs: ['a_position', 'a_normal', 'a_uv'], }); @@ -76,6 +82,9 @@ export default class ForwardPlusRenderer extends BaseRenderer { gl.uniform1i(this._shaderProgram.u_clusterbuffer, 3); // TODO: Bind any other shader inputs + gl.uniform2f(this._shaderProgram.u_canvasSize, canvas.width, canvas.height); + + gl.uniform2f(this._shaderProgram.u_nearFarPlane, camera.near, 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); diff --git a/src/shaders/deferred.frag.glsl.js b/src/shaders/deferred.frag.glsl.js index 50f1e75..59a1ccd 100644 --- a/src/shaders/deferred.frag.glsl.js +++ b/src/shaders/deferred.frag.glsl.js @@ -4,17 +4,128 @@ export default function(params) { precision highp float; uniform sampler2D u_gbuffers[${params.numGBuffers}]; + + uniform sampler2D u_lightbuffer; + uniform sampler2D u_clusterbuffer; + + uniform vec2 u_nearFarPlane; + uniform vec2 u_canvasSize; + uniform vec3 u_cameraPosition; varying vec2 v_uv; + + #define TWO_COMPONENT_NORMAL 1 + + struct Light { + vec3 position; + float radius; + vec3 color; + }; + + float ExtractFloat(sampler2D texture, int textureWidth, int textureHeight, int index, int component) { + float u = float(index + 1) / float(textureWidth + 1); + int pixel = component / 4; + float v = float(pixel + 1) / float(textureHeight + 1); + vec4 texel = texture2D(texture, vec2(u, v)); + int pixelComponent = component - pixel * 4; + if (pixelComponent == 0) { + return texel[0]; + } else if (pixelComponent == 1) { + return texel[1]; + } else if (pixelComponent == 2) { + return texel[2]; + } else if (pixelComponent == 3) { + return texel[3]; + } + } + + Light UnpackLight(int index) { + Light light; + float u = float(index + 1) / float(${params.numLights + 1}); + vec4 v1 = texture2D(u_lightbuffer, vec2(u, 0.3)); + vec4 v2 = texture2D(u_lightbuffer, vec2(u, 0.6)); + light.position = v1.xyz; + + // LOOK: This extracts the 4th float (radius) of the (index)th light in the buffer + // Note that this is just an example implementation to extract one float. + // There are more efficient ways if you need adjacent values + light.radius = ExtractFloat(u_lightbuffer, ${params.numLights}, 2, index, 3); + + light.color = v2.rgb; + return light; + } + + // Cubic approximation of gaussian curve so we falloff to exactly 0 at the light radius + float cubicGaussian(float h) { + if (h < 1.0) { + return 0.25 * pow(2.0 - h, 3.0) - pow(1.0 - h, 3.0); + } else if (h < 2.0) { + return 0.25 * pow(2.0 - h, 3.0); + } else { + return 0.0; + } + } void main() { // TODO: extract data from g buffers and do lighting - // vec4 gb0 = texture2D(u_gbuffers[0], v_uv); - // vec4 gb1 = texture2D(u_gbuffers[1], v_uv); + vec4 gb0 = texture2D(u_gbuffers[0], v_uv); + vec4 gb1 = texture2D(u_gbuffers[1], v_uv); // vec4 gb2 = texture2D(u_gbuffers[2], v_uv); // vec4 gb3 = texture2D(u_gbuffers[3], v_uv); - gl_FragColor = vec4(v_uv, 0.0, 1.0); + vec3 albedo = gb0.xyz; + vec3 position = gb1.xyz; + +#if TWO_COMPONENT_NORMAL == 1 + vec2 tmp = vec2(gb0.w, gb1.w) * 2.0 - 1.0; + vec4 nn = vec4(tmp, 1.0, -1.0); + float l = dot(nn.xyz, -nn.xyw); + nn.z = l; + nn.xy *= sqrt(l); + vec3 normal = nn.xyz * 2.0 + vec3(0.0, 0.0, -1.0); +#else + vec4 gb2 = texture2D(u_gbuffers[2], v_uv); + vec3 normal = gb2.xyz; +#endif // TWO_COMPONENT_NORMAL + + vec3 fragColor = vec3(0.0); + + int clustersNum = ${params.xSlices} * ${params.ySlices} * ${params.zSlices}; + int componentsNum = int(ceil(float(${params.numLightsMax} + 1) / 4.0)); + + int x = int(floor(gl_FragCoord.x / float(u_canvasSize.x ) * float(${params.xSlices}))); + int y = int(floor(gl_FragCoord.y / float(u_canvasSize.y) * float(${params.ySlices}))); + int z = int(floor((gl_FragCoord.z - u_nearFarPlane.x) / (u_nearFarPlane.y - u_nearFarPlane.x) * float(${params.zSlices}))); + int clusterIdx = x + y * ${params.xSlices} + z * ${params.xSlices} * ${params.ySlices}; + int lightCount = int(ExtractFloat(u_clusterbuffer, clustersNum, componentsNum, clusterIdx, 0)); + + + for (int i = 1; i <= ${params.numLights}; ++i) { + if (i > lightCount) { + break; + } + int lightIdx = int(ExtractFloat(u_clusterbuffer, clustersNum, componentsNum, clusterIdx, i)); + Light light = UnpackLight(lightIdx); + float lightDistance = distance(light.position, position); + vec3 L = (light.position - position) / lightDistance; + + float lightIntensity = cubicGaussian(2.0 * lightDistance / light.radius); + float lambertTerm = max(dot(L, normal), 0.0); + fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity); + + // blinnphong + vec3 V = normalize(u_cameraPosition - position); + vec3 H = normalize(L + V); + float specularTerm = pow( max(dot(H, normal), 0.0), 100.0); + fragColor += specularTerm * light.color * vec3(lightIntensity); + } + + const vec3 ambientLight = vec3(0.025); + fragColor += albedo * ambientLight; + + fragColor = clamp(fragColor, vec3(0.0), vec3(1.0)); + + gl_FragColor = vec4(fragColor, 1.0); } `; } \ No newline at end of file diff --git a/src/shaders/deferredToTexture.frag.glsl b/src/shaders/deferredToTexture.frag.glsl index bafc086..4a51a39 100644 --- a/src/shaders/deferredToTexture.frag.glsl +++ b/src/shaders/deferredToTexture.frag.glsl @@ -9,6 +9,8 @@ varying vec3 v_position; varying vec3 v_normal; varying vec2 v_uv; +#define TWO_COMPONENT_NORMAL 1 + vec3 applyNormalMap(vec3 geomnor, vec3 normap) { normap = normap * 2.0 - 1.0; vec3 up = normalize(vec3(0.001, 1, 0.001)); @@ -21,9 +23,16 @@ void main() { vec3 norm = applyNormalMap(v_normal, vec3(texture2D(u_normap, v_uv))); vec3 col = vec3(texture2D(u_colmap, v_uv)); - // TODO: populate your g buffer - // gl_FragData[0] = ?? - // gl_FragData[1] = ?? - // gl_FragData[2] = ?? - // gl_FragData[3] = ?? +#if TWO_COMPONENT_NORMAL == 1 + vec2 tmp = normalize(norm.xy) * (sqrt(-norm.z * 0.5 + 0.5)); + tmp = tmp * 0.5 + 0.5; + gl_FragData[0] = vec4(col, tmp.x); + gl_FragData[1] = vec4(v_position, tmp.y); + +#else + gl_FragData[0] = vec4(col, 1.0); + gl_FragData[1] = vec4(v_position, 1.0); + gl_FragData[2] = vec4(norm, 0.0); +#endif // TWO_COMPONENT_NORMAL + } \ No newline at end of file diff --git a/src/shaders/forwardPlus.frag.glsl.js b/src/shaders/forwardPlus.frag.glsl.js index 022fda7..9349b4f 100644 --- a/src/shaders/forwardPlus.frag.glsl.js +++ b/src/shaders/forwardPlus.frag.glsl.js @@ -1,6 +1,5 @@ export default function(params) { return ` - // TODO: This is pretty much just a clone of forward.frag.glsl.js #version 100 precision highp float; @@ -9,6 +8,9 @@ export default function(params) { uniform sampler2D u_normap; uniform sampler2D u_lightbuffer; + uniform vec2 u_canvasSize; + uniform vec2 u_nearFarPlane; + // TODO: Read this buffer to determine the lights influencing a cluster uniform sampler2D u_clusterbuffer; @@ -81,8 +83,23 @@ export default function(params) { vec3 fragColor = vec3(0.0); - for (int i = 0; i < ${params.numLights}; ++i) { - Light light = UnpackLight(i); + int clustersNum = ${params.xSlices} * ${params.ySlices} * ${params.zSlices}; + int componentsNum = int(ceil(float(${params.numLightsMax} + 1) / 4.0)); + + int x = int(floor(gl_FragCoord.x / float(u_canvasSize.x ) * float(${params.xSlices}))); + int y = int(floor(gl_FragCoord.y / float(u_canvasSize.y) * float(${params.ySlices}))); + int z = int(floor((gl_FragCoord.z - u_nearFarPlane.x) / (u_nearFarPlane.y - u_nearFarPlane.x) * float(${params.zSlices}))); + int clusterIdx = x + y * ${params.xSlices} + z * ${params.xSlices} * ${params.ySlices}; + int lightCount = int(ExtractFloat(u_clusterbuffer, clustersNum, componentsNum, clusterIdx, 0)); + + for (int i = 1; i <= ${params.numLights}; ++i) { + if (i > lightCount) { + break; + } + + int lightIdx = int(ExtractFloat(u_clusterbuffer, clustersNum, componentsNum, clusterIdx, i)); + + Light light = UnpackLight(lightIdx); float lightDistance = distance(light.position, v_position); vec3 L = (light.position - v_position) / lightDistance; @@ -90,6 +107,7 @@ export default function(params) { float lambertTerm = max(dot(L, normal), 0.0); fragColor += albedo * lambertTerm * light.color * vec3(lightIntensity); + } const vec3 ambientLight = vec3(0.025);