Parc3l: Combining Three.js, Rust, and WebAssembly!
WebAssembly has been interesting me lately, specifically the prospect of doing arithmetically-intensive operations with it, not unlike the fantastic physics engine Emscripten port Ammo.js. Compiling something like that is out of the scope of this little post (but should be getting easier!), and I’ve left some links at the bottom so you can clear more about WebAssembly if you’re interested.
As a simple example I put together Parc3l (live demo). It uses the Parcel bundler tool as a really convenient way to combine Three.js code with Rust in the same web page.
All I did was install parcel
, create a few files, and I was up and running!
Let’s walk through the code:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Parc3l</title>
<link rel="stylesheet" href="main.scss">
</head>
<body class="main">
<script src="./index.js"></script>
</body>
</html>
This isn’t too bad, it’s a minimal HTML page. Notably, Parcel will replace index.js
with the final, bundledparcel.js
; likewise .scss
with a compiled .css
.
index.js
This is where most of the magic happens, let’s go through it one chunk at a time:
import './main.scss'import * as THREE from 'three';
import {add} from './wow.rs';
console.log(add(2, 3));let camera, scene, renderer;
let geometry, material, mesh;init();
animate();
All we do here at first is bring in other assets: our CSS, Three.js for 3D graphics, and this little wow.rs
file (a Rust file!) (the wow.rs
will get compiled to a .wasm
file when we run parcel build
so that it can be imported). Let’s console.log
right after the import to make sure exporting from Rust is working, so we call the add
function it defines (we’ll dig into this in the next section) and can see in the console the result:
Nearing the end of this top-level chunk we declare some global Three variables like camera
, scene
, and renderer
. Lastly we call the methods that kick-off rendering our scene: init()
and animate()
.
The whole init()
function that follows (in fact most of the file) originates from the “Creating a scene” exercise in the Three.js docs, so we won’t go over it again here. Picking back up at line 32:
let hello = document.createElement('div');
hello.id = "hello";
document.body.appendChild(hello);
Here we have lines that create and attach an element to the DOM that we’ll later use to output the calculations from our Three updates.
function animate() {requestAnimationFrame( animate );mesh.rotation.x = add(mesh.rotation.x, 0.01);
mesh.rotation.y = add(mesh.rotation.y, 0.02);if (hello) {
hello.innerText = `Hello from Rust! New rotation: {
x: ${mesh.rotation.x},
y: ${mesh.rotation.y}
}`;
}renderer.render( scene, camera );
}
And finally, our Rust code being called inside Javascript! Every 60 seconds we run this animate
function, and when we do we use a compiled function called add
to add two numbers together and assign the result back to the same variable, so really we’re incrementing the x and y rotation values by a small number every frame. In addition we update our #hello
text so we can see the updates as actual numbers.
(Note: In a real application we’d probably want to do computation like this from a WebAssembly module running in a WebWorker, but I wanted to keep things simple here.)
wow.rs
use std::f64;#[no_mangle]
pub fn add(a: f64, b: f64) -> f64 {
return a + b
}
And finally the new-comer: Rust! First we pull in the floating point type because we’ll be using it in our function. Next we use #[no_mangle]
above our function declaration so that the WASM compiler doesn’t optimize away our function name (which would mean we wouldn’t be able to call it from JS, defeating the whole purpose!) Our actual function is dead simple: take two floats, add ’em up, return that result.
Now, whenever we used add()
in the index.js
file above we call this pre-optimized version!
Serving it up!
This will all work fine when we run parcel serve
(which is what I was using when I was developing this project locally) but I wanted to host everything in Glitch so you could easily browse my code! In order to run on the server we’ll need a static build of our code, which is easy enough:
parcel build index.html --out-dir docs
And then we add:
start.js
const express = require('express') const app = express() app.use(express.static('docs')) app.listen(8000, () => console.log('Serving at http://localhost:8000!'))
Glitch runs this file automatically (via the npm start
command) to serve our assets. All it does is start a static server that spits out all the files in our docs
build directory. So with this we should see our files now, right? Well, not yet. When I went to parc3l.glitch.me while setting this tutorial up I was given a blank page, and this:
MIME types?? Yes, MIME types. In short, they’re a piece of information that tells the browser what file is coming that way the browser knows how to decode it!
If you’re like me, you’ve never worked with an experimental filetype before, meaning every file you’ve served before has had a MIME type built into whatever server you were using. Well, turns out adding a new MIME type on the fly to Express isn’t too hard:
// Need to add the .wasm MIME type explicitly as it's not standard yet
express.static.mime.types['wasm'] = 'application/wasm'
And voilà! We can now go to parc3l.glitch.me & see a glorious spinning cube, with the math being done by Rust via WebAssembly!
Serving on Github Pages
I made the build directory docs
so that I could host this on gh-pages from the Github repo, which works like magic! Wait, why? Why didn’t we need to fiddle with the MIME types (which we wouldn’t even have access to, because gh-pages only serves static assets). Well, it’s not magic after all. I got curious so I searched “github pages mime” and found this page. It notes that Github:
aggregates MIME types from the Apache and Nginx projects as well as the official IANA list of internet content types.
And we can even go see the line that adds the correct MIME type. Sweet!
This is a pretty trivial example but hopefully it gives you an idea of how easy it is to get up and running with Rust in the browser, specifically combining it with Javascript in order to update the page. You can view/remix the code over here, and feel free to reach out to me on Twitter with questions or feedback!
(Edit July 5, 2018): Now Glitch websites are embeddable directly on Medium, so here’s the website!
Some resources:
Rust:
- https://developer.mozilla.org/en-US/docs/WebAssembly
- A cartoon intro to WebAssembly
- Introduction to WebAssembly
JS bundlers:
- Parcel (specifically Parcel+Rust)
- Bankai
- Webpack
- Rollup