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

**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)
* 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)

## Overview

This project implements a GPU-driven 3D Gaussian splatting renderer using WebGPU. The goal is to take a set of 3D Gaussians and render high-quality, order-independent, depth-sorted 2D splats (quads) on the screen. The implementation follows the assignment stages: loading scene and camera data, preprocessing Gaussians on the GPU (culling, covariance → 2D conic projection, color evaluation), sorting by depth, and rendering the resulting splats with an indirect draw call.

### Live Demo

[![](img/thumb.png)](http://TODO.github.io/Project4-WebGPU-Forward-Plus-and-Clustered-Deferred)
[![](<Screenshot 2025-10-29 235258.png>)](https://christinaqiu3.com/Project5-WebGPU-Gaussian-Splat-Viewer/)

### Demo Video/GIF

[![](img/video.mp4)](TODO)
[![](images/hw_5_1.gif)](TODO)

## Performance Analysis

### Comparing point-cloud and gaussian renderer

The point-cloud renderer displays each point as a simple dot or quad with uniform size and color, producing a sparse and noisy appearance without smooth transitions. It lacks depth-dependent opacity, shading, or blending between nearby points.

The Gaussian renderer, produces smooth, continuous surfaces because each point is rendered as an anisotropic 2D Gaussian ellipse instead of a fixed-size point. The splats blend together, giving a more realistic and soft appearance, especially around regions with dense point samples.

### Workgroup size affecting performance

insert graphs

### View-frustum culling give performance improvement

### (TODO: Your README)
insert graphs

*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.
### Number of guassians affects performance

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
insert graphs

### Credits

Expand Down
Binary file added Screenshot 2025-10-29 235258.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/Screenshot 2025-10-29 235015.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/Screenshot 2025-10-29 235258.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/hw_5_1.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
163 changes: 160 additions & 3 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 {

render_settings_buffer: GPUBuffer,
}

// Utility to create GPU buffers
Expand All @@ -17,7 +17,7 @@ const createBuffer = (
data?: ArrayBuffer | ArrayBufferView
) => {
const buffer = device.createBuffer({ label, size, usage });
if (data) device.queue.writeBuffer(buffer, 0, data);
if (data) device.queue.writeBuffer(buffer, 0, data as BufferSource);
return buffer;
};

Expand All @@ -36,6 +36,22 @@ export default function get_renderer(

const nulling_data = new Uint32Array([0]);

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

const render_settings_buffer = createBuffer(
device,
'render settings buffer',
8,
GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
new Float32Array([1.0, pc.sh_deg])
);

// ===============================================
// Create Compute Pipeline and Bind Groups
// ===============================================
Expand Down Expand Up @@ -63,24 +79,165 @@ export default function get_renderer(
],
});

const camera_bind_group = device.createBindGroup({
label: 'camera bind group',
layout: preprocess_pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: camera_buffer } },
],
});

const gaussian_preprocess_bind_group = device.createBindGroup({
label: 'gaussian preprocess bind group',
layout: preprocess_pipeline.getBindGroupLayout(1),
entries: [
{ binding: 0, resource: { buffer: pc.gaussian_3d_buffer } },
],
});

var splat_size = 0;
splat_size += 16; // for position, size, color
splat_size *= pc.num_points;

const splat_buffer = createBuffer(
device,
'splat buffer',
splat_size,
GPUBufferUsage.STORAGE,
null
);

const compute_bind_group = device.createBindGroup({
label: 'compute bind group',
layout: preprocess_pipeline.getBindGroupLayout(3),
entries: [
{ binding: 0, resource: { buffer: splat_buffer } },//buffer: pc.gaussian_3d_buffer } },
{ binding: 1, resource: { buffer: render_settings_buffer } },
{ binding: 2, resource: { buffer: pc.sh_buffer}},
],
});

// ===============================================
// Create Render Pipeline and Bind Groups
// ===============================================
// instance quads for each point in cloud, compute pipeline and then rendering pipeline
// use create buffer function tomake vertex and index buffer (using gpu buffer usage, buffer usage.indirect)
// pc.num_points number of quads

const indirect_buffer = createBuffer(
device,
'indirect draw buffer',
4 * 4, // 4 uint32 values
GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
new Uint32Array([6, 0, 0, 0]) // 6 vertices per quad, num_points instances
);

const render_pipeline = device.createRenderPipeline({
label: 'gaussian render pipeline',
layout: 'auto',
vertex: {
module: device.createShaderModule({ code: renderWGSL }),
entryPoint: 'vs_main',
buffers: [],
},
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',
},
}
},
],
},
});

const render_bind_group = device.createBindGroup({
label: 'render bind group',
layout: render_pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: splat_buffer } },
{ binding: 1, resource: { buffer: sorter.ping_pong[0].sort_indices_buffer } },
{ binding: 2, resource: { buffer: camera_buffer}},
],
});

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

