invaders/src/render.rs

464 lines
16 KiB
Rust

use std::{mem::size_of, rc::Rc};
use js_sys::{Float32Array, Uint16Array, WebAssembly::Memory};
use wasm_bindgen::{prelude::Closure, JsCast, JsValue};
use web_sys::{
window, HtmlCanvasElement, HtmlImageElement, WebGl2RenderingContext,
WebGl2RenderingContext as Gl, WebGlBuffer, WebGlProgram, WebGlShader, WebGlTexture,
};
use crate::utils;
/// stores the rendering context and data required for rendering the scene
pub struct Renderer {
// the webgl2 rendering context obtained from the canvas
gl: WebGl2RenderingContext,
canvas: HtmlCanvasElement,
// rendering data: buffers, shader program and texture. stored as `Option`s since they still
// need to be set by the user after initializing the renderer.
buffer: Option<Buffer>,
shader_program: Option<WebGlProgram>,
texture: Option<Rc<WebGlTexture>>,
}
/// store vertex and index buffer
pub struct Buffer {
// vertex and index buffer
vertex_buffer: WebGlBuffer,
index_buffer: WebGlBuffer,
// number of elements inside the index buffer
index_len: usize,
}
impl Renderer {
/// create a new renderer for the canvas with the id `canvas_id`.
pub fn new(canvas_id: &str) -> Result<Renderer, JsValue> {
let document = window().unwrap().document().unwrap();
// obtain the canvas element
let canvas = document
.get_element_by_id(canvas_id)
.ok_or("failed to get canvas element")?
.dyn_into::<HtmlCanvasElement>()?;
// obtain a webgl2 graphics context
let gl = canvas
.get_context("webgl2")?
.ok_or("failed to get graphics context")?
.dyn_into::<WebGl2RenderingContext>()?;
Ok(Renderer {
gl,
canvas,
buffer: None,
shader_program: None,
texture: None,
})
}
/// draw the scene.
pub fn draw(&self) -> Result<(), JsValue> {
// create shorthands for the rendering context and rendering data
let gl = &self.gl;
let buffer = self.buffer.as_ref().ok_or("no buffer is specified")?;
let shader_program = self
.shader_program
.as_ref()
.ok_or("no shader program is specified")?;
let texture = self
.texture
.as_ref()
.map(|x| unsafe { Rc::downgrade(x).as_ptr().as_ref() }.unwrap());
// set the background color of the canvas to black
gl.clear_color(0.0, 0.0, 0.0, 1.0);
gl.clear(Gl::COLOR_BUFFER_BIT);
// apply the shader program
gl.use_program(Some(shader_program));
// bind the vertex and array buffer to ARRAY_BUFFER and ELEMENT_ARRAY_BUFFER, respectively,
// so that opengl knows which data we want to access
gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&buffer.vertex_buffer));
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_buffer));
// bind the texture
gl.active_texture(Gl::TEXTURE0);
gl.bind_texture(Gl::TEXTURE_2D, texture);
let sampler_location = gl
.get_uniform_location(shader_program, "uTexture")
.ok_or("failed to get uniform location")?;
gl.uniform1i(Some(&sampler_location), 0);
// set the viewport to the width and height of the canvas
gl.viewport(
0,
0,
self.canvas.width() as i32,
self.canvas.height() as i32,
);
// draw the vertices as triangles
gl.draw_elements_with_i32(
Gl::TRIANGLES,
buffer.index_len as i32,
Gl::UNSIGNED_SHORT,
0,
);
// unbind buffers and textures
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
gl.bind_texture(Gl::TEXTURE_2D, None);
Ok(())
}
/// initialize the buffer contents
pub fn initialize_buffer(
&mut self,
vertices: &Vec<f32>,
indices: &Vec<u16>,
static_draw: bool,
) -> Result<(), JsValue> {
// create vertex and index buffers based on the arrays from the function parameters
let vertex_buffer = self.create_vertex_buffer(vertices, static_draw)?;
let index_buffer = self.create_index_buffer(indices, static_draw)?;
self.buffer = Some(Buffer {
vertex_buffer,
index_buffer,
index_len: indices.len(),
});
Ok(())
}
/// create a vertex buffer
fn create_vertex_buffer(
&self,
buffer_data: &Vec<f32>,
static_draw: bool,
) -> Result<WebGlBuffer, JsValue> {
let gl = &self.gl;
// create a javascript `Float32Array` to store the data in
let vertices_array = {
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
let loc = buffer_data.as_ptr() as u32 / 4;
Float32Array::new(&memory_buffer).subarray(loc, loc + buffer_data.len() as u32)
};
// specify the draw mode. `STATIC_DRAW` specifies that the buffer data is set only once and
// used many times. `DYNAMIC_DRAW` is used when the data is changed frequently.
let draw_mode = match static_draw {
true => Gl::STATIC_DRAW,
false => Gl::DYNAMIC_DRAW,
};
// create the buffer
let vertex_buffer = gl.create_buffer().ok_or("failed to create buffer")?;
// bind the buffer to `ARRAY_BUFFER` and write the data to it
gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&vertex_buffer));
gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &vertices_array, draw_mode);
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
Ok(vertex_buffer)
}
/// create an index buffer
fn create_index_buffer(
&self,
buffer_data: &Vec<u16>,
static_draw: bool,
) -> Result<WebGlBuffer, JsValue> {
let gl = &self.gl;
// create a javascript `Uint16Array` to store the data in
let indices_array = {
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
let loc = buffer_data.as_ptr() as u32 / 2;
Uint16Array::new(&memory_buffer).subarray(loc, loc + buffer_data.len() as u32)
};
// specify the draw mode. `STATIC_DRAW` specifies that the buffer data is set only once and
// used many times. `DYNAMIC_DRAW` is used when the data is changed frequently.
let draw_mode = match static_draw {
true => Gl::STATIC_DRAW,
false => Gl::DYNAMIC_DRAW,
};
// create the buffer
let index_buffer = gl.create_buffer().ok_or("failed to create buffer")?;
// bind the buffer to `ELEMENT_ARRAY_BUFFER` and write the data to it
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&index_buffer));
gl.buffer_data_with_array_buffer_view(Gl::ELEMENT_ARRAY_BUFFER, &indices_array, draw_mode);
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
Ok(index_buffer)
}
/// update the buffer contents
pub fn update_buffer(
&mut self,
vertices: &Vec<f32>,
indices: &Vec<u16>,
static_draw: bool,
) -> Result<(), JsValue> {
let gl = &self.gl;
let buffer = self.buffer.as_mut().ok_or("no buffer is specified")?;
// create a javascript `Float32Array` to store the vertex array data in
let vertices_array = {
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
let loc = vertices.as_ptr() as u32 / 4;
Float32Array::new(&memory_buffer).subarray(loc, loc + vertices.len() as u32)
};
// create a javascript `Uint16Array` to store the index array data in
let indices_array = {
let memory_buffer = wasm_bindgen::memory().dyn_into::<Memory>()?.buffer();
let loc = indices.as_ptr() as u32 / 2;
Uint16Array::new(&memory_buffer).subarray(loc, loc + indices.len() as u32)
};
// specify the draw mode. `STATIC_DRAW` specifies that the buffer data is set only once and
// used many times. `DYNAMIC_DRAW` is used when the data is changed frequently.
let draw_mode = match static_draw {
true => Gl::STATIC_DRAW,
false => Gl::DYNAMIC_DRAW,
};
// bind the vertex buffer to `ARRAY_BUFFER` and write the data into it
gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&buffer.vertex_buffer));
gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &vertices_array, draw_mode);
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
// bind the index buffer to `ELEMENT_ARRAY_BUFFER` and write the data into it
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_buffer));
gl.buffer_data_with_array_buffer_view(Gl::ELEMENT_ARRAY_BUFFER, &indices_array, draw_mode);
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
// obtain the length of the index array
buffer.index_len = indices.len();
Ok(())
}
/// initialize the shader program
pub fn set_shader_program(
&mut self,
vertex_src: &str,
fragment_src: &str,
) -> Result<(), JsValue> {
let gl = &self.gl;
// compile vertex and fragment shader
let vertex_shader = self.compile_shader(Gl::VERTEX_SHADER, vertex_src)?;
let fragment_shader = self.compile_shader(Gl::FRAGMENT_SHADER, fragment_src)?;
// link the shader program
let shader_program = self.link_shader_program(&vertex_shader, &fragment_shader)?;
// delete the shaders since we no longer need them after linking the program
gl.delete_shader(Some(&vertex_shader));
gl.delete_shader(Some(&fragment_shader));
self.shader_program = Some(shader_program);
Ok(())
}
/// compile an opengl shader
fn compile_shader(&self, shader_type: u32, source: &str) -> Result<WebGlShader, String> {
let gl = &self.gl;
// create the shader
let shader = gl
.create_shader(shader_type)
.ok_or("unable to create shader object")?;
// add the shader source code and compile it
gl.shader_source(&shader, source);
gl.compile_shader(&shader);
// check if the compilation was successful
if gl
.get_shader_parameter(&shader, Gl::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(shader)
} else {
Err(gl
.get_shader_info_log(&shader)
.unwrap_or_else(|| String::from("unknown error creating shader")))
}
}
/// link a shader program consisting of a vertex and a fragment shader
fn link_shader_program(
&self,
vert_shader: &WebGlShader,
frag_shader: &WebGlShader,
) -> Result<WebGlProgram, String> {
let gl = &self.gl;
// create the program
let program = gl
.create_program()
.ok_or("unable to create shader program")?;
// attach the shaders and link them together
gl.attach_shader(&program, vert_shader);
gl.attach_shader(&program, frag_shader);
gl.link_program(&program);
// check if the linking was successful
if gl
.get_program_parameter(&program, Gl::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(program)
} else {
Err(gl
.get_program_info_log(&program)
.unwrap_or_else(|| String::from("unknown error linking shader program")))
}
}
/// map a shader attribute to a position in the vertex data
pub fn map_shader_attribute(
&self,
attribute_name: &str,
size: i32,
stride: i32,
offset: i32,
) -> Result<(), JsValue> {
// define shorthands for rendering context and rendering data
let gl = &self.gl;
let buffer = self.buffer.as_ref().ok_or("no buffer is specified")?;
let shader_program = self
.shader_program
.as_ref()
.ok_or("no shader program is specified")?;
// bind the buffers we want to map
gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&buffer.vertex_buffer));
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&buffer.index_buffer));
// get the location of the attribute within the shader
let attribute = gl.get_attrib_location(shader_program, attribute_name) as u32;
// map the attribute to the vertex buffer data, specifying the size, stride and offset of
// the data elements.
gl.vertex_attrib_pointer_with_i32(
attribute,
size,
Gl::FLOAT,
false,
stride * size_of::<f32>() as i32,
offset * size_of::<f32>() as i32,
);
// enable the attribute since it is not enabled by default
gl.enable_vertex_attrib_array(attribute);
// unbind the buffers
gl.bind_buffer(Gl::ARRAY_BUFFER, None);
gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, None);
Ok(())
}
/// initialize the texture
pub fn set_texture(&mut self, path: &str) -> Result<(), JsValue> {
let texture = self.load_texture(path)?;
self.texture = Some(texture);
Ok(())
}
/// load the texture from the internet
fn load_texture(&self, path: &str) -> Result<Rc<WebGlTexture>, JsValue> {
let gl = &self.gl;
// create and bind the texture
let texture = gl.create_texture().ok_or("failed to create texture")?;
gl.bind_texture(Gl::TEXTURE_2D, Some(&texture));
// temporarily create a black rectangle since we need to wait for the texture to be
// transferred over the internet
let pixel: [u8; 4] = [0, 0, 0, 255];
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
0,
Gl::RGBA as i32,
1,
1,
0,
Gl::RGBA,
Gl::UNSIGNED_BYTE,
Some(&pixel),
)?;
// create an `<img>` element
let img = HtmlImageElement::new().unwrap();
img.set_cross_origin(Some(""));
let imgrc = Rc::new(img);
let texture = Rc::new(texture);
{
let img = imgrc.clone();
let texture = texture.clone();
let gl = Rc::new(gl.clone());
let a = Closure::wrap(Box::new(move || {
// bind the texture
gl.bind_texture(Gl::TEXTURE_2D, Some(&texture));
// load the data of the `<img>` element into the texture
if let Err(e) = gl.tex_image_2d_with_u32_and_u32_and_html_image_element(
Gl::TEXTURE_2D,
0,
Gl::RGBA as i32,
Gl::RGBA,
Gl::UNSIGNED_BYTE,
&img,
) {
utils::alert_err(e);
};
// set texture properties: texture filtering (to make the textures appear
// pixelated) and wrapping (in order to fix a bug)
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::NEAREST as i32);
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::NEAREST as i32);
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32);
gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32);
// generate mipmaps for the texture
gl.generate_mipmap(Gl::TEXTURE_2D);
// unbind the texture
gl.bind_texture(Gl::TEXTURE_2D, None);
}) as Box<dyn FnMut()>);
// once the `<img>` loads, execute the closure that loads the image into the texture
imgrc.set_onload(Some(a.as_ref().unchecked_ref()));
a.forget();
}
// set the source of the image
imgrc.set_src(path);
// unbind the texture
gl.bind_texture(Gl::TEXTURE_2D, None);
Ok(texture)
}
}