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
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,38 @@

**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)
* Ruichi Zhang
* Tested on: **Google Chrome 141.0.7390.123** on
Windows 10, AMD Ryzen 9 7950X3D @ 4201 Mhz, 16 Core(s), NVIDIA GeForce RTX 4080 SUPER

### Live Demo

[![](img/thumb.png)](http://TODO.github.io/Project4-WebGPU-Forward-Plus-and-Clustered-Deferred)
[![](images/gaussian_screenshot.png)](https://pabloo0610.github.io/Project5-WebGPU-Gaussian-Splat-Viewer/)

### Demo Video/GIF

[![](img/video.mp4)](TODO)
![](images/gaussian_splatting.gif)
<!-- <video width="640" height="360" controls>
<source src="images/gaussian_splatting.mp4" type="video/mp4">
Your browser does not support the video tag.
</video> -->

### (TODO: Your README)
## Overview

*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 project implements a GPU-based Gaussian Splatting renderer and compares it against a baseline point cloud renderer.

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
## Performance Analysis
### Comparison between Point-Cloud and Gaussian Renderer
The point-cloud renderer displays individual points directly, which results in a sparse and discrete appearance, especially when the point density is low. In contrast, the Gaussian renderer blends each point as a smooth 3D Gaussian splat, creating continuous surfaces and more realistic shading. While the Gaussian method achieves much higher visual fidelity and better approximates object geometry, it also introduces additional computation for blending and opacity accumulation. Consequently, the Gaussian renderer is more computationally expensive but produces a significantly smoother and more photorealistic image compared to the point-cloud renderer

### Effect of Workgroup Size on Performance
Changing the workgroup size in the Gaussian renderer affects how many threads are launched per compute group and how efficiently GPU resources are utilized. Increasing the workgroup size typically improves performance up to an optimal point (such as 128 or 256 threads per group), as it increases occupancy and reduces scheduling overhead. However, beyond that point, performance may degrade slightly due to register pressure, shared memory limitations, or reduced warp scheduling flexibility. Thus, performance scales non-linearly with workgroup size, and finding the optimal configuration depends on the specific GPU architecture.

### Effect of View-Frustum Culling on Performance
Enabling view-frustum culling improves performance because it prevents the renderer from processing Gaussians that are outside the camera’s visible region. By discarding off-screen splats early, the GPU can focus computation on only the visible subset, reducing both memory bandwidth usage and blending operations. The performance gain is especially noticeable in large scenes where many Gaussians lie outside the camera’s view. The cost of performing the culling check is small compared to the rendering time saved, making this an effective optimization.

### Effect of Number of Gaussians on Performance
The number of Gaussians directly impacts rendering performance since each Gaussian requires projection, blending, and shading computations. As the number of Gaussians increases, the total workload scales approximately linearly, leading to longer frame times and higher GPU memory usage. For smaller datasets, the GPU can handle the workload efficiently due to high parallelism, but at very large counts, bandwidth and arithmetic throughput become bottlenecks. Therefore, performance decreases with increasing Gaussian count, reflecting the trade-off between scene detail and real-time rendering speed.

### Credits

Expand Down
Binary file added images/gaussian_screenshot.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/gaussian_splatting.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 images/gaussian_splatting.mp4
Binary file not shown.
142 changes: 138 additions & 4 deletions src/renderers/gaussian-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { get_sorter,c_histogram_block_rows,C } from '../sort/sort';
import { Renderer } from './renderer';

export interface GaussianRenderer extends Renderer {

set_gaussian_multiplier: (multiplier: number) => void,
}

// Utility to create GPU buffers
Expand Down Expand Up @@ -36,6 +36,39 @@ export default function get_renderer(

const nulling_data = new Uint32Array([0]);

const null_buffer = createBuffer(
device,
'null buffer',
4,
GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
nulling_data
);

device.queue.writeBuffer(null_buffer, 0, nulling_data);

const splat_buffer = createBuffer(
device,
'splat_data',
pc.num_points * 6 * 4,
GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
);

const render_settings_buffer = createBuffer(
device,
'render_settings',
2 * 4,
GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
new Float32Array([1.0, pc.sh_deg])
);

const draw_indirect_buffer = createBuffer(
device,
'draw_indirect',
4 * 4,
GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
new Uint32Array([6, 0, 0, 0])
);

// ===============================================
// Create Compute Pipeline and Bind Groups
// ===============================================
Expand All @@ -52,6 +85,25 @@ export default function get_renderer(
},
});

const preprocess_camera_bind_group = device.createBindGroup({
label: 'preprocess_camera',
layout: preprocess_pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: camera_buffer } },
{ binding: 1, resource: { buffer: render_settings_buffer } },
],
});

