Skip to content

Instantly share code, notes, and snippets.

@17cupsofcoffee
Created January 7, 2021 23:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 17cupsofcoffee/7589c6c78cabb021d397e1e6022ead09 to your computer and use it in GitHub Desktop.
Save 17cupsofcoffee/7589c6c78cabb021d397e1e6022ead09 to your computer and use it in GitHub Desktop.
Triangle From Scratch (but it's web)

Triangle From Scratch (but it's web)

  1. Run cargo new triangle-from-scratch --lib to create the project.
  2. Make the library into a cdylib - that's the usual crate type for WASM.
    • You can do binary crates too, but IDK how well wasm-pack supports them, and the end result is basically the same, so may as well stay idiomatic.
  3. Add the dependencies:
    • wasm-bindgen generates bindings between the Rust and JS sides of your code, and saves you having to write a ton of glue code yourself.
      • It is possible to do WASM stuff without this, but it's a bit of a nightmare.
    • js-sys is bindings to the core JS APIs (i.e. stuff that exists in any environment, browser, Node, whatever).
      • We use this solely for Float32Array::view, which allows you to expose a Rust slice in a form we can pass to WebGL.
      • There's also a version of that WebGL function that takes &[u8] though, so we could just use that if we did some gross slice casting.
    • web-sys is bindings to the web platform APIs.
      • We use this for basically everything else (DOM manipulation, WebGL calls, etc).
      • Since the web platform is dang huge, most of this crate is behind feature flags to keep compile times down.
      • This could be removed if we wrote our own wasm-bindgen mappings, but that's not something I've done much of.
  4. Copy the code from the wasm-bindgen WebGL example into lib.rs.
    • TODO: This needs breaking down so the reader can actually write the code from scratch.
    • #[wasm_bindgen(start)] is nice because it makes it so the function auto-runs when you initialze the WASM module, kind of like a main function.
      • If you have a binary crate, main automatically gets this attribute applied, IIRC.
    • Everything else is pretty much just plain old Rust/WebGL code, hopefully it's understandable?
  5. Run wasm-pack build --target web to actually build the crate.
    • --target web builds a native JS module, instead of targeting bundlers like Webpack.
    • Native JS modules are supported basically everywhere that WASM is supported, so this is fine if we're not integrating into a larger JS project that already uses Webpack.
  6. Create a stub index.html with a canvas and some JS to actually load the built WASM.
    • The script tag has to be type="module" for it to be able to use native JS modules (and by extension, for it to be able to load our code).
    • The init is the default export from wasm-bindgen's generated code (name can be whatever you want) - it's an async function which you usually have to await before actually using any of the stuff in the WASM module. But because we did #[wasm_bindgen(start)] earlier, it automatically invokes our start function, so we can just fire-and-forget.
  7. Spin up a web server in the root of the project and navigate to index.html.
    • You can't just open index.html - browser vendors are making a big push to lock down what functionality is accessible via the file:// schema, and WASM/JS modules are completely blocked.
    • I just run Python 3's built-in server when I need a quick and dirty localhost server: python -m http.server
  8. Enjoy your triangle.
[package]
name = "triangle-from-scratch"
version = "0.1.0"
authors = ["Joe Clay <27cupsofcoffee@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.69"
js-sys = "0.3.46"
[dependencies.web-sys]
version = "0.3.46"
features = [
'Document',
'Element',
'HtmlCanvasElement',
'WebGlBuffer',
'WebGlRenderingContext',
'WebGlProgram',
'WebGlShader',
'Window',
]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Triangle From Scratch</title>
</head>
<body>
<canvas id="canvas" height="150" width="150"></canvas>
<script type="module">
import init from "./pkg/triangle_from_scratch.js";
init();
</script>
</body>
</html>
// The below code is taken from https://github.com/rustwasm/wasm-bindgen, under the terms of the
// MIT license:
//
// Copyright (c) 2014 Alex Crichton
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
// I've added a couple of additional annotations for the
// non-web-inclined (hello Lokathor).
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{WebGlProgram, WebGlRenderingContext, WebGlShader};
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id("canvas").unwrap();
// `dyn_into` comes from the `JsCast` trait, and lets you do a runtime-checked cast between
// JS types. For example, `get_element_by_id` gives us an `Element`, but to access the
// functionality of the `canvas` element, we need to cast it to the right subclass.
let canvas: web_sys::HtmlCanvasElement = canvas.dyn_into::<web_sys::HtmlCanvasElement>()?;
// We're using WebGL 1 here, but WebGL 2 probably wouldn't be *too* different for an example
// this simple.
let context = canvas
.get_context("webgl")?
.unwrap()
.dyn_into::<WebGlRenderingContext>()?;
let vert_shader = compile_shader(
&context,
WebGlRenderingContext::VERTEX_SHADER,
r#"
attribute vec4 position;
void main() {
gl_Position = position;
}
"#,
)?;
let frag_shader = compile_shader(
&context,
WebGlRenderingContext::FRAGMENT_SHADER,
r#"
void main() {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
"#,
)?;
let program = link_program(&context, &vert_shader, &frag_shader)?;
context.use_program(Some(&program));
let vertices: [f32; 9] = [-0.7, -0.7, 0.0, 0.7, -0.7, 0.0, 0.0, 0.7, 0.0];
let buffer = context.create_buffer().ok_or("failed to create buffer")?;
context.bind_buffer(WebGlRenderingContext::ARRAY_BUFFER, Some(&buffer));
// Note that `Float32Array::view` is somewhat dangerous (hence the
// `unsafe`!). This is creating a raw view into our module's
// `WebAssembly.Memory` buffer, but if we allocate more pages for ourself
// (aka do a memory allocation in Rust) it'll cause the buffer to change,
// causing the `Float32Array` to be invalid.
//
// As a result, after `Float32Array::view` we have to be very careful not to
// do any memory allocations before it's dropped.
unsafe {
let vert_array = js_sys::Float32Array::view(&vertices);
context.buffer_data_with_array_buffer_view(
WebGlRenderingContext::ARRAY_BUFFER,
&vert_array,
WebGlRenderingContext::STATIC_DRAW,
);
}
context.vertex_attrib_pointer_with_i32(0, 3, WebGlRenderingContext::FLOAT, false, 0, 0);
context.enable_vertex_attrib_array(0);
context.clear_color(0.0, 0.0, 0.0, 1.0);
context.clear(WebGlRenderingContext::COLOR_BUFFER_BIT);
context.draw_arrays(
WebGlRenderingContext::TRIANGLES,
0,
(vertices.len() / 3) as i32,
);
// Note we're not doing any `requestAnimationFrame` looping here - we just draw our triangle
// and then call it a day.
Ok(())
}
pub fn compile_shader(
context: &WebGlRenderingContext,
shader_type: u32,
source: &str,
) -> Result<WebGlShader, String> {
let shader = context
.create_shader(shader_type)
.ok_or_else(|| String::from("Unable to create shader object"))?;
context.shader_source(&shader, source);
context.compile_shader(&shader);
if context
.get_shader_parameter(&shader, WebGlRenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(shader)
} else {
Err(context
.get_shader_info_log(&shader)
.unwrap_or_else(|| String::from("Unknown error creating shader")))
}
}
pub fn link_program(
context: &WebGlRenderingContext,
vert_shader: &WebGlShader,
frag_shader: &WebGlShader,
) -> Result<WebGlProgram, String> {
let program = context
.create_program()
.ok_or_else(|| String::from("Unable to create shader object"))?;
context.attach_shader(&program, vert_shader);
context.attach_shader(&program, frag_shader);
context.link_program(&program);
if context
.get_program_parameter(&program, WebGlRenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(program)
} else {
Err(context
.get_program_info_log(&program)
.unwrap_or_else(|| String::from("Unknown error creating program object")))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment