前前回と前回を通して、RustでのWebGLと行列計算のやり方を解説しました。最後にこれらを利用し、トーラス(ドーナッツみたいなやつ)を描画したいと思います。
ソースコードをこちらで公開しています。動いているところだけ見たいという方はこちらからどうぞ
前提条件
- Rustの基礎
- WebGLの基礎
- レンダリングの基礎
- JavaScriptの基礎
- 板ポリ1枚描画しただけで感動できるようなピュア(?)な心
謝辞
この記事の内容はWebGL 開発支援サイト wgld.orgのフォンシェーディングまでを参考に作りました。ありがとうございます。
1. 新規プロジェクトの作成
terminal
$ cargo new webgl --lib
2.vertex/fragmentシェーダーの作成
srcフォルダ内にshaderフォルダを作り、以下のvertexシェーダーとfragmentシェーダーを配置します。
vertex.vert
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mvpMatrix;
varying vec3 vNormal;
varying vec4 vColor;
void main(void) {
vNormal = normal;
vColor = color;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
fragment.frag
precision mediump float;
uniform mat4 invMatrix;
uniform vec3 lightDirection;
uniform vec3 eyeDirection;
uniform vec4 ambientColor;
varying vec3 vNormal;
varying vec4 vColor;
void main(void){
vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
vec3 invEye = normalize(invMatrix * vec4(eyeDirection, 0.0)).xyz;
vec3 halfLE = normalize(invLight + invEye);
float diffuse = clamp(dot(vNormal, invLight), 0.0, 1.0);
float specular = pow(clamp(dot(vNormal, halfLE), 0.0, 1.0), 50.0);
vec4 destColor = vColor * vec4(vec3(diffuse), 1.0) + vec4(vec3(specular), 1.0) + ambientColor;
gl_FragColor = destColor;
}
3. トーラスの3dモデルを生成する
こちらを参考にしました。
shapes.rs
pub fn torus(row: u16, column: u16, i_rad: f32, o_rad: f32) -> (Vec<f32>, Vec<f32>, Vec<f32>, Vec<u16>) {
let mut pos = Vec::new();
let mut nor = Vec::new();
let mut col = Vec::new();
let mut idx = Vec::new();
const PI: f32 = std::f32::consts::PI;
for i in 0..=row {
let r = PI * 2. / row as f32 * i as f32;
let rr = r.cos();
let ry = r.sin();
for ii in 0..=column {
let tr = PI * 2. / column as f32 * ii as f32;
let tx = (rr * i_rad + o_rad) * tr.cos();
let ty = ry * i_rad;
let tz = (rr * i_rad + o_rad) * tr.sin();
let rx = rr * tr.cos();
let rz = rr * tr.sin();
pos.push(tx);
pos.push(ty);
pos.push(tz);
nor.push(rx);
nor.push(ry);
nor.push(rz);
let tc = hsva(360. / column as f32 * ii as f32, 1., 1., 1.).unwrap();
for c in tc {
col.push(c);
}
}
}
for i in 0..row {
for ii in 0..column {
let r = (column + 1) * i + ii;
idx.push(r);
idx.push(r + column + 1);
idx.push(r + 1);
idx.push(r + column + 1);
idx.push(r + column + 2);
idx.push(r + 1);
}
}
(pos, nor, col, idx)
}
fn hsva(h: f32, s: f32, v: f32, a: f32) -> Result<[f32; 4], String> {
if s > 1. || v > 1. || a > 1. {
return Err("invalid value".to_string());
}
if s == 0. {
return Ok([v, v, v, a]);
}
let th = h % 360.;
let i = (th / 60.).floor() as usize;
let f = th / 60. - i as f32;
let m = v * (1. - s);
let n = v * (1. - s * f);
let k = v * (1. - s * (1. - f));
let r = [v, n, m, m, k, v];
let g = [k, v, v, n, m, m];
let b = [m, m, k, v, v, n];
Ok([r[i], g[i], b[i], a])
}
4. 行列計算
🦀Rust + 💻WebGL = 💛 2. 行列計算編を参照してください。
mat_4.rs
pub struct Matrix {
value: [f32; 16],
}
#[allow(dead_code)]
impl Matrix {
//--create new matrix--
// <return> Matrix
pub fn new() -> Self {
Self {
value: [
1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
] as [f32; 16],
}
}
//--Get matrix value--
// <return> [f32; 16]
pub fn get_value(&self) -> [f32; 16] {
self.value
}
//--Set a value in the matrix
// <argument>
// m &[f32; 16] : value to set
pub fn set_value(&mut self, m: &[f32; 16]) -> &mut Self {
self.value = *m;
self
}
//--Set the identity matrix in the matrix
pub fn set_identity(&mut self) -> &mut Self {
self.value = [
1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
];
self
}
//--Set other matrix value in the matrix
// <argument>
// m &Matrix : other matrix to set
pub fn substitution(&mut self, m: &Matrix) -> &mut Self {
self.value = m.value;
self
}
//--Multiply other matrix
// <argument>
// m &Matrix : other matrix to multiply
// <note>
// Column-major order!
pub fn multiply(&mut self, m: &Matrix) -> &mut Self {
let mut dest: [f32; 16] = [0.; 16];
dest[0] = m.value[0] * self.value[0]
+ m.value[1] * self.value[4]
+ m.value[2] * self.value[8]
+ m.value[3] * self.value[12];
dest[1] = m.value[0] * self.value[1]
+ m.value[1] * self.value[5]
+ m.value[2] * self.value[9]
+ m.value[3] * self.value[13];
dest[2] = m.value[0] * self.value[2]
+ m.value[1] * self.value[6]
+ m.value[2] * self.value[10]
+ m.value[3] * self.value[14];
dest[3] = m.value[0] * self.value[3]
+ m.value[1] * self.value[7]
+ m.value[2] * self.value[11]
+ m.value[3] * self.value[15];
dest[4] = m.value[4] * self.value[0]
+ m.value[5] * self.value[4]
+ m.value[6] * self.value[8]
+ m.value[7] * self.value[12];
dest[5] = m.value[4] * self.value[1]
+ m.value[5] * self.value[5]
+ m.value[6] * self.value[9]
+ m.value[7] * self.value[13];
dest[6] = m.value[4] * self.value[2]
+ m.value[5] * self.value[6]
+ m.value[6] * self.value[10]
+ m.value[7] * self.value[14];
dest[7] = m.value[4] * self.value[3]
+ m.value[5] * self.value[7]
+ m.value[6] * self.value[11]
+ m.value[7] * self.value[15];
dest[8] = m.value[8] * self.value[0]
+ m.value[9] * self.value[4]
+ m.value[10] * self.value[8]
+ m.value[11] * self.value[12];
dest[9] = m.value[8] * self.value[1]
+ m.value[9] * self.value[5]
+ m.value[10] * self.value[9]
+ m.value[11] * self.value[13];
dest[10] = m.value[8] * self.value[2]
+ m.value[9] * self.value[6]
+ m.value[10] * self.value[10]
+ m.value[11] * self.value[14];
dest[11] = m.value[8] * self.value[3]
+ m.value[9] * self.value[7]
+ m.value[10] * self.value[11]
+ m.value[11] * self.value[15];
dest[12] = m.value[12] * self.value[0]
+ m.value[13] * self.value[4]
+ m.value[14] * self.value[8]
+ m.value[15] * self.value[12];
dest[13] = m.value[12] * self.value[1]
+ m.value[13] * self.value[5]
+ m.value[14] * self.value[9]
+ m.value[15] * self.value[13];
dest[14] = m.value[12] * self.value[2]
+ m.value[13] * self.value[6]
+ m.value[14] * self.value[10]
+ m.value[15] * self.value[14];
dest[15] = m.value[12] * self.value[3]
+ m.value[13] * self.value[7]
+ m.value[14] * self.value[11]
+ m.value[15] * self.value[15];
self.value = dest;
self
}
//--Transpose the matrix--
pub fn transpose(&mut self) -> &mut Self {
self.value = [
self.value[0],
self.value[4],
self.value[8],
self.value[12],
self.value[1],
self.value[5],
self.value[9],
self.value[13],
self.value[2],
self.value[6],
self.value[10],
self.value[14],
self.value[3],
self.value[7],
self.value[11],
self.value[15],
];
self
}
//--Create translation matrix and multiply it--
pub fn translation(&mut self, v: &[f32; 3]) -> &mut Self {
let translation_mat = Matrix {
value: [
1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., v[0], v[1], v[2], 1.,
],
};
self.multiply(&translation_mat);
self
}
//--Create scaling matrix and multiply it--
pub fn scaling(&mut self, v: &[f32; 3]) -> &mut Self {
let scaling_mat = Matrix {
value: [
v[0], 0., 0., 0., 0., v[1], 0., 0., 0., 0., v[2], 0., 0., 0., 0., 1.,
],
};
self.multiply(&scaling_mat);
self
}
pub fn rotate_around_y(&mut self, rad: f32) -> &mut Self {
let r_c = rad.cos();
let r_s = rad.sin();
let y_mut = Matrix {
value: [
r_c, 0., r_s, 0., 0., 1., 0., 0., -r_s, 0., r_c, 0., 0., 0., 0., 1.,
],
};
self.multiply(&y_mut);
self
}
pub fn rotate_around_x(&mut self, rad: f32) -> &mut Self {
let r_c = rad.cos();
let r_s = rad.sin();
let x_mut = Matrix {
value: [
r_c, 0., r_s, 0., 0., 1., 0., 0., -r_s, 0., r_c, 0., 0., 0., 0., 1.,
],
};
self.multiply(&x_mut);
self
}
pub fn rotate_around_z(&mut self, rad: f32) -> &mut Self {
let r_c = rad.cos();
let r_s = rad.sin();
let z_mut = Matrix {
value: [
r_c, -r_s, 0., 0., r_s, r_c, 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
],
};
self.multiply(&z_mut);
self
}
//--Create look_at matrix--
pub fn look_at(&mut self, from: &[f32; 3], to: &[f32; 3], up: &[f32; 3]) -> &mut Self {
let mut l: f32;
let mut x: [f32; 3] = [0.; 3];
let mut y: [f32; 3] = [0.; 3];
let mut z: [f32; 3] = [0.; 3];
z[0] = from[0] - to[0];
z[1] = from[1] - to[1];
z[2] = from[2] - to[2];
l = (z[0] * z[0] + z[1] * z[1] + z[2] * z[2]).sqrt().recip();
z[0] *= l;
z[1] *= l;
z[2] *= l;
x[0] = up[1] * z[2] - up[2] * z[1];
x[1] = up[2] * z[0] - up[0] * z[2];
x[2] = up[0] * z[1] - up[1] * z[0];
l = (x[0] * x[0] + x[1] * x[1] + x[2] * x[2]).sqrt().recip();
x[0] *= l;
x[1] *= l;
x[2] *= l;
y[0] = z[1] * x[2] - z[2] * x[1];
y[1] = z[2] * x[0] - z[0] * x[2];
y[2] = z[0] * x[1] - z[1] * x[0];
l = (y[0] * y[0] + y[1] * y[1] + y[2] * y[2]).sqrt().recip();
y[0] *= l;
y[1] *= l;
y[2] *= l;
let d_12: f32 = -(x[0] * from[0] + x[1] * from[1] + x[2] * from[2]);
let d_13: f32 = -(y[0] * from[0] + y[1] * from[1] + y[2] * from[2]);
let d_14: f32 = -(z[0] * from[0] + z[1] * from[1] + z[2] * from[2]);
self.value = [
x[0], y[0], z[0], 0.,
x[1], y[1], z[1], 0.,
x[2], y[2], z[2], 0.,
d_12, d_13, d_14, 1.,
];
self
}
//--Create perspective projections matrix--
// <argument>
// aspect f32 ratio parameter is the width divided by the height
// fovy f32 field of view y-axis
// near f32 near clipping plane
// far f32 far clipping plane
// <note>
// Right-Handed Coordinate System!
pub fn perspective(&mut self, aspect: f32, fovy: f32, near: f32, far: f32) -> &mut Self {
let mut dest: [f32; 16] = [0.; 16];
let t: f32 = (fovy / 2.).tan();
let d: f32 = far - near;
dest[0] = 1. / (t * aspect);
dest[5] = 1. / t;
dest[10] = - far / d;
dest[11] = -1.;
dest[14] = - far * near / d;
self.value = dest;
self
}
//--inverse the matrix
pub fn inverse(&mut self) -> Result<&mut Self, i8> {
const SIZE: usize = 4;
let mut inv = Matrix::identity();
let mut buf: f32;
let mut a = self.value;
for i in 0..SIZE {
if a[i * SIZE + i] == 0. {
return Err(-1);
}
buf = 1. / a[i * SIZE + i];
for j in 0..SIZE {
a[i * SIZE + j] *= buf;
inv[i * SIZE + j] *= buf;
}
for j in 0..SIZE {
if i != j {
buf = a[j * SIZE + i];
for k in 0..SIZE {
a[j * SIZE + k] -= a[i * SIZE + k] * buf;
inv[j * SIZE + k] -= inv[i * SIZE + k] * buf;
}
}
}
}
self.value = inv;
Ok(self)
}
fn identity() -> [f32; 16] {
[
1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
] as [f32; 16]
}
}
5. WebGLの操作
🦀Rust + 💻WebGL = 💛 1.WebGL基本操作編を参照してください。
webgl.rs
use wasm_bindgen::JsCast;
use web_sys::WebGlRenderingContext as GL;
use web_sys::*;
pub fn get_webgl_context(height: u32, width: u32) -> Result<WebGlRenderingContext, String> {
//Get WebGLContext
let document = window().unwrap().document().unwrap();
let canvas = document
.get_element_by_id("canvas")
.ok_or_else(|| String::from("canvas doesn't exist :("))?;
let canvas: web_sys::HtmlCanvasElement =
canvas.dyn_into::<web_sys::HtmlCanvasElement>().unwrap();
canvas.set_height(height);
canvas.set_width(width);
let gl: WebGlRenderingContext = canvas
.get_context("webgl")
.unwrap()
.ok_or_else(|| String::from("webgl is not supported in this browser :("))?
.dyn_into()
.unwrap();
//Initialize WebGLContext
gl.enable(GL::BLEND);
gl.blend_func(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA);
gl.clear_color(0.0, 0.0, 0.0, 1.0); //RGBA
gl.clear_depth(1.);
Ok(gl)
}
pub fn link_program(
gl: &WebGlRenderingContext,
vert_source: &str,
frag_source: &str,
) -> Result<WebGlProgram, String> {
let program = gl
.create_program()
.ok_or_else(|| String::from("Error creating program"))?;
let vert_shader = compile_shader(&gl, GL::VERTEX_SHADER, vert_source).unwrap();
let frag_shader = compile_shader(&gl, GL::FRAGMENT_SHADER, frag_source).unwrap();
gl.attach_shader(&program, &vert_shader);
gl.attach_shader(&program, &frag_shader);
gl.link_program(&program);
if gl
.get_program_parameter(&program, WebGlRenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
gl.use_program(Some(&program));
Ok(program)
} else {
Err(gl
.get_program_info_log(&program)
.unwrap_or_else(|| String::from("Unknown error creating program object")))
}
}
fn compile_shader(
gl: &WebGlRenderingContext,
shader_type: u32,
source: &str,
) -> Result<WebGlShader, String> {
let shader = gl
.create_shader(shader_type)
.ok_or_else(|| String::from("Error creating shader"))?;
gl.shader_source(&shader, source);
gl.compile_shader(&shader);
if gl
.get_shader_parameter(&shader, WebGlRenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(shader)
} else {
Err(gl
.get_shader_info_log(&shader)
.unwrap_or_else(|| String::from("Unable to get shader info log")))
}
}
#[allow(dead_code)]
pub fn create_vbo_array(gl: &GL, data: &[f32]) -> Result<WebGlBuffer, String> {
let vbo = gl.create_buffer().ok_or("Failed to create buffer :(")?;
gl.bind_buffer(GL::ARRAY_BUFFER, Some(&vbo));
unsafe {
let f32_array = js_sys::Float32Array::view(data);
gl.buffer_data_with_array_buffer_view(GL::ARRAY_BUFFER, &f32_array, GL::STATIC_DRAW)
}
gl.bind_buffer(GL::ARRAY_BUFFER, None);
Ok(vbo)
}
pub fn create_vbo_vector(gl: &GL, data: &Vec<f32>) -> Result<WebGlBuffer, String> {
let vbo = gl.create_buffer().ok_or("Failed to create buffer :(")?;
gl.bind_buffer(GL::ARRAY_BUFFER, Some(&vbo));
unsafe {
let f32_array = js_sys::Float32Array::view(&(*data));
gl.buffer_data_with_array_buffer_view(GL::ARRAY_BUFFER, &f32_array, GL::STATIC_DRAW)
}
gl.bind_buffer(GL::ARRAY_BUFFER, None);
Ok(vbo)
}
#[allow(dead_code)]
pub fn create_ibo_array(gl: &GL, data: &[u16]) -> Result<WebGlBuffer, String> {
let ibo = gl.create_buffer().ok_or("Failed to create buffer :(")?;
gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, Some(&ibo));
unsafe {
let ui16_array = js_sys::Uint16Array::view(data);
gl.buffer_data_with_array_buffer_view(
GL::ELEMENT_ARRAY_BUFFER,
&ui16_array,
GL::STATIC_DRAW,
);
}
gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, None);
Ok(ibo)
}
pub fn create_ibo_vector(gl: &GL, data: &Vec<u16>) -> Result<WebGlBuffer, String> {
let ibo = gl.create_buffer().ok_or("Failed to create buffer")?;
gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, Some(&ibo));
unsafe {
let ui16_array = js_sys::Uint16Array::view(&(*data));
gl.buffer_data_with_array_buffer_view(
GL::ELEMENT_ARRAY_BUFFER,
&ui16_array,
GL::STATIC_DRAW,
);
}
gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, None);
Ok(ibo)
}
pub fn set_attribute(gl: &GL, vbo: &[WebGlBuffer], att_location: &[u32], att_stride: &[i32]) {
for i in 0..vbo.len() {
gl.bind_buffer(GL::ARRAY_BUFFER, Some(&vbo[i]));
gl.enable_vertex_attrib_array(att_location[i]);
gl.vertex_attrib_pointer_with_i32(att_location[i], att_stride[i], GL::FLOAT, false, 0, 0);
}
}
6. lib.rsの編集
lib.rs
use wasm_bindgen::prelude::*;
use wasm_bindgen::*;
use web_sys::WebGlRenderingContext as GL;
mod mat_4;
mod shapes;
mod webgl;
fn request_animation_frame(f: &Closure<dyn FnMut()>) {
web_sys::window()
.unwrap()
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("should register 'requestAnimationFrame'");
}
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let height: f32 = 500.;
let width: f32 = 500.;
//-----Get context
let gl = webgl::get_webgl_context(height as u32, width as u32).unwrap();
//-----Compile and link program
let program = webgl::link_program(
&gl,
include_str!("shader/vertex.vert"),
include_str!("shader/fragment.frag"),
)
.unwrap();
//Create vertex buffer object
let att_location: [u32; 3] = [
gl.get_attrib_location(&program, "position") as u32,
gl.get_attrib_location(&program, "normal") as u32,
gl.get_attrib_location(&program, "color") as u32,
];
let att_stride: [i32; 3] = [3, 3, 4];
let torus_data = shapes::torus(32, 32, 1.0, 2.0);
let position = torus_data.0;
let normal = torus_data.1;
let color = torus_data.2;
let index = torus_data.3;
//Crate and set vbo
let position_vbo = webgl::create_vbo_vector(&gl, &position).unwrap();
let normal_vbo = webgl::create_vbo_vector(&gl, &normal).unwrap();
let color_vbo = webgl::create_vbo_vector(&gl, &color).unwrap();
webgl::set_attribute(
&gl,
&[position_vbo, normal_vbo, color_vbo],
&att_location,
&att_stride,
);
//Crate and set ibo
let ibo = webgl::create_ibo_vector(&gl, &index).unwrap();
gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, Some(&ibo));
//Model, view and projection transformation
let uni_location = [
gl.get_uniform_location(&program, "mvpMatrix").unwrap(),
gl.get_uniform_location(&program, "invMatrix").unwrap(),
gl.get_uniform_location(&program, "lightDirection").unwrap(),
gl.get_uniform_location(&program, "eyeDirection").unwrap(),
gl.get_uniform_location(&program, "ambientColor").unwrap(),
];
gl.enable(GL::DEPTH_TEST);
gl.enable(GL::CULL_FACE);
gl.depth_func(GL::LEQUAL);
let mut m_matrix = mat_4::Matrix::new();
let mut v_matrix = mat_4::Matrix::new();
let mut p_matrix = mat_4::Matrix::new();
let mut mvp_matrix = mat_4::Matrix::new();
let mut tmp_matrix = mat_4::Matrix::new();
let mut inv_matrix = mat_4::Matrix::new();
let eye_direction = [0., 0., 15.];
v_matrix.look_at(&eye_direction, &[0., 0., 0.], &[0., 1., 0.]);
p_matrix.perspective(width / height, 45., 0.1, 100.);
tmp_matrix.substitution(&p_matrix).multiply(&v_matrix);
let light_direction = [-0.5, 0.5, 0.5];
let ambient_color = [0.1, 0.1, 0.1, 1.0];
//call once per animation frame
let f = std::rc::Rc::new(std::cell::RefCell::new(None));
let g = f.clone();
let mut i: f32 = 0.;
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
if i >= 360. {
i = 0.;
}
i += 1.;
//Webgl initialize
gl.clear_color(0.0, 0.0, 0.0, 1.0);
gl.clear_depth(1.);
gl.clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT);
let rad = i * std::f32::consts::PI / 180.;
//Draw by element
m_matrix
.set_identity()
.rotate_around_y(rad)
.rotate_around_z(rad);
mvp_matrix.substitution(&tmp_matrix).multiply(&m_matrix);
inv_matrix.substitution(&m_matrix).inverse().unwrap();
gl.uniform_matrix4fv_with_f32_array(Some(&uni_location[0]), false, &mvp_matrix.get_value());
gl.uniform_matrix4fv_with_f32_array(Some(&uni_location[1]), false, &inv_matrix.get_value());
gl.uniform3fv_with_f32_array(Some(&uni_location[2]), &light_direction);
gl.uniform3fv_with_f32_array(Some(&uni_location[3]), &eye_direction);
gl.uniform4fv_with_f32_array(Some(&uni_location[4]), &ambient_color);
gl.draw_elements_with_i32(GL::TRIANGLES, index.len() as i32, GL::UNSIGNED_SHORT, 0);
//Context redrawn
gl.flush();
request_animation_frame(f.borrow().as_ref().unwrap());
}) as Box<dyn FnMut()>));
request_animation_frame(g.borrow().as_ref().unwrap());
Ok(())
}
7. コンパイル & 呼び出し
まず、WebAssemblyにコンパイルします。
terminal
$ wasm-pack build --target web
そしたら以下のhtmlファイルで呼び出します。
index.html
<!DOCTYPE html>
<html>
<head>
<title>WebGL & Rust</title>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module">
import init from './pkg/webgl.js';
async function start() {
await init();
};
start();
</script>
</body>
</html>
そうしたら、ローカルでサーバーを立ち上げます。VScodeの拡張機能であるLive Serverが手軽で良いでしょう。
まとめ
始めはなかなか難しいですが、慣れればRustって凄い書きやすいなーと感じます。皆さんもRust + WebGLでグラフィックスプログラミング、始めませんか?