const preprocess_gaussian_bind_group = device.createBindGroup({
label: 'preprocess_gaussian',
layout: preprocess_pipeline.getBindGroupLayout(1),
entries: [
{ binding: 0, resource: { buffer: pc.gaussian_3d_buffer } },
{ binding: 1, resource: { buffer: pc.sh_buffer } },
{ binding: 2, resource: { buffer: splat_buffer } },
],
});

const sort_bind_group = device.createBindGroup({
label: 'sort',
layout: preprocess_pipeline.getBindGroupLayout(2),
Expand All @@ -67,20 +119,102 @@ export default function get_renderer(
// ===============================================
// Create Render Pipeline and Bind Groups
// ===============================================

const render_pipeline = device.createRenderPipeline({
label: 'gaussian_render',
layout: 'auto',
vertex: {
module: device.createShaderModule({ code: renderWGSL }),
entryPoint: 'vs_main',
},
fragment: {
module: device.createShaderModule({ code: renderWGSL }),
entryPoint: 'fs_main',
targets: [{
format: presentation_format,
blend: {
color: {
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
operation: 'add',
},
alpha: {
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha',
operation: 'add',
},
},
}],
},
primitive: {
topology: 'triangle-list',
},
});

const render_camera_bind_group = device.createBindGroup({
label: 'render_camera',
layout: render_pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: camera_buffer } },
],
});

const render_splat_bind_group = device.createBindGroup({
label: 'render_splat',
layout: render_pipeline.getBindGroupLayout(1),
entries: [
{ binding: 0, resource: { buffer: splat_buffer } },
{ binding: 1, resource: { buffer: sorter.ping_pong[0].sort_indices_buffer } },
],
});

// ===============================================
// Command Encoder Functions
// ===============================================

const preprocess = (encoder: GPUCommandEncoder) => {
const pass = encoder.beginComputePass({ label: 'preprocess' });
pass.setPipeline(preprocess_pipeline);
pass.setBindGroup(0, preprocess_camera_bind_group);
pass.setBindGroup(1, preprocess_gaussian_bind_group);
pass.setBindGroup(2, sort_bind_group);
const workgroup_count = Math.ceil(pc.num_points / C.histogram_wg_size);
pass.dispatchWorkgroups(workgroup_count);
pass.end();
};

const render = (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => {
const pass = encoder.beginRenderPass({
label: 'gaussian_render',
colorAttachments: [
{
view: texture_view,
loadOp: 'clear',
storeOp: 'store',
}
],
});
pass.setPipeline(render_pipeline);
pass.setBindGroup(0, render_camera_bind_group);
pass.setBindGroup(1, render_splat_bind_group);
pass.drawIndirect(draw_indirect_buffer, 0);
pass.end();
};

// ===============================================
// Return Render Object
// ===============================================
return {
frame: (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => {
encoder.copyBufferToBuffer(null_buffer, 0, sorter.sort_info_buffer, 0, 4);
encoder.copyBufferToBuffer(null_buffer, 0, sorter.sort_dispatch_indirect_buffer, 0, 4);
preprocess(encoder);
sorter.sort(encoder);
encoder.copyBufferToBuffer(sorter.sort_info_buffer, 0, draw_indirect_buffer, 4, 4);

render(encoder, texture_view);
},
camera_buffer,
set_gaussian_multiplier: (multiplier: number) => {
device.queue.writeBuffer(render_settings_buffer, 0, new Float32Array([multiplier, pc.sh_deg]));
},
};
}
}
3 changes: 2 additions & 1 deletion src/renderers/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export default async function init(
{min: 0, max: 1.5}
).on('change', (e) => {
//TODO: Bind constants to the gaussian renderer.
gaussian_renderer.set_gaussian_multiplier(e.value);
});
}

