This repo is intended to demonstrate a minimal example of integrating a wasm module implemented in Rust to display webgpu content in a React webapp.
Recently I wanted to integrate a shader-based simulation in a website, and while the wgpu repo has a ton of amazing examples, I found it to be somewhat obfuscated how the wgpu modues in the example code are actually integrated into a website, due to the fact that in that project, there is a script which builds the wasm, generates the necessary artifacts and website, and the example site and wasm module contain some extra logic to select the relevant example for display. It took a bit of digging to figure out how the pieces fit together, and I wanted to save others the work of figuring out how to build a wgpu/wasm module in Rust and display it on a website.
The fact that we can display rich, interactive content implemented in Rust on the web is awesome, and I want this to be more accessible.
This project contains a couple of components:
-
The
wgpu_module
crate contains the rust implementation of the wgpu code which will be compiled to a wasm module for display on the site. It renders a simple triangle using a single wgpu shader. -
The
site
dir contains a simple Vite React application which displays the content rendered by the wgpu module. -
The
run.sh
script handles the process of building the module, generating the necessary artifacts and running the vite application.
In order to run this project, a few dependencies are needed:
To run the Vite applicaiton, you will need to have yarn installed. Instructions can be found here.
You will also need the target wasm32-unknown-unknown
in your rust toolchain, and to have the wasm-bindgen-cli
installed. This can be achieved by running the following commands:
$ rustup target add wasm32-unknown-unknown
$ cargo install wasm-bindgen-cli
To build and run the example in the webapp, simply call the run.sh
script:
$ /bin/bash run.sh
It's also possible to run the wgpu example as a standalone native app like so:
$ cargo run -p wgpu_module
In order to display wgpu content on the web, the follwing is needed:
- A wasm module containing the wgpu implementation.
- A javascript binding, to allow your web front-end application to communicate with the wasm module.
- A website with a canvas to display the wgpu content.
We can achieve this with the following steps:
The first step is to build our rust crate as a wasm module. For this we use cargo
with the wasm
target selected:
cargo build --target wasm32-unknown-unknown -p wgpu_module
This will generate the file wgpu_module.wasm
in the directory ./target/wasm32-unknown-unknown/debug
. We're using debug for simplicity, but we could do a release build as well.
Once we have our wasm module built, we need to generate the js bindings. This is achieved with the following command:
wasm-bindgen target/wasm32-unknown-unknown/debug/wgpu_module.wasm --target web --out-dir site/src/generated --out-name wgpu_module
This will generate the js bindings and place them in the src
directory of our React project.
The site project is using typescript, so I'm generating typescript bindings, but if you want pure js you can use the --no-typescript
option on the bindgen command.
Now that everything is in place, the last thing we need to do is call the module we created from our react front-end code. We do this inside site/src/App.tsx
.
First we need to import the module:
import wgpu_module from "./generated/wgpu_module";
This imports the typescript module we generated with bindgen.
Next we need to call the module:
function App() {
useEffect(() => {
wgpu_module(); // Calls `__wbg_init()` from wasm-bindgen
}, []);
...
}
This will call the default function from our generated typescript module.
To understand what this is doing, we can look at our main
function inside wgpu_module/src/lib.rs
:
First we create an event loop:
let event_loop = EventLoop::new().unwrap();
Then we create a window object, bound to our html canvas
element:
let mut builder = winit::window::WindowBuilder::new();
#[cfg(target_arch = "wasm32")]
{
use wasm_bindgen::JsCast;
use winit::platform::web::WindowBuilderExtWebSys;
let canvas = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id("canvas")
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();
builder = builder.with_canvas(Some(canvas));
}
let window = builder.build(&event_loop).unwrap();
Notice we query the element by id with the call .get_element_by_id("canvas")
, so this requires that we have a canvas element with the correct id in our site:
<canvas id="canvas"/>
Finally, we spawn our task to render to the window with the event loop we created:
wasm_bindgen_futures::spawn_local(run(event_loop, window));
This will call our run
function, which renders to the window using our shader whenever a resize event, or redraw event is detected.