const compute = (encoder: GPUCommandEncoder) => {
const pass = encoder.beginComputePass();
pass.setPipeline(preprocess_pipeline);
pass.setBindGroup(0, camera_bind_group);
pass.setBindGroup(1, gaussian_preprocess_bind_group);
pass.setBindGroup(2, sort_bind_group);
pass.setBindGroup(3, compute_bind_group);
pass.dispatchWorkgroups(Math.ceil(pc.num_points / C.histogram_wg_size));
pass.end();
}

// ===============================================
// Return Render Object
// ===============================================
return {
frame: (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => {

// clear sorter buffers
encoder.copyBufferToBuffer(
zero_buffer,
0,
sorter.sort_info_buffer,
0,
4
);
encoder.copyBufferToBuffer(
zero_buffer,
0,
sorter.sort_dispatch_indirect_buffer,
0,
4
);

// Preprocess compute pass
compute(encoder);

sorter.sort(encoder);

encoder.copyBufferToBuffer(
sorter.sort_info_buffer,
0,
indirect_buffer,
4,
4
);

const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: texture_view,
loadOp: 'clear',
storeOp: 'store',
clearValue: { r: 0, g: 0, b: 0, a: 1 },
},
],
});
pass.setPipeline(render_pipeline);
pass.setBindGroup(0, render_bind_group);
pass.drawIndirect(indirect_buffer, 0);
pass.end();

},
camera_buffer,
render_settings_buffer,
};
}
7 changes: 7 additions & 0 deletions src/renderers/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ export default async function init(
{min: 0, max: 1.5}
).on('change', (e) => {
//TODO: Bind constants to the gaussian renderer.
if (gaussian_renderer) {
device.queue.writeBuffer(
gaussian_renderer.render_settings_buffer,
0,
new Float32Array([params.gaussian_multiplier]) //,0
);
}
});
}

Expand Down
88 changes: 84 additions & 4 deletions src/shaders/gaussian.wgsl
Original file line number Diff line number Diff line change
@@ -1,22 +1,102 @@
struct VertexOutput {
@builtin(position) position: vec4<f32>,
//TODO: information passed from vertex shader to fragment shader
@location(0) color: vec4<f32>,
@location(1) conic_opacity: vec4<f32>,
@location(2) center: vec2<f32>,
};

struct Splat {
//TODO: information defined in preprocess compute shader
pos: vec4<f32>,
// opacity: f32,
// rot: mat2x2<f32>,
scale: vec2<f32>,
color: array<u32, 2>,
conic_opacity: array<u32, 2>,
};

struct Camera {
viewMat: mat4x4<f32>,
invViewMat: mat4x4<f32>,
projMat: mat4x4<f32>,
invProjMat: mat4x4<f32>,
viewport: vec2<f32>,
focal: vec2<f32>,
}

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

@vertex
fn vs_main(
fn vs_main(
@builtin(instance_index) global_instance_index: u32,
@builtin(vertex_index) local_vertex_index: u32
) -> VertexOutput {
//TODO: reconstruct 2D quad based on information from splat, pass
//TODO: reconstruct 2D quad based on information from splat, pass to fragment shader

let index = sort_indices[global_instance_index];
let splat = splats[index];

let x = splat.pos.x;
let y = splat.pos.y;
let w = splat.scale.x * 2.0f; // because scale is half-width
let h = splat.scale.y * 2.0f;

// array of 6 vec2s using x y w h
// does the order matter?
let quad = array<vec2<f32>,6>(
vec2<f32>(x - w, y + h),
vec2<f32>(x - w, y - h),
vec2<f32>(x + w, y - h),
vec2<f32>(x + w, y - h),
vec2<f32>(x + w, y + h),
vec2<f32>(x - w, y + h)
);

// vertex positions using local_vertex_index
let position = vec4(quad[local_vertex_index], 0.0, 1.0); // splat.pos.z for depth

let unpacked_a = unpack2x16float(splat.color[0]);
let unpacked_b = unpack2x16float(splat.color[1]);
let color = vec4(unpacked_a.x, unpacked_a.y, unpacked_b.x, unpacked_b.y);
let size = (vec2f(w, h) * .5f + .5f) * camera.viewport.xy;

// conic
let conic_unpacked_a = unpack2x16float(splat.conic_opacity[0]);
let conic_unpacked_b = unpack2x16float(splat.conic_opacity[1]);
let conic = vec3f(conic_unpacked_a.x, conic_unpacked_a.y, conic_unpacked_b.x);
let opacity = conic_unpacked_b.y;

var out: VertexOutput;
out.position = vec4<f32>(1. ,1. , 0., 1.);
out.position = position;
out.color = color;
out.conic_opacity = vec4f(conic, opacity);
out.center = vec2f(x, y);

return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(1.);

var pos = (in.position.xy / camera.viewport) * 2.f - 1.f;
pos.y *= -1.f;

var offset = (pos.xy - in.center.xy) * camera.viewport * vec2f(-.5f, .5f);

var power = in.conic_opacity.x * pow(offset.x, 2.f) + in.conic_opacity.z * pow(offset.y, 2.f);
power = power * -.5f - in.conic_opacity.y * offset.x * offset.y;

if (power > 0.f) {
return vec4f(0.f);
}

let alpha = min(.99f, in.conic_opacity.w * exp(power));

return in.color * 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