Expand Down Expand Up @@ -157,4 +158,4 @@ export default async function init(
}

requestAnimationFrame(frame);
}
}
83 changes: 81 additions & 2 deletions src/shaders/gaussian.wgsl
Original file line number Diff line number Diff line change
@@ -1,22 +1,101 @@
struct VertexOutput {
@builtin(position) position: vec4<f32>,
//TODO: information passed from vertex shader to fragment shader
@location(0) center_ndc: vec2<f32>,
@location(1) color: vec3<f32>,
@location(2) opacity: f32,
@location(3) conic: vec3<f32>,
};

struct Splat {
//TODO: information defined in preprocess compute shader
uv_size: array<u32, 2>,
color_opacity: array<u32, 2>,
conic_radius: array<u32, 2>
};

struct CameraUniforms {
view: mat4x4<f32>,
view_inv: mat4x4<f32>,
proj: mat4x4<f32>,
proj_inv: mat4x4<f32>,
viewport: vec2<f32>,
focal: vec2<f32>
};

struct RenderSettings {
gaussian_scaling: f32,
sh_deg: f32,
}

// Bind groups
@group(0) @binding(0) var<uniform> camera: CameraUniforms;

@group(1) @binding(0) var<storage, read> splats: array<Splat>;
@group(1) @binding(1) var<storage, read> sort_indices : array<u32>;

@vertex
fn vs_main(
@builtin(vertex_index) vertex_index: u32,
@builtin(instance_index) instance_index: u32
) -> VertexOutput {
//TODO: reconstruct 2D quad based on information from splat, pass
let index = sort_indices[instance_index];
let quad_positions = array<vec2<f32>, 6>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, -1.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(-1.0, -1.0),
vec2<f32>( 1.0, -1.0),
vec2<f32>( 1.0, 1.0)
);
let quad_coord = quad_positions[vertex_index];

let splat = splats[index];

let ndc_pos = unpack2x16float(splat.uv_size[0]);
let quad_size = unpack2x16float(splat.uv_size[1]);
//let quad_size = vec2<f32>(0.01, 0.01); // Temporary fixed size for testing
let color_rg = unpack2x16float(splat.color_opacity[0]);
let color_b_opacity = unpack2x16float(splat.color_opacity[1]);
let color = vec3<f32>(color_rg.x, color_rg.y, color_b_opacity.x);
let opacity = color_b_opacity.y;

let conic_ab = unpack2x16float(splat.conic_radius[0]);
let conic_c_radius = unpack2x16float(splat.conic_radius[1]);
let conic = vec3<f32>(conic_ab.x, conic_ab.y, conic_c_radius.x);
let radius = conic_c_radius.y;

let vertex_ndc = ndc_pos + quad_coord * quad_size;

var out: VertexOutput;
out.position = vec4<f32>(1. ,1. , 0., 1.);
out.position = vec4<f32>(vertex_ndc, 0.0, 1.0);
out.center_ndc = ndc_pos.xy;
out.color = color;
out.opacity = opacity;
out.conic = conic;

return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(1.);
var ndc = (in.position.xy / camera.viewport) * 2.0 - 1.0;
ndc.y *= -1.0;
var d = (ndc - in.center_ndc) * camera.viewport * 0.5;
d.x = -d.x;
let conic = in.conic;

let power = -0.5 * (conic.x * d.x * d.x +
2.0 * conic.y * d.x * d.y +
conic.z * d.y * d.y);
if (power > 0.0) {
discard;
}

let alpha = clamp(in.opacity * exp(power), 0.0, 0.99);
let final_color = in.color;
//let final_color = vec3<f32>(1.0,0.0,0.0); // For testing: render all splats in red

return vec4<f32>(final_color * alpha, alpha);
}
2 changes: 1 addition & 1 deletion src/shaders/point_cloud.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fn vs_main(
let pos = vec4<f32>(a.x, a.y, b.x, 1.);

// TODO: MVP calculations
out.position = pos;
out.position = camera.proj * camera.view * pos;

return out;
}
Expand Down
Loading