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
51 changes: 51 additions & 0 deletions .github/workflows/jekyll-gh-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll with GitHub Pages dependencies preinstalled

on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false

jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3

# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
28 changes: 28 additions & 0 deletions .github/workflows/npm-grunt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: NodeJS with Grunt

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x, 20.x, 22.x]

steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Build
run: |
npm install
grunt
47 changes: 30 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,44 @@
# Project5-WebGPU-Gaussian-Splat-Viewer

**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 4**
**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)
* Maya Diaz Huizar
* Tested on: Google Chrome 132.0 on Windows 10, AMD Ryzen 9 5900X @ 3.7GHz 32GB RAM, Nvidia RTX 3080 10GB

### Live Demo
## Live Demo (requires Chrome and a WebGPU compatible GPU)
[Demo Link](https://aorus1.github.io/Project5-WebGPU-Gaussian-Splat-Viewer/)
[Sample Files](https://drive.google.com/drive/folders/1rwtArEbj7GfjMeK6mD3a4QN6ZFLKQoBB?usp=sharing)

[![](img/thumb.png)](http://TODO.github.io/Project4-WebGPU-Forward-Plus-and-Clustered-Deferred)
## Demo Video

### Demo Video/GIF
[![Demo Video]()](images/demo.mp4)

[![](img/video.mp4)](TODO)
## Project Overview

### (TODO: Your README)
The WebGPU Gaussian Splat Viewer is a 3D renderer for Gaussian splats, designed to display and visualize point cloud data using a Gaussian splatting technique. Gaussian splats allow for smooth, realistic rendering of point clouds with transparency effects. This viewer is built in WebGPU, featuring a Gaussian renderer that includes preprocessing and rendering pipelines, depth sorting, and spherical harmonics-based shading.

*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.
## Features

This assignment has a considerable amount of performance analysis compared
to implementation work. Complete the implementation early to leave time!
### Gaussian Preprocessing Pipeline
The preprocessing pipeline handles 3D Gaussian data before rendering:
- **View-Frustum Culling**: Removes non-visible Gaussians outside the camera’s view.
- **3D to 2D Transformation**: Transforms Gaussian points into 2D screen space for rendering.
- **Covariance Calculation**: Computes a 2D conic for each Gaussian based on a covariance matrix derived from 3D rotation and scaling data.
- **Opacity Scaling**: Applies a sigmoid function to Gaussian opacities for realistic transparency.
- **Depth Sorting**: Uses a GPU-based radix sort to sort Gaussians by depth for accurate transparency effects.

### Credits
## Performance Analysis
- **Point Cloud vs. Gaussian Renderer**: Gaussian splatting improves rendering quality for point clouds, with spherical harmonics and Gaussian opacity scaling providing smoother, more realistic visuals. However, the Gaussian renderer is more computationally intensive than direct point cloud rendering due to shading and transparency effects. Performance is obviously and noticeably worse, but this is understandable.

- **Effect of Workgroup Size on Performance**: Workgroup size significantly impacts performance in the Gaussian renderer. I found the default parameter given to be the most performant, which makes sense as its likely a nice middle ground.

- **View-Frustum Culling**: Culling offers a substantial performance boost by eliminating off-screen Gaussians from rendering, reducing the number of draw calls and unnecessary GPU computations. The speedup is substantial because a significant number of threads can be abandoned, leading to faster draws.

- **Gaussian Count and Performance**: As the number of Gaussians increases, rendering performance decreases due to increased depth sorting and fragment processing requirements. Testing with varying Gaussian counts reveals the expected worse performance with increased Gaussian counts.

## Credits
- [Vite](https://vitejs.dev/)
- [tweakpane](https://tweakpane.github.io/docs//v3/monitor-bindings/)
- [stats.js](https://github.com/mrdoob/stats.js)
- [tweakpane](https://cocopon.github.io/tweakpane/)
- [wgpu-matrix](https://github.com/greggman/wgpu-matrix)
- Special Thanks to: Shrek Shao (Google WebGPU team) & [Differential Guassian Renderer](https://github.com/graphdeco-inria/diff-gaussian-rasterization)
- [WebGPU Developer Tools](https://chrome.google.com/webstore/detail/webgpu-developer-tools/)
- Special thanks to Shrek Shao from Google WebGPU and [Differential Gaussian Renderer](https://github.com/graphdeco-inria/diff-gaussian-rasterization) for inspiration and resources.
Binary file added images/demo.mp4
Binary file not shown.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/camera/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ interface CameraPreset {
rotation: Mat4,
}

export async function load_camera_presets(file: string): Promise<CameraPreset[]> {
const blob = new Blob([file]);
export async function load_camera_presets(file: File): Promise<CameraPreset[]> {
const blob = new Blob([file], { type: file.type });
const arrayBuffer = await new Promise((resolve, reject) => {
const reader = new FileReader();

Expand Down
113 changes: 89 additions & 24 deletions src/renderers/gaussian-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { PointCloud } from '../utils/load';
import preprocessWGSL from '../shaders/preprocess.wgsl';
import renderWGSL from '../shaders/gaussian.wgsl';
import { get_sorter,c_histogram_block_rows,C } from '../sort/sort';
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
// Utility function to create GPU buffers with optional data initialization
const createBuffer = (
device: GPUDevice,
label: string,
Expand All @@ -28,17 +28,16 @@ export default function get_renderer(
camera_buffer: GPUBuffer,
): GaussianRenderer {

// Initialize sorter for GPU-based sorting of point cloud data
const sorter = get_sorter(pc.num_points, device);

// ===============================================
// Initialize GPU Buffers
// ===============================================

// Initialize GPU Buffers
const nulling_data = new Uint32Array([0]);
const null_buffer = createBuffer(device, 'null buffer', 4, GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, nulling_data);
const splatBuffer = createBuffer(device, 'splat buffer', pc.num_points * 96, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
const render_settings_buffer = createBuffer(device, 'render settings buffer', 4 * 2, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, new Float32Array([1.0, pc.sh_deg]));

// ===============================================
// Create Compute Pipeline and Bind Groups
// ===============================================
// Create Compute Pipeline and Bind Groups
const preprocess_pipeline = device.createComputePipeline({
label: 'preprocess',
layout: 'auto',
Expand All @@ -52,35 +51,101 @@ export default function get_renderer(
},
});

const sort_bind_group = device.createBindGroup({
label: 'sort',
layout: preprocess_pipeline.getBindGroupLayout(2),
const preprocess_bind_group = device.createBindGroup({
label: 'preprocess bind group',
layout: preprocess_pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: sorter.sort_info_buffer } },
{ binding: 1, resource: { buffer: sorter.ping_pong[0].sort_depths_buffer } },
{ binding: 2, resource: { buffer: sorter.ping_pong[0].sort_indices_buffer } },
{ binding: 3, resource: { buffer: sorter.sort_dispatch_indirect_buffer } },
{ binding: 4, resource: { buffer: camera_buffer } },
{ binding: 5, resource: { buffer: pc.gaussian_3d_buffer } },
{ binding: 6, resource: { buffer: splatBuffer } },
{ binding: 7, resource: { buffer: render_settings_buffer } },
{ binding: 8, resource: { buffer: pc.sh_buffer } },
],
});

// Create Render Pipeline and Bind Groups
const render_pipeline = device.createRenderPipeline({
label: 'render gaussian',
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',
},
},
}],
}
});

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

const indirect_draw_data = new Uint32Array([6, 0, 0, 0]);
const indirect_draw_buffer = createBuffer(device, 'indirect draw buffer', 4 * 4, GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST, indirect_draw_data);

// ===============================================
// Create Render Pipeline and Bind Groups
// ===============================================

// Command Encoder Functions
const preprocess = (encoder: GPUCommandEncoder) => {
const preprocess_pass = encoder.beginComputePass();
preprocess_pass.setPipeline(preprocess_pipeline);
preprocess_pass.setBindGroup(0, preprocess_bind_group);
preprocess_pass.dispatchWorkgroups(Math.ceil(pc.num_points / C.histogram_wg_size), 1, 1);
preprocess_pass.end();
};

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

const render = (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => {
const pass = encoder.beginRenderPass({
label: "render gaussian pass",
colorAttachments: [{
view: texture_view,
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(render_pipeline);
pass.setBindGroup(0, render_bind_group);
pass.drawIndirect(indirect_draw_buffer, 0);
pass.end();
};

// ===============================================
// Return Render Object
// ===============================================
// Return Render Object with Frame Function
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, indirect_draw_buffer, 4, 4);

render(encoder, texture_view);
},
camera_buffer,
render_settings_buffer,
};
}
32 changes: 17 additions & 15 deletions src/renderers/point-cloud-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ export default function get_renderer(
pc: PointCloud,
device: GPUDevice,
presentation_format: GPUTextureFormat,
camera_buffer: GPUBuffer): Renderer {
const render_shader = device.createShaderModule({code: pointcloud_wgsl});
camera_buffer: GPUBuffer
): Renderer {

// Create Shader Module
const render_shader = device.createShaderModule({ code: pointcloud_wgsl });

// Create Render Pipeline
const render_pipeline = device.createRenderPipeline({
label: 'render',
layout: 'auto',
Expand All @@ -25,44 +30,41 @@ export default function get_renderer(
},
});

// Create Bind Groups
const camera_bind_group = device.createBindGroup({
label: 'point cloud camera',
layout: render_pipeline.getBindGroupLayout(0),
entries: [{binding: 0, resource: { buffer: camera_buffer }}],
entries: [{ binding: 0, resource: { buffer: camera_buffer } }],
});

const gaussian_bind_group = device.createBindGroup({
label: 'point cloud gaussians',
layout: render_pipeline.getBindGroupLayout(1),
entries: [
{binding: 0, resource: { buffer: pc.gaussian_3d_buffer }},
],
entries: [{ binding: 0, resource: { buffer: pc.gaussian_3d_buffer } }],
});

// Render Function
const render = (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => {
const pass = encoder.beginRenderPass({
label: 'point cloud render',
colorAttachments: [
{
view: texture_view,
loadOp: 'clear',
storeOp: 'store',
}
],
colorAttachments: [{
view: texture_view,
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(render_pipeline);
pass.setBindGroup(0, camera_bind_group);
pass.setBindGroup(1, gaussian_bind_group);

pass.draw(pc.num_points);
pass.end();
};

// Return Renderer Object
return {
frame: (encoder: GPUCommandEncoder, texture_view: GPUTextureView) => {
render(encoder, texture_view);
},

camera_buffer,
};
}
Loading