- Run
cargo new triangle-from-scratch --lib
to create the project. - 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.
- 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.
- We use this solely for
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.
- Copy the code from the
wasm-bindgen
WebGL example intolib.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 amain
function.- If you have a binary crate,
main
automatically gets this attribute applied, IIRC.
- If you have a binary crate,
- Everything else is pretty much just plain old Rust/WebGL code, hopefully it's understandable?
- 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.
- Create a stub
index.html
with acanvas
and some JS to actually load the built WASM.- The
script
tag has to betype="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 fromwasm-bindgen
's generated code (name can be whatever you want) - it's an async function which you usually have toawait
before actually using any of the stuff in the WASM module. But because we did#[wasm_bindgen(start)]
earlier, it automatically invokes ourstart
function, so we can just fire-and-forget.
- The
- 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 thefile://
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
- You can't just open
- Enjoy your triangle.
Created
January 7, 2021 23:04
-
-
Save 17cupsofcoffee/7589c6c78cabb021d397e1e6022ead09 to your computer and use it in GitHub Desktop.
Triangle From Scratch (but it's web)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "triangle-from-scratch" | |
version = "0.1.0" | |
authors = ["Joe Clay <[email protected]>"] | |
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', | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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