diff --git a/README.md b/README.md index 4103e3b5..de2d36bd 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,97 @@ WebGL Forward+ and Clustered Deferred Shading **University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 4** -* (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) +* Christina Qiu + * [LinkedIn](https://www.linkedin.com/in/christina-qiu-6094301b6/), [personal website](https://christinaqiu3.github.io/), [twitter](), etc. +* Tested on: Windows 11, Intel Core i7-13700H @ 2.40GHz, 16GB RAM, NVIDIA GeForce RTX 4060 Laptop GPU (Personal laptop) ### Live Demo -[![](img/thumb.png)](http://TODO.github.io/Project4-WebGPU-Forward-Plus-and-Clustered-Deferred) +[![]()](http://christinaqiu3.com/Project4-WebGPU-Forward-Plus-and-Clustered-Deferred) ### Demo Video/GIF -[![](img/video.mp4)](TODO) +[![](hw_4_1-1.gif)] + +(30+ second video/gif of your project running) + +## OVERVIEW + +This project implements a small GPU renderer in WebGPU with three progressively more advanced rendering methods and the supporting infrastructure needed to compare them. + +### Naive renderer + +Implemented a functional rasterization-based naive pipeline as a baseline. Key work: created and uploaded a camera view–projection uniform buffer from camera.ts, added the buffer to a bind group used by the naive pipeline, and updated the vertex shader to transform vertices with the view-proj matrix. This exposes the basic host→GPU data flow (create buffer, write buffer, bind group, pipeline layout, shader use) and provides a reference image and timing for later comparisons. + +### Forward+ (clustered forward shading) + +Implemented tiled (X × Y × Z) clustering of the camera frustum on the GPU via a compute shader. For each cluster we compute view-space AABB bounds (logarithmic Z slicing), test all scene lights for overlap, and store light indices and counts in a cluster buffer. During fragment shading the shader looks up the fragment’s cluster and only accumulates the lights assigned to that cluster. This greatly reduces per-fragment light loops for scenes with many lights. + +Important implementation notes: + +* Camera uniforms include invProj/view matrices and near/far to enable NDC → view unprojection in the compute shader. + +* Cluster storage is a tightly packed structured buffer: a light count, fixed-size light index array, and cluster AABB. Host-side allocation mirrors WGSL alignment and padding. + +* The clustering compute pass is dispatched once per frame before the G-buffer / shading passes. + +### Clustered Deferred (G-buffer + fullscreen lighting) + +Reused the Forward+ clustering to build a deferred lighting pipeline: first render geometry into a G-buffer (position, normal, albedo), then run a fullscreen pass that samples the G-buffer and accumulates lights from the fragment’s cluster (using the same cluster buffer). This separates geometry shading from expensive per-light lighting and decouples material evaluation from lighting accumulation. + +### Notes + +* Memory layout and alignment: when creating structured storage/uniform buffers for WGSL, the host layout must match WGSL’s std140-like packing rules. I checked field offsets and padded host buffers so device.createBindGroup and shader reads align correctly. + +* Logarithmic Z slicing: implemented the usual log-based formula for slice boundaries to balance near/far precision and avoid excessive near-plane clustering. + +* Light culling: cluster tests use sphere-vs-AABB intersection in view space for a conservative inclusion test. + +* Render pass correctness: G-buffer (3 targets) and fullscreen (swapchain) use separate render passes; pipeline fragment target formats must match the render pass attachments exactly. + +* Tradeoffs: Forward+/Clustered Deferred pay a setup cost (clustering compute pass and extra buffers) and are most beneficial when scene light counts are large. For scenes with few lights, the clustering overhead can outweigh benefits. + +## Performance Analysis + +### Forward+ vs. Clustered Deferred Shading + + + +In my implementation, Forward+ shading consistently ran faster than Clustered Deferred for moderate to high light counts. The Clustered Deferred method would be slightly more efficient when the fragment shading workload is heavy (e.g., expensive BRDFs or high material variation), since lighting is decoupled from geometry and done in a fullscreen pass. + +Naive shading (for baseline): 14FPS when numlights = 500. 1FPS when numlights = 5000. + +* Slowest under many lights because every fragment loops over all lights globally. + +Forward+ average frame time: 60FPS when numlights = 500. 20 FPS when numlights = 5000. + +* Performs lighting in a single geometry pass, writing directly to the framebuffer. This avoids the cost of: + * Creating and writing large G-buffers (position, normal, albedo textures). + * Performing a second fullscreen pass to read and combine them. + +Clustered Deferred average frame time: 11FPS when numlights = 500. 11FPS when numlights = 5000. + +* Due to multiple render passes, and each G-buffer write/read consumes GPU memory bandwidth. +* Clustered Deferred becomes advantageous when: + * Materials are complex (many parameters or procedural shading). Deferred shading stores pre-shaded attributes and performs lighting only once in screen space. + * The number of lights is extremely large + + +Performance Differences + +* Memory Bandwidth: Deferred shading writes multiple 16-bit/32-bit render targets per fragment, which is expensive even if lighting is cheaper. Forward+ skips that entirely. + +* Fill Rate: Deferred pipelines are more fill-rate–limited since every pixel is written 3–4 times per G-buffer attachment. + +* Depth Culling: Forward+ benefits from early-Z rejection before lighting; deferred pipelines shade all visible pixels regardless. + +* Lighting Complexity: Both use the same clustered light indexing, so the per-fragment lighting cost scales equally. The main difference is whether that lighting happens once or after a G-buffer stage. + + + -### (TODO: Your README) -*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. -This assignment has a considerable amount of performance analysis compared -to implementation work. Complete the implementation early to leave time! ### Credits @@ -30,3 +102,30 @@ to implementation work. Complete the implementation early to leave time! - [dat.GUI](https://github.com/dataarts/dat.gui) - [stats.js](https://github.com/mrdoob/stats.js) - [wgpu-matrix](https://github.com/greggman/wgpu-matrix) +- [Ortiz Blog](https://www.aortiz.me/2018/12/21/CG.html#forward-shading) + + + + +Optimize your TypeScript and/or WGSL code. Chrome's profiling tools are useful for this. For each change that improves performance, show the before and after render times. + + + +For each new effect feature (required or extra), please provide the following analysis: + +Concise overview and explanation of the feature. +Performance change due to adding the feature. +If applicable, how do parameters (such as number of lights, number of tiles, etc.) affect performance? Show data with graphs. +Show timing in milliseconds, not FPS. +If you did something to accelerate the feature, what did you do and why? +How might this feature be optimized beyond your current implementation? +For each performance feature (required or extra), please provide: + +Concise overview and explanation of the feature. +Detailed performance improvement analysis of adding the feature. +What is the best case scenario for your performance improvement? What is the worst? Explain briefly. +Are there tradeoffs to this performance feature? Explain briefly. +How do parameters (such as number of lights, number of tiles, etc.) affect performance? Show data with graphs. +Show timing in milliseconds, not FPS. +Show debug views when possible. +If the debug view correlates with performance, explain how. \ No newline at end of file diff --git a/Screenshot 2025-10-18 234605.png b/Screenshot 2025-10-18 234605.png new file mode 100644 index 00000000..e396083d Binary files /dev/null and b/Screenshot 2025-10-18 234605.png differ diff --git a/hw_4_1-1.gif b/hw_4_1-1.gif new file mode 100644 index 00000000..9b63faaf Binary files /dev/null and b/hw_4_1-1.gif differ diff --git a/hw_4_1.gif b/hw_4_1.gif new file mode 100644 index 00000000..9b63faaf Binary files /dev/null and b/hw_4_1.gif differ diff --git a/src/main.ts b/src/main.ts index fa689254..4759c673 100644 --- a/src/main.ts +++ b/src/main.ts @@ -50,7 +50,7 @@ function setRenderer(mode: string) { } const renderModes = { naive: 'naive', forwardPlus: 'forward+', clusteredDeferred: 'clustered deferred' }; -let renderModeController = gui.add({ mode: renderModes.naive }, 'mode', renderModes); +let renderModeController = gui.add({ mode: renderModes.forwardPlus}, 'mode', renderModes); renderModeController.onChange(setRenderer); setRenderer(renderModeController.getValue()); diff --git a/src/renderers/clustered_deferred.ts b/src/renderers/clustered_deferred.ts index 00a326ca..978e186f 100644 --- a/src/renderers/clustered_deferred.ts +++ b/src/renderers/clustered_deferred.ts @@ -6,11 +6,218 @@ export class ClusteredDeferredRenderer extends renderer.Renderer { // TODO-3: add layouts, pipelines, textures, etc. needed for Forward+ here // you may need extra uniforms such as the camera view matrix and the canvas resolution + sceneUniformsBindGroupLayout: GPUBindGroupLayout; + sceneUniformsBindGroup: GPUBindGroup; + pipeline: GPURenderPipeline; + + + + // added texture buffers + positionTexture: GPUTexture; + positionTextureView: GPUTextureView; + normalTexture: GPUTexture; + normalTextureView: GPUTextureView; + albedoTexture: GPUTexture; + albedoTextureView: GPUTextureView; + depthTexture: GPUTexture; + depthTextureView: GPUTextureView; + + deferredBindGroupLayout: GPUBindGroupLayout; + deferredBindGroup: GPUBindGroup; + deferredPipeline: GPURenderPipeline; + constructor(stage: Stage) { super(stage); // TODO-3: initialize layouts, pipelines, textures, etc. needed for Forward+ here // you'll need two pipelines: one for the G-buffer pass and one for the fullscreen pass + + this.positionTexture = renderer.device.createTexture({ + size: [renderer.canvas.width, renderer.canvas.height], + format: "rgba16float", + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING + }); + this.positionTextureView = this.positionTexture.createView(); + + this.normalTexture = renderer.device.createTexture({ + size: [renderer.canvas.width, renderer.canvas.height], + format: "rgba16float", + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING + }); + this.normalTextureView = this.normalTexture.createView(); + + this.albedoTexture = renderer.device.createTexture({ + size: [renderer.canvas.width, renderer.canvas.height], + format: renderer.canvasFormat, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING + }); + this.albedoTextureView = this.albedoTexture.createView(); + + this.depthTexture = renderer.device.createTexture({ + size: [renderer.canvas.width, renderer.canvas.height], + format: "depth24plus", + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING + }); + this.depthTextureView = this.depthTexture.createView(); + + + + + this.sceneUniformsBindGroupLayout = renderer.device.createBindGroupLayout({ + label: "forward+ scene uniforms bind group layout", + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: "uniform" } + }, + { // lightSet + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + buffer: { type: "read-only-storage" }, + }, + { // cluster + binding: 2, + visibility: GPUShaderStage.FRAGMENT, + buffer: {type: "read-only-storage"}, + } + ] + }); + this.sceneUniformsBindGroup = renderer.device.createBindGroup({ + label: "forward+ scene uniforms bind group", + layout: this.sceneUniformsBindGroupLayout, + entries: [ + { + binding: 0, + resource: { buffer: this.camera.uniformsBuffer } + }, + { // lightSet + binding: 1, + resource: { buffer: this.lights.lightSetStorageBuffer } + }, + { // cluster + binding: 2, + resource: { buffer: this.lights.clusterBuffer } + } + ] + }); + + + + this.pipeline = renderer.device.createRenderPipeline({ + layout: renderer.device.createPipelineLayout({ + label: "forward+ pipeline layout", + bindGroupLayouts: [ + this.sceneUniformsBindGroupLayout, + renderer.modelBindGroupLayout, + renderer.materialBindGroupLayout + ] + }), + depthStencil: { + depthWriteEnabled: true, + depthCompare: "less", + format: "depth24plus" + }, + vertex: { + module: renderer.device.createShaderModule({ + label: "forward+ vert shader", + code: shaders.naiveVertSrc + }), + buffers: [ renderer.vertexBufferLayout ] + }, + fragment: { + module: renderer.device.createShaderModule({ + label: "forward+ frag shader", + code: shaders.clusteredDeferredFragSrc, + }), + targets: [ + { + format: "rgba16float" + }, + { + format: "rgba16float" + }, + { + format: renderer.canvasFormat, + } + ] + } + + + }); + + this.deferredBindGroupLayout = renderer.device.createBindGroupLayout({ + label: "deferred fullscreen pass bind group layout", + entries: [ + { + binding: 0,// position + visibility: GPUShaderStage.FRAGMENT, + texture: {} + }, + { + binding: 1,// normal + visibility: GPUShaderStage.FRAGMENT, + texture: {} + }, + { + binding: 2,// albedo + visibility: GPUShaderStage.FRAGMENT, + texture: {} + }, + ] + }); + this.deferredBindGroup = renderer.device.createBindGroup({ + label: "deferred fullscreen pass bind group", + layout: this.deferredBindGroupLayout, + entries: [ + { + binding: 0, + resource: this.positionTextureView + }, + { + binding: 1, + resource: this.normalTextureView + }, + { + binding: 2, + resource: this.albedoTextureView + }, + ] + }); + this.deferredPipeline = renderer.device.createRenderPipeline({ + layout: renderer.device.createPipelineLayout({ + label: "deferred pipeline layout", + bindGroupLayouts: [ + // check ordering? + this.sceneUniformsBindGroupLayout, //group 0 + this.deferredBindGroupLayout // group 1 + ] + }), + depthStencil: { + depthWriteEnabled: false, + depthCompare: "less", + format: "depth24plus" + }, + vertex: { + module: renderer.device.createShaderModule({ + label: "deferred vert shader", + code: shaders.clusteredDeferredFullscreenVertSrc + }), + entryPoint: "main", + }, + fragment: { + module: renderer.device.createShaderModule({ + label: "deferred frag shader", + code: shaders.clusteredDeferredFullscreenFragSrc, + }), + entryPoint: "main", + targets: [ + { + format: renderer.canvasFormat, + } + ] + } + }); } override draw() { @@ -18,5 +225,78 @@ export class ClusteredDeferredRenderer extends renderer.Renderer { // - run the clustering compute shader // - run the G-buffer pass, outputting position, albedo, and normals // - run the fullscreen pass, which reads from the G-buffer and performs lighting calculations + + const encoder = renderer.device.createCommandEncoder(); + this.lights.doLightClustering(encoder); + const canvasTextureView = renderer.context.getCurrentTexture().createView(); + + const renderPass = encoder.beginRenderPass({ + label: "forward+ render pass", + colorAttachments: [ + { + view: this.positionTextureView, + loadOp: "clear", + storeOp: "store", + clearValue: [0, 0, 0, 1], + }, + { + view: this.normalTextureView, + loadOp: "clear", + storeOp: "store", + clearValue: [0, 0, 0, 1], + }, + { + view: this.albedoTextureView, + loadOp: "clear", + storeOp: "store", + clearValue: [0, 0, 0, 1], + }, + ], + depthStencilAttachment: { + view: this.depthTextureView, + depthClearValue: 1.0, + depthLoadOp: "clear", + depthStoreOp: "store", + } + }); + renderPass.setPipeline(this.pipeline); + renderPass.setBindGroup(shaders.constants.bindGroup_scene, this.sceneUniformsBindGroup); // bindgroup_scene + + this.scene.iterate(node => { + renderPass.setBindGroup(shaders.constants.bindGroup_model, node.modelBindGroup); // bindgroup_model + }, material => { + renderPass.setBindGroup(shaders.constants.bindGroup_material, material.materialBindGroup); // bindgroup_material + }, primitive => { + renderPass.setVertexBuffer(0, primitive.vertexBuffer); + renderPass.setIndexBuffer(primitive.indexBuffer, 'uint32'); + renderPass.drawIndexed(primitive.numIndices); + } + ); + renderPass.end(); + + const fullscreenPass = encoder.beginRenderPass({ + label: "deferred fullscreen render pass", + colorAttachments: [ + { + view: canvasTextureView, + clearValue: [0, 0, 0, 0], + loadOp: "clear", + storeOp: "store" + } + ], + depthStencilAttachment: { + view: this.depthTextureView, + depthClearValue: 1.0, + depthLoadOp: "clear", + depthStoreOp: "store", + } + }); + fullscreenPass.setPipeline(this.deferredPipeline); + fullscreenPass.setBindGroup(shaders.constants.bindGroup_scene, this.sceneUniformsBindGroup); + fullscreenPass.setBindGroup(1, this.deferredBindGroup); // bindgroup_textures + fullscreenPass.draw(6); + fullscreenPass.end(); + + renderer.device.queue.submit([encoder.finish()]); } } diff --git a/src/renderers/forward_plus.ts b/src/renderers/forward_plus.ts index 471796fd..73cbefdc 100644 --- a/src/renderers/forward_plus.ts +++ b/src/renderers/forward_plus.ts @@ -6,15 +6,143 @@ export class ForwardPlusRenderer extends renderer.Renderer { // TODO-2: add layouts, pipelines, textures, etc. needed for Forward+ here // you may need extra uniforms such as the camera view matrix and the canvas resolution + sceneUniformsBindGroupLayout: GPUBindGroupLayout; + sceneUniformsBindGroup: GPUBindGroup; + + depthTexture: GPUTexture; + depthTextureView: GPUTextureView; + + pipeline: GPURenderPipeline; + constructor(stage: Stage) { super(stage); // TODO-2: initialize layouts, pipelines, textures, etc. needed for Forward+ here + this.sceneUniformsBindGroupLayout = renderer.device.createBindGroupLayout({ + label: "forward+ scene uniforms bind group layout", + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT, + buffer: { type: "uniform" } + }, + { // lightSet + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + buffer: { type: "read-only-storage" }, + }, + { // cluster + binding: 2, + visibility: GPUShaderStage.FRAGMENT, + buffer: {type: "read-only-storage"}, + } + ] + }); + + this.sceneUniformsBindGroup = renderer.device.createBindGroup({ + label: "forward+ scene uniforms bind group", + layout: this.sceneUniformsBindGroupLayout, + entries: [ + { + binding: 0, + resource: { buffer: this.camera.uniformsBuffer } + }, + { // lightSet + binding: 1, + resource: { buffer: this.lights.lightSetStorageBuffer } + }, + { // cluster + binding: 2, + resource: { buffer: this.lights.clusterBuffer } + } + ] + }); + + this.depthTexture = renderer.device.createTexture({ + size: [renderer.canvas.width, renderer.canvas.height], + format: "depth24plus", + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING + }); + this.depthTextureView = this.depthTexture.createView(); + + this.pipeline = renderer.device.createRenderPipeline({ + layout: renderer.device.createPipelineLayout({ + label: "forward+ pipeline layout", + bindGroupLayouts: [ + this.sceneUniformsBindGroupLayout, + renderer.modelBindGroupLayout, + renderer.materialBindGroupLayout + ] + }), + depthStencil: { + depthWriteEnabled: true, + depthCompare: "less", + format: "depth24plus" + }, + vertex: { + module: renderer.device.createShaderModule({ + label: "forward+ vert shader", + code: shaders.naiveVertSrc + }), + buffers: [ renderer.vertexBufferLayout ] + }, + fragment: { + module: renderer.device.createShaderModule({ + label: "forward+ frag shader", + code: shaders.forwardPlusFragSrc, + }), + targets: [ + { + format: renderer.canvasFormat, + } + ] + } + }); } override draw() { // TODO-2: run the Forward+ rendering pass: // - run the clustering compute shader // - run the main rendering pass, using the computed clusters for efficient lighting + const encoder = renderer.device.createCommandEncoder(); + this.lights.doLightClustering(encoder); + const canvasTextureView = renderer.context.getCurrentTexture().createView(); + + const renderPass = encoder.beginRenderPass({ + label: "forward+ render pass", + colorAttachments: [ + { + view: canvasTextureView, + clearValue: { r: 0.3, g: 0.3, b: 0.3, a: 1.0 }, + loadOp: "clear", + storeOp: "store", + } + ], + depthStencilAttachment: { + view: this.depthTextureView, + depthClearValue: 1.0, + depthLoadOp: "clear", + depthStoreOp: "store", + } + }); + renderPass.setPipeline(this.pipeline); + + renderPass.setBindGroup(shaders.constants.bindGroup_scene, this.sceneUniformsBindGroup); + + this.scene.iterate(node => { + renderPass.setBindGroup(shaders.constants.bindGroup_model, node.modelBindGroup); + }, material => { + renderPass.setBindGroup(shaders.constants.bindGroup_material, material.materialBindGroup); + }, primitive => { + renderPass.setVertexBuffer(0, primitive.vertexBuffer); + renderPass.setIndexBuffer(primitive.indexBuffer, 'uint32'); + renderPass.drawIndexed(primitive.numIndices); + }); + + renderPass.end(); + + renderer.device.queue.submit([encoder.finish()]); + + // this.lights.readClusterCounts(renderer.device, renderer.device.queue, this.lights.clusterBuffer); } } diff --git a/src/renderers/naive.ts b/src/renderers/naive.ts index 0bf82417..75f525dc 100644 --- a/src/renderers/naive.ts +++ b/src/renderers/naive.ts @@ -18,10 +18,15 @@ export class NaiveRenderer extends renderer.Renderer { label: "scene uniforms bind group layout", entries: [ // TODO-1.2: add an entry for camera uniforms at binding 0, visible to only the vertex shader, and of type "uniform" + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: { type: "uniform" } + }, { // lightSet binding: 1, visibility: GPUShaderStage.FRAGMENT, - buffer: { type: "read-only-storage" } + buffer: { type: "read-only-storage" }, } ] }); @@ -34,6 +39,10 @@ export class NaiveRenderer extends renderer.Renderer { // you can access the camera using `this.camera` // if you run into TypeScript errors, you're probably trying to upload the host buffer instead { + binding: 0, + resource: { buffer: this.camera.uniformsBuffer } + }, + { // lightSet binding: 1, resource: { buffer: this.lights.lightSetStorageBuffer } } @@ -106,6 +115,7 @@ export class NaiveRenderer extends renderer.Renderer { renderPass.setPipeline(this.pipeline); // TODO-1.2: bind `this.sceneUniformsBindGroup` to index `shaders.constants.bindGroup_scene` + renderPass.setBindGroup(shaders.constants.bindGroup_scene, this.sceneUniformsBindGroup); this.scene.iterate(node => { renderPass.setBindGroup(shaders.constants.bindGroup_model, node.modelBindGroup); diff --git a/src/shaders/clustered_deferred.fs.wgsl b/src/shaders/clustered_deferred.fs.wgsl index 4e86f573..9e967c5f 100644 --- a/src/shaders/clustered_deferred.fs.wgsl +++ b/src/shaders/clustered_deferred.fs.wgsl @@ -1,3 +1,31 @@ // TODO-3: implement the Clustered Deferred G-buffer fragment shader // This shader should only store G-buffer information and should not do any shading. +@group(${bindGroup_material}) @binding(0) var diffuseTex: texture_2d; +@group(${bindGroup_material}) @binding(1) var diffuseTexSampler: sampler; + +struct FragmentInput { + @location(0) pos: vec3f, + @location(1) nor: vec3f, + @location(2) uv: vec2f, +} + +struct FragmentOutput { + @location(0) position: vec4f, + @location(1) normal: vec4f, + @location(2) albedo: vec4f, +} + +@fragment +fn main(in: FragmentInput) -> FragmentOutput { + let diffuseColor = textureSample(diffuseTex, diffuseTexSampler, in.uv); + if (diffuseColor.a < 0.5f) { + discard; + } + + var out: FragmentOutput; + out.position = vec4f(in.pos, 1.0); + out.normal = vec4f(normalize(in.nor), 1.0); + out.albedo = diffuseColor; + return out; +} diff --git a/src/shaders/clustered_deferred_fullscreen.fs.wgsl b/src/shaders/clustered_deferred_fullscreen.fs.wgsl index 68235c41..5acbbc09 100644 --- a/src/shaders/clustered_deferred_fullscreen.fs.wgsl +++ b/src/shaders/clustered_deferred_fullscreen.fs.wgsl @@ -1,3 +1,53 @@ // TODO-3: implement the Clustered Deferred fullscreen fragment shader // Similar to the Forward+ fragment shader, but with vertex information coming from the G-buffer instead. + +@group (${bindGroup_scene}) @binding(0) var cameraUniforms: CameraUniforms; +@group (${bindGroup_scene}) @binding(1) var lightSet: LightSet; +@group (${bindGroup_scene}) @binding(2) var clusterSet: ClusterSet; + +@group (1) @binding(0) var positionTexture: texture_2d;// deffered bind group layout +@group (1) @binding(1) var normalTexture: texture_2d; +@group (1) @binding(2) var albedoTexture: texture_2d; + +@fragment +fn main(@builtin(position) fragPos: vec4) -> @location(0) vec4 { + let uv = fragPos.xy / cameraUniforms.canvasResolution; + + // Fetch G-buffer data + let worldPos = textureLoad(positionTexture, vec2(fragPos.xy), 0).xyz; + let normal = normalize(textureLoad(normalTexture, vec2(fragPos.xy), 0).xyz); + let albedo = textureLoad(albedoTexture, vec2(fragPos.xy), 0); + + // Determine cluster index + let clusterDims = vec3(${numClustersX}, ${numClustersY}, ${numClustersZ}); + + let viewPos = cameraUniforms.viewMat * vec4f(worldPos, 1.0); + let viewProj = cameraUniforms.viewProjMat * vec4f(worldPos, 1.0); + let ndcPos = (viewProj.xyz / viewProj.w) * 0.5 + vec3f(0.5, 0.5, 0.5); + let clusterX = u32(clamp(ndcPos.x * f32(clusterDims.x), 0.0, f32(clusterDims.x - 1u))); + let clusterY = u32(clamp(ndcPos.y * f32(clusterDims.y), 0.0, f32(clusterDims.y - 1u))); + + // Logarithmic depth slicing + let near = cameraUniforms.nearPlane; + let far = cameraUniforms.farPlane; + let viewDepth = -viewPos.z; + let logDepth = log(far/near); + let zSliceF = clamp(floor(log(-viewPos.z/near) / logDepth * f32(clusterDims.z)), 0.0, f32(clusterDims.z - 1u)); + let clusterZ = u32(zSliceF); + + let clusterIdx: u32 = clusterX + clusterY * clusterDims.x + clusterZ * clusterDims.x * clusterDims.y; + + var totalLightContrib = vec3f(0, 0, 0); + + let cluster = clusterSet.clusters[clusterIdx]; + for (var i: u32 = 0u; i < cluster.numLights; i = i + 1u) { + let lightIdx = cluster.lightIndices[i]; + let light = lightSet.lights[lightIdx]; + + totalLightContrib += calculateLightContrib(light, worldPos, normal); + } + + var finalColor = albedo.rgb * totalLightContrib; + return vec4(finalColor, 1); +} \ No newline at end of file diff --git a/src/shaders/clustered_deferred_fullscreen.vs.wgsl b/src/shaders/clustered_deferred_fullscreen.vs.wgsl index 1e43a884..d1a00703 100644 --- a/src/shaders/clustered_deferred_fullscreen.vs.wgsl +++ b/src/shaders/clustered_deferred_fullscreen.vs.wgsl @@ -1,3 +1,14 @@ // TODO-3: implement the Clustered Deferred fullscreen vertex shader // This shader should be very simple as it does not need all of the information passed by the the naive vertex shader. + +@vertex +fn main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4 { + var pos = array, 3>( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0) + ); + + return vec4(pos[vertexIndex], 0.0, 1.0); +} \ No newline at end of file diff --git a/src/shaders/clustering.cs.wgsl b/src/shaders/clustering.cs.wgsl index 575d6e5a..b6b3b00d 100644 --- a/src/shaders/clustering.cs.wgsl +++ b/src/shaders/clustering.cs.wgsl @@ -9,15 +9,99 @@ // - Convert these screen and depth bounds into view-space coordinates. // - Store the computed bounding box (AABB) for the cluster. -// ------------------------------------ -// Assigning lights to clusters: -// ------------------------------------ -// For each cluster: -// - Initialize a counter for the number of lights in this cluster. +@group(${bindGroup_scene}) @binding(0) var cameraUniforms: CameraUniforms; +@group(${bindGroup_scene}) @binding(1) var lightSet: LightSet; +@group(${bindGroup_scene}) @binding(2) var clusterSet: ClusterSet; + +// Unproject an NDC point (x,y,z in [-1,1]) to view space using invProj. +fn screenToViewSpace(screenCoord: vec2, depthNDC: f32) -> vec3 { + let ndcX = (screenCoord.x / cameraUniforms.canvasResolution.x) * 2.0 - 1.0; + let ndcY = 1.0 - (screenCoord.y / cameraUniforms.canvasResolution.y) * 2.0; + let ndc = vec4(ndcX, ndcY, depthNDC, 1.0); + var viewPos = cameraUniforms.invProjMat * ndc; + viewPos /= viewPos.w; + return viewPos.xyz; +} + +// Sphere-AABB intersection test (view-space) +fn sphereIntersectsAABB(center: vec3, r: f32, aMin: vec3, aMax: vec3) -> bool { + let closest = clamp(center, aMin, aMax); + let distSq = dot(closest - center, closest - center); + return distSq <= r * r; +} + +fn lineIntersectionPlane(a: vec3f, b: vec3f, planeZ: f32) -> vec3f { + let ab = b - a; + let t = (planeZ - a.z) / ab.z; + return a + t * ab; +} + +@compute +@workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) gid: vec3) { + // Cluster grid dimensions + let clusterDims = vec3(${numClustersX}, ${numClustersY}, ${numClustersZ}); + if (gid.x >= clusterDims.x || gid.y >= clusterDims.y || gid.z >= clusterDims.z) { + return; + } + + let clusterIdx: u32 = gid.x + gid.y * clusterDims.x + gid.z * clusterDims.x * clusterDims.y; + + // Calculate screen-space bounds for this cluster + let x0 = f32(gid.x) / f32(clusterDims.x) * cameraUniforms.canvasResolution.x; + let x1 = f32(gid.x + 1u) / f32(clusterDims.x) * cameraUniforms.canvasResolution.x; + let y0 = f32(gid.y) / f32(clusterDims.y) * cameraUniforms.canvasResolution.y; + let y1 = f32(gid.y + 1u) / f32(clusterDims.y) * cameraUniforms.canvasResolution.y; + + // Logarithmic depth slicing + let near = cameraUniforms.nearPlane; + let far = cameraUniforms.farPlane; + let zSlice = f32(gid.z) / f32(clusterDims.z); + let zSlice1 = f32(gid.z + 1u) / f32(clusterDims.z); + + let zNear = -near * pow(far / near, zSlice); + let zFar = -near * pow(far / near, zSlice1); + + // Convert to view-space bounding box corners + let clusterMinNear = screenToViewSpace(vec2(x0, y0), 0.0); + let clusterMaxFar = screenToViewSpace(vec2(x1, y1), 0.0); + + let eye = vec3f(0, 0, 0); + let minPointNear = lineIntersectionPlane(eye, clusterMinNear, zNear); + let minPointFar = lineIntersectionPlane(eye, clusterMinNear, zFar); + let maxPointNear = lineIntersectionPlane(eye, clusterMaxFar, zNear); + let maxPointFar = lineIntersectionPlane(eye, clusterMaxFar, zFar); + + let minBBox = vec3( + min(clusterMinNear.x, clusterMaxFar.x), + min(clusterMinNear.y, clusterMaxFar.y), + min(zNear, zFar) + ); + + let maxBBox = vec3( + max(clusterMinNear.x, clusterMaxFar.x), + max(clusterMinNear.y, clusterMaxFar.y), + max(zNear, zFar) + ); + + //let minBBox = min(min(minPointNear, minPointFar), min(maxPointNear, maxPointFar)); + //let maxBBox = max(max(minPointNear, minPointFar), max(maxPointNear, maxPointFar)); + + // Assign lights to this cluster + var counter: u32 = 0u; + for (var lightIdx: u32 = 0u; lightIdx < lightSet.numLights; lightIdx++) { + if (counter >= ${maxLightsPerCluster}u) { + break; + } + + let light = lightSet.lights[lightIdx]; + let lightPosView = (cameraUniforms.viewMat * vec4(light.pos, 1.0)).xyz; -// For each light: -// - Check if the light intersects with the cluster’s bounding box (AABB). -// - If it does, add the light to the cluster's light list. -// - Stop adding lights if the maximum number of lights is reached. + if (sphereIntersectsAABB(lightPosView, 2.f, minBBox, maxBBox)) { // use 2.f instead of lightRadius! + clusterSet.clusters[clusterIdx].lightIndices[counter] = lightIdx; + counter += 1u; + } + } -// - Store the number of lights assigned to this cluster. + clusterSet.clusters[clusterIdx].numLights = counter; +} \ No newline at end of file diff --git a/src/shaders/common.wgsl b/src/shaders/common.wgsl index 738e9c4e..c19d0fc7 100644 --- a/src/shaders/common.wgsl +++ b/src/shaders/common.wgsl @@ -12,13 +12,30 @@ struct LightSet { // TODO-2: you may want to create a ClusterSet struct similar to LightSet +struct Cluster { + numLights: u32, + lightIndices: array +} + +struct ClusterSet { + numClusters: u32, + clusters: array // each cluster holds the number of lights affecting it +} + struct CameraUniforms { // TODO-1.3: add an entry for the view proj mat (of type mat4x4f) + viewProjMat: mat4x4, + nearPlane: f32, + farPlane: f32, + invProjMat : mat4x4, // inverse (clip -> view) + viewMat : mat4x4, // view matrix (world -> view) + invViewMat : mat4x4, // inverse view (view -> world) + canvasResolution: vec2 // canvas width and height } // CHECKITOUT: this special attenuation function ensures lights don't affect geometry outside the maximum light radius fn rangeAttenuation(distance: f32) -> f32 { - return clamp(1.f - pow(distance / ${lightRadius}, 4.f), 0.f, 1.f) / (distance * distance); + return clamp(1.f - pow(distance / 2.f, 4.f), 0.f, 1.f) / (distance * distance); // put 2.f instead of lightRadius! } fn calculateLightContrib(light: Light, posWorld: vec3f, nor: vec3f) -> vec3f { diff --git a/src/shaders/forward_plus.fs.wgsl b/src/shaders/forward_plus.fs.wgsl index 0500e3df..2dcc8f37 100644 --- a/src/shaders/forward_plus.fs.wgsl +++ b/src/shaders/forward_plus.fs.wgsl @@ -2,6 +2,19 @@ // See naive.fs.wgsl for basic fragment shader setup; this shader should use light clusters instead of looping over all lights +@group(${bindGroup_scene}) @binding(0) var cameraUniforms: CameraUniforms; +@group(${bindGroup_scene}) @binding(1) var lightSet: LightSet; +@group(${bindGroup_scene}) @binding(2) var clusterSet: ClusterSet; +@group(${bindGroup_material}) @binding(0) var diffuseTex: texture_2d; +@group(${bindGroup_material}) @binding(1) var diffuseTexSampler: sampler; + +struct FragmentInput +{ + @location(0) pos: vec3f, + @location(1) nor: vec3f, + @location(2) uv: vec2f +} + // ------------------------------------ // Shading process: // ------------------------------------ @@ -14,3 +27,61 @@ // Add the calculated contribution to the total light accumulation. // Multiply the fragment’s diffuse color by the accumulated light contribution. // Return the final color, ensuring that the alpha component is set appropriately (typically to 1). + +@fragment +fn main(in: FragmentInput) -> @location(0) vec4f +{ + let diffuseColor = textureSample(diffuseTex, diffuseTexSampler, in.uv); + if (diffuseColor.a < 0.5f) { + discard; + } + + // Determine cluster index based on fragment coordinates + let clusterX = ${numClustersX}u; + let clusterY = ${numClustersY}u; + let clusterZ = ${numClustersZ}u; + + let screenPos = cameraUniforms.viewProjMat * vec4(in.pos, 1.0); + let ndcPos = screenPos.xyz / screenPos.w; + + let viewPos = (cameraUniforms.viewMat * vec4(in.pos, 1.0)).xyz; + + // Compute cluster indices + let cx = u32(clamp((ndcPos.x + 1.0) * 0.5 * f32(clusterX), 0.0, f32(clusterX - 1u))); + let cy = u32(clamp((ndcPos.y + 1.0) * 0.5 * f32(clusterY), 0.0, f32(clusterY - 1u))); + let cz = u32(clamp(log((-viewPos.z) / cameraUniforms.nearPlane) / log(cameraUniforms.farPlane / cameraUniforms.nearPlane) * f32(clusterZ), 0.0, f32(clusterZ - 1u))); + + let clusterIndex = cx + cy * clusterX + cz * clusterX * clusterY; + + // todo print the z slice to see if increasing + let numTemp = f32(clusterSet.clusters[clusterIndex].numLights) / f32(${maxLightsPerCluster}); + + +// DEBUGGING + let x = f32(cx) / f32(${numClustersX}); + let y = f32(cy) / f32(${numClustersY}); + let z = f32(cz) / f32(${numClustersZ}); + + //return vec4(0., 0., z, 1.0); + // DEBUGGING + + //return vec4f(numTemp, numTemp, numTemp, 1f); + + // Retrieve cluster data + + var totalLightContrib = vec3f(0, 0, 0); + let nor = normalize(in.nor); + for (var i: u32 = 0u; i < clusterSet.clusters[clusterIndex].numLights; i = i + 1u) { + let lightIdx = clusterSet.clusters[clusterIndex].lightIndices[i]; + let light = lightSet.lights[lightIdx]; + totalLightContrib += calculateLightContrib(light, in.pos, nor); + } + + // let brightness = f32(cluster.numLights) / f32(${maxLightsPerCluster}); + + // return vec4(vec3(brightness), 1.0); + +let temp = -2.f * f32(cz) / f32(clusterZ); + var finalColor = diffuseColor.rgb * totalLightContrib; + return vec4(finalColor, 1); +} diff --git a/src/shaders/naive.fs.wgsl b/src/shaders/naive.fs.wgsl index 0afeaac7..cfea80a5 100644 --- a/src/shaders/naive.fs.wgsl +++ b/src/shaders/naive.fs.wgsl @@ -1,4 +1,6 @@ +@group(${bindGroup_scene}) @binding(0) var camera: CameraUniforms; @group(${bindGroup_scene}) @binding(1) var lightSet: LightSet; +//@group(${bindGroup_scene}) @binding(2) var clusterSet: ClusterSet; @group(${bindGroup_material}) @binding(0) var diffuseTex: texture_2d; @group(${bindGroup_material}) @binding(1) var diffuseTexSampler: sampler; @@ -19,7 +21,7 @@ fn main(in: FragmentInput) -> @location(0) vec4f } var totalLightContrib = vec3f(0, 0, 0); - for (var lightIdx = 0u; lightIdx < lightSet.numLights; lightIdx++) { + for (var lightIdx: u32 = 0u; lightIdx < lightSet.numLights; lightIdx += 1u) { let light = lightSet.lights[lightIdx]; totalLightContrib += calculateLightContrib(light, in.pos, normalize(in.nor)); } diff --git a/src/shaders/naive.vs.wgsl b/src/shaders/naive.vs.wgsl index 5a7ddd4b..5d82ce71 100644 --- a/src/shaders/naive.vs.wgsl +++ b/src/shaders/naive.vs.wgsl @@ -2,6 +2,7 @@ // TODO-1.3: add a uniform variable here for camera uniforms (of type CameraUniforms) // make sure to use ${bindGroup_scene} for the group +@group(${bindGroup_scene}) @binding(0) var camera: CameraUniforms; @group(${bindGroup_model}) @binding(0) var modelMat: mat4x4f; @@ -26,7 +27,8 @@ fn main(in: VertexInput) -> VertexOutput let modelPos = modelMat * vec4(in.pos, 1); var out: VertexOutput; - out.fragPos = ??? * modelPos; // TODO-1.3: replace ??? with the view proj mat from your CameraUniforms uniform variable + // TODO-1.3: replace ??? with the view proj mat from your CameraUniforms uniform variable + out.fragPos = camera.viewProjMat * modelPos; out.pos = modelPos.xyz / modelPos.w; out.nor = in.nor; out.uv = in.uv; diff --git a/src/shaders/shaders.ts b/src/shaders/shaders.ts index 584c008f..c08b5f40 100644 --- a/src/shaders/shaders.ts +++ b/src/shaders/shaders.ts @@ -27,10 +27,17 @@ export const constants = { bindGroup_scene: 0, bindGroup_model: 1, bindGroup_material: 2, + bindGroup_textures: 1, moveLightsWorkgroupSize: 128, - lightRadius: 2 + lightRadius: 2, + + numClustersX: 16, + numClustersY: 9, + numClustersZ: 24, + + maxLightsPerCluster: 1024 }; // ================================= diff --git a/src/stage/camera.ts b/src/stage/camera.ts index 7d2a4a1e..0954a524 100644 --- a/src/stage/camera.ts +++ b/src/stage/camera.ts @@ -3,14 +3,47 @@ import { toRadians } from "../math_util"; import { device, canvas, fovYDegrees, aspectRatio } from "../renderer"; class CameraUniforms { - readonly buffer = new ArrayBuffer(16 * 4); + readonly buffer = new ArrayBuffer(288); private readonly floatView = new Float32Array(this.buffer); set viewProjMat(mat: Float32Array) { // TODO-1.1: set the first 16 elements of `this.floatView` to the input `mat` + for (let i = 0; i < 16; i++) { + this.floatView[i] = mat[i]; + } } // TODO-2: add extra functions to set values needed for light clustering here + set nearPlane(near: number) { + this.floatView[16] = near; + } + set farPlane(far: number) { + this.floatView[17] = far; + } + set invProjMat(mat: Float32Array) { + for (let i = 0; i < 16; i++) { + this.floatView[20 + i] = mat[i]; + } + } + set viewMat(mat: Float32Array) { + for (let i = 0; i < 16; i++) { + this.floatView[36 + i] = mat[i]; + } + } + set invViewMat(mat: Float32Array) { + for (let i = 0; i < 16; i++) { + this.floatView[52 + i] = mat[i]; + } + } + set canvasResolution(res: [number, number]) { + this.floatView[68] = res[0]; + this.floatView[69] = res[1]; + // do i need to set an offset? + } + + // set numClusters(numClusters: number) { + // this.floatView[16] = numClusters; + // } } export class Camera { @@ -39,7 +72,18 @@ export class Camera { // // note that you can add more variables (e.g. inverse proj matrix) to this buffer in later parts of the assignment + this.uniformsBuffer = device.createBuffer({ + label: 'Camera Uniforms', + size: 288,// this.uniforms.buffer.byteLength, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + this.populateCameraBuffer(); + this.projMat = mat4.perspective(toRadians(fovYDegrees), aspectRatio, Camera.nearPlane, Camera.farPlane); + this.uniforms.nearPlane = Camera.nearPlane; + this.uniforms.farPlane = Camera.farPlane; + this.uniforms.invProjMat = mat4.invert(this.projMat); + this.uniforms.canvasResolution = [canvas.width, canvas.height]; this.rotateCamera(0, 0); // set initial camera vectors @@ -52,6 +96,12 @@ export class Camera { canvas.addEventListener('mousemove', (event) => this.onMouseMove(event)); } + private populateCameraBuffer() { + // TODO-1.1: upload `this.uniforms.buffer` (host side) to `this.uniformsBuffer` (device side) + // check `lights.ts` for examples of using `device.queue.writeBuffer()` + device.queue.writeBuffer(this.uniformsBuffer, 0, this.uniforms.buffer); + } + private onKeyEvent(event: KeyboardEvent, down: boolean) { this.keys[event.key.toLowerCase()] = down; if (this.keys['alt']) { // prevent issues from alt shortcuts @@ -129,10 +179,14 @@ export class Camera { const viewMat = mat4.lookAt(this.cameraPos, lookPos, [0, 1, 0]); const viewProjMat = mat4.mul(this.projMat, viewMat); // TODO-1.1: set `this.uniforms.viewProjMat` to the newly calculated view proj mat + this.uniforms.viewProjMat = viewProjMat; // TODO-2: write to extra buffers needed for light clustering here - + this.uniforms.viewMat = viewMat; + this.uniforms.invViewMat = mat4.invert(viewMat); + // TODO-1.1: upload `this.uniforms.buffer` (host side) to `this.uniformsBuffer` (device side) // check `lights.ts` for examples of using `device.queue.writeBuffer()` + device.queue.writeBuffer(this.uniformsBuffer, 0, this.uniforms.buffer); } } diff --git a/src/stage/lights.ts b/src/stage/lights.ts index a6eed919..a489263d 100644 --- a/src/stage/lights.ts +++ b/src/stage/lights.ts @@ -13,7 +13,7 @@ function hueToRgb(h: number) { export class Lights { private camera: Camera; - numLights = 500; + numLights = 5000; static readonly maxNumLights = 5000; static readonly numFloatsPerLight = 8; // vec3f is aligned at 16 byte boundaries @@ -29,6 +29,10 @@ export class Lights { moveLightsComputePipeline: GPUComputePipeline; // TODO-2: add layouts, pipelines, textures, etc. needed for light clustering here + clusterBindGroupLayout: GPUBindGroupLayout; + clusterBindGroup: GPUBindGroup; + clusterComputePipeline: GPUComputePipeline; + clusterBuffer: GPUBuffer; constructor(camera: Camera) { this.camera = camera; @@ -94,6 +98,72 @@ export class Lights { }); // TODO-2: initialize layouts, pipelines, textures, etc. needed for light clustering here + const maxLights = shaders.constants.maxLightsPerCluster; + + const numClusters = shaders.constants.numClustersX * shaders.constants.numClustersY * shaders.constants.numClustersZ; + // numLights (4) + padding (4) + lightIndices (4 * maxLightsPerCluster) + const clusterStructSize = 16 + 4 * maxLights; + const clusterBufferSize = numClusters * clusterStructSize;//* Math.ceil(clusterStructSize / 16) * 16; // + 16 is for numClusters u32 + padding + + this.clusterBuffer = device.createBuffer({ + label: "cluster buffer", + size: 16 + clusterBufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC + }); + + this.clusterBindGroupLayout = device.createBindGroupLayout({ + label: "clustering compute bind group layout", + entries: [ + { binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: "uniform" } // cameraUniforms + }, + { binding: 1, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: "read-only-storage" } // lightSet + }, + { binding: 2, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: "storage" } // clusterSet + } + ] + }); + + this.clusterBindGroup = device.createBindGroup({ + label: "clustering compute bind group", + layout: this.clusterBindGroupLayout, + entries: [ + { + binding: 0, + resource: { buffer: this.camera.uniformsBuffer } + }, + { + binding: 1, + resource: { buffer: this.lightSetStorageBuffer } + }, + { + binding: 2, + resource: { buffer: this.clusterBuffer } + } + ] + }); + + this.clusterComputePipeline = device.createComputePipeline({ + label: "clustering compute pipeline", + layout: device.createPipelineLayout({ + label: "clustering compute pipeline layout", + bindGroupLayouts: [ this.clusterBindGroupLayout ] + }), + compute: { + module: device.createShaderModule({ + label: "clustering compute shader", + code: shaders.clusteringComputeSrc + }), + entryPoint: "main" + } + }); + + } private populateLightsBuffer() { @@ -113,6 +183,16 @@ export class Lights { doLightClustering(encoder: GPUCommandEncoder) { // TODO-2: run the light clustering compute pass(es) here // implementing clustering here allows for reusing the code in both Forward+ and Clustered Deferred + const computePass = encoder.beginComputePass(); + computePass.setPipeline(this.clusterComputePipeline); + computePass.setBindGroup(0, this.clusterBindGroup); + + computePass.dispatchWorkgroups( + Math.ceil(shaders.constants.numClustersX / 8), + Math.ceil(shaders.constants.numClustersY / 8), + shaders.constants.numClustersZ + ); + computePass.end(); } // CHECKITOUT: this is where the light movement compute shader is dispatched from the host