React + Rust + Wasm: WebGL2 Rendering

Neo Quest | Nikhil Gupta / October 31, 2022
4 min read
Summary
In this article, we will render a triangle in our React application from our Rust WASM library using WebGL2 APIs. We will build on the previous tutorial available here.
Add js-sys WebGL Features to web-sys
First of all, let's add js-sys and enable the required features for web-sys
dependency:
# Cargo.toml
[package]
name = "rust-wasm-lib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
js-sys = "0.3.60"
wasm-bindgen = "0.2.83"
[dependencies.web-sys]
version = "0.3.4"
features = [
'Document',
'Element',
'HtmlElement',
'Node',
'Window',
'CanvasRenderingContext2d',
'HtmlCanvasElement',
'WebGlBuffer',
'WebGlVertexArrayObject',
'WebGl2RenderingContext',
'WebGlProgram',
'WebGlShader'
]
WebGL Rendering using Rust
Last time, we exposed a function to render a circle using Canvas 2D APIs. Now, let's modify our lib.rs
to expose another function that will take the DOM Id of the canvas element, extract a WebGL2 context and render a triangle.
Create a WebGL2 context
#[wasm_bindgen]
pub fn render_with_web_gl(element_id: &str) -> Result<(), JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id(element_id).unwrap();
let canvas: web_sys::HtmlCanvasElement = canvas.dyn_into::<web_sys::HtmlCanvasElement>()?;
let context = canvas
.get_context("webgl2")?
.unwrap()
.dyn_into::<WebGl2RenderingContext>()?;
# We will fill this piece later
Ok(())
}
Add a function to compile WebGL shaders
fn compile_shader(
context: &WebGl2RenderingContext,
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, WebGl2RenderingContext::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")))
}
}
Add a function to link a WebGL program
fn link_program(
context: &WebGl2RenderingContext,
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, WebGl2RenderingContext::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")))
}
}
Add Vertex and Fragment Shaders for rendering a triangle
#[wasm_bindgen]
pub fn render_with_web_gl(element_id: &str) -> Result<(), JsValue> {
# ....
let vert_shader = compile_shader(
&context,
WebGl2RenderingContext::VERTEX_SHADER,
r##"#version 300 es
in vec4 position;
void main() {
gl_Position = position;
}
"##,
)?;
let frag_shader = compile_shader(
&context,
WebGl2RenderingContext::FRAGMENT_SHADER,
r##"#version 300 es
precision highp float;
out vec4 outColor;
void main() {
outColor = vec4(1, 1, 1, 1);
}
"##,
)?;
let program = link_program(&context, &vert_shader, &frag_shader)?;
context.use_program(Some(&program));
# ...
}
Create VAOs
#[wasm_bindgen]
pub fn render_with_web_gl(element_id: &str) -> Result<(), JsValue> {
# ....
let vertices: [f32; 9] = [-0.7, -0.7, 0.0, 0.7, -0.7, 0.0, 0.0, 0.7, 0.0];
let position_attribute_location = context.get_attrib_location(&program, "position");
let buffer = context.create_buffer().ok_or("Failed to create buffer")?;
context.bind_buffer(WebGl2RenderingContext::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 positions_array_buf_view = js_sys::Float32Array::view(&vertices);
context.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
&positions_array_buf_view,
WebGl2RenderingContext::STATIC_DRAW,
);
}
let vao = context
.create_vertex_array()
.ok_or("Could not create vertex array object")?;
context.bind_vertex_array(Some(&vao));
context.vertex_attrib_pointer_with_i32(
position_attribute_location as u32,
3,
WebGl2RenderingContext::FLOAT,
false,
0,
0,
);
context.enable_vertex_attrib_array(position_attribute_location as u32);
context.bind_vertex_array(Some(&vao));
let vert_count = (vertices.len() / 3) as i32;
draw(&context, vert_count); # Defined below
# ...
}
Actual rendering
fn draw(context: &WebGl2RenderingContext, vert_count: i32) {
context.clear_color(0.0, 0.0, 0.0, 1.0);
context.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
context.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, vert_count);
}
Build the new wasm library
Let's run wasm-pack again to build the updated library
wasm-pack build --target web
Call the new function from the demo app
Finally, let's add a canvas to our DOM and call the exported render
function from our App.ts
file like so:
// App.ts
import React, { useEffect } from 'react';
import init, { render_with_web_gl } from "rust-wasm-lib";
import './App.css';
function App() {
useEffect(() => {
init().then(() => {
render_with_web_gl("canvas");
});
}, [])
return (
<div className="App">
<canvas id="canvas" width={400} height={400} />
</div>
);
}
export default App;
Now, if you run the updated app, you should see a triangle on the screen. :)
If you liked this article, subscribe here to get the complete code and updates for the entire collection: Rust & Wasm