rust-webgl-rectangle/src/draw.rs

343 lines
17 KiB
Rust

use std::mem::size_of;
use js_sys::WebAssembly;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
HtmlCanvasElement, WebGl2RenderingContext, WebGl2RenderingContext as WebGl, WebGlProgram,
WebGlShader,
};
use crate::{f32_array, u16_array, utils};
/// vertex shader source code
const VERT_SRC: &str = include_str!("./shader/vert.glsl");
const FRAG_SRC: &str = include_str!("./shader/frag.glsl");
/// stores game state and other attributes required for running the game, such as the webgl
/// rendering context.
struct Game {
canvas: HtmlCanvasElement,
gl: WebGl2RenderingContext,
}
impl Game {
/// initialize the canvas by obtaining a valid graphics context
fn new() -> Result<Self, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document
.get_element_by_id("canvas")
.ok_or("failed to get canvas element")?
.dyn_into::<HtmlCanvasElement>()?;
let gl = canvas
.get_context("webgl2")?
.ok_or("failed to get webgl rendering context")?
.dyn_into::<WebGl2RenderingContext>()?;
Ok(Game { canvas, gl })
}
/// draw the game to the canvas
fn draw(&self) -> Result<(), JsValue> {
let gl = &self.gl;
// define vertex data for drawing the rectangle. since opengl is a 3D rendering library,
// the coordinates have to be specified as three-dimensional values (x, y and z). since we
// only want to draw a two-dimensional scene, the z coordinate will simply be zero. also,
// the coordinates are given as normalized device coordinates in order to avoid being
// dependant on a single, hard-coded resolution. these coordinates are transformed
// according to the values provided to the glViewport function.
//
// (-1,1) (0,1) (1,1)
// ┌───────────┬───────────┐
// │ ╷ │
// │ ╷ │
// (-1,0) ├─ ─ ─ ─ ─(0,0)─ ─ ─ ─ ─┤ (1,0)
// │ ╵ │
// │ ╵ │
// └───────────┴───────────┘
// (-1,-1) (0,-1) (1,-1)
//
// in addition to the coordinates, the colors for the vertices are stored. immediately
// after the coordinates. thus, the layout of this data structure is:
//
// ┌─────────────────┬─────────────────┐
// │ coordinates │ color │
// │ ╷ ╷ │ ╷ ╷ │
// │ X │ Y │ Z │ R │ G │ B │
// └─────┴─────┴─────┴─────┴─────┴─────┘
//
let vertices: [f32; 24] = [
-0.5, 0.5, 0.0, 1.0, 0.0, 0.0, //
-0.5, -0.5, 0.0, 1.0, 1.0, 0.0, //
0.5, -0.5, 0.0, 0.0, 1.0, 0.0, //
0.5, 0.5, 0.0, 0.0, 0.0, 1.0, //
];
let vertices_array = f32_array!(vertices);
// define the indices of the vertices to draw. since we only defined four vertices in the
// vertex array but want to draw a rectangle that consists of two triangles, we need to
// specify which vertices should be used for the construction of each triangle. the drawing
// below, albeit very minimal due to constrains with the unicode system, shows the
// rectangle split up into two triangles labelled with their appropriate vertex indices.
//
// 0 3
// ┌──────────────────┐
// │ ¯--_ Δ023 │
// │ ¯--_ │
// │ ¯--_ │
// │ Δ012 ¯--_ │
// └──────────────────┘
// 1 2
//
let indices: [u16; 6] = [
0, 1, 2, //
0, 2, 3, //
];
let index_array = u16_array!(indices);
// for sending the vertex data to the gpu, we need to allocate some memory on the gpu where
// we can store the data. this memory is managed through vertex buffer objects. with these
// buffers, we can send large batches of data to the graphics card. the buffer that we
// created is identified by a unique id, which is returned by the glGenBuffer (or in the
// case of webgl, gl.create_buffer) function.
let vertex_buffer = gl.create_buffer().ok_or("failed to create buffer")?;
// since opengl supports multiple types of buffers, we need to specify that our vertex
// buffer is an array buffer using the glBindBuffer function. opengl allows us to bind
// multiple buffers at once, as long as they have a different buffer type. in order to bind
// a buffer that has the same buffer type as another already bound buffer, the bound buffer
// will have to be unbound before binding the new buffer.
gl.bind_buffer(WebGl::ARRAY_BUFFER, Some(&vertex_buffer));
// after the buffer is bound, any buffer calls will affect the buffer specified in the
// glBindBuffer function. through a call to the glBufferData function, we can copy the
// previously defined vertex data into the buffer's memory on the gpu. in this case, it
// will get copied into the previously created array buffer. the third parameter specifies
// how the graphics card should manage the data. STATIC_DRAW specifies that the buffer data
// is set only once and used many times. other options include STREAM_DRAW (set once, used
// at most a few times) and DYNAMIC_DRAW (data changed often and used many times). the gpu
// can then place the buffer in the most suitable location (e.g. a location with faster
// writes for DYNAMIC_DRAW).
gl.buffer_data_with_array_buffer_view(
WebGl::ARRAY_BUFFER,
&vertices_array,
WebGl::STATIC_DRAW,
);
// similarly, create a buffer that holds the vertex indices. in this case, we have to set
// the buffer type to ELEMENT_ARRAY_BUFFER, and not ARRAY_BUFFER as before.
let index_buffer = gl.create_buffer().ok_or("failed to create buffer")?;
gl.bind_buffer(WebGl::ELEMENT_ARRAY_BUFFER, Some(&index_buffer));
gl.buffer_data_with_array_buffer_view(
WebGl::ELEMENT_ARRAY_BUFFER,
&index_array,
WebGl::STATIC_DRAW,
);
// unbind the array and element array buffers
gl.bind_buffer(WebGl::ARRAY_BUFFER, None);
gl.bind_buffer(WebGl::ELEMENT_ARRAY_BUFFER, None);
// compile both vertex and fragment shaders. the compilation process is handled by the
// compile_shader helper function, see below. note that the shaders are compiled
// dynamically at run-time.
let vert_shader = self.compile_shader(WebGl::VERTEX_SHADER, VERT_SRC)?;
let frag_shader = self.compile_shader(WebGl::FRAGMENT_SHADER, FRAG_SRC)?;
// both shaders are now compiled. now, they need to be combined in a shader program. the
// program linking process is handled by the link_program helper function, see below. when
// linking shaders, the output of one shader will be used as the input of the next shader.
// when outputs and inputs do not match, linker errors will occur.
let shader_program = self.link_program(&vert_shader, &frag_shader)?;
// activate the shader program by calling glUseProgram. the program's shaders will be used
// when rendering objects.
gl.use_program(Some(&shader_program));
// since we do not need the shader objects anymore, they can be deleted.
gl.delete_shader(Some(&vert_shader));
gl.delete_shader(Some(&frag_shader));
// introduce and bind a vertex array buffer. this is required in order to add vertex buffer
// objects to it and to make the configuration of vertex data and attributes easier.
let vao = gl
.create_vertex_array()
.ok_or("failed to create vertex array object")?;
gl.bind_vertex_array(Some(&vao));
// re-bind the buffers since we unbound them earlier
gl.bind_buffer(WebGl::ARRAY_BUFFER, Some(&vertex_buffer));
gl.bind_buffer(WebGl::ELEMENT_ARRAY_BUFFER, Some(&index_buffer));
// we have to specify what part of our input data goes to which vertex attribute in the
// vertex shader. this means we have to specify how opengl should interpret the vertex data
// before rendering. in this case, the vertex coordinates are specified as three floats,
// followed by three values specifying the vertex color. first, we obtain the location of
// the `coords` attribute within the vertex shader through the function
// glGetAttribLocation, which will be used later for the glVertexAttribPointer function.
let coord_attrib = gl.get_attrib_location(&shader_program, "aCoord") as u32;
let color_attrib = gl.get_attrib_location(&shader_program, "aColor") as u32;
// with knowledge of our data layout and attribute locations, we can tell opengl how it
// should interpret the data. the parameters glVertexAttribPointer function are as follows:
// - index: specifies which vertex attribute we want to configure, in this case `aCoord`
// and `aColor`
// - size: size of the vertex attribute, in this case 3 since the attribute is a vec3
// - type: type of the data, in this case float (f32), since vec3 consists of floats
// - normalized: specifies if integer data should be normalized, not relevant.
// - stride: stride between vertex data. if the data is tightly packed together, a stride
// of 0 can be used to let opengl detrmine it automatically. however, since we combined
// coordinates and colors, we need to manually specify the stride.
// - offset: offset of where data begins within the buffer. the coordinates start at the
// beginning of the buffer, while the colors are offset by 3 elements.
//
// ┌─────────────────┬─────────────────╥─────────────────┬─────────────────┐
// │ coordinates │ color ║ coordinates │ color │
// │ ╷ ╷ │ ╷ ╷ ║ ╷ ╷ │ ╷ ╷ │
// │ X │ Y │ Z │ R │ G │ B ║ X │ Y │ Z │ R │ G │ B │
// └─────┴─────┴─────┴─────┴─────┴─────╨─────┴─────┴─────┴─────┴─────┴─────┘
// coordinates ─┨ offset: 0
// ┠───────────────────────────────────┨ stride: 6 * f32 = 24
// ┖─ ─ ─ ─ ─ ─ ─ ─ ─┨ data ┖─ ─ ─ ─ ─ ─ ─ ─ ─┨ data
//
// color ───────────────────┨ offset: 3 * f32 = 12
// ┠───────────────────────────────────┨ stride: 6 * f32 = 24
// ┖─ ─ ─ ─ ─ ─ ─ ─ ─┨ data ┖─ ─ ─ ─ ─ ─ ─ ─ ─┨ data
gl.vertex_attrib_pointer_with_i32(
coord_attrib,
3,
WebGl::FLOAT,
false,
6 * size_of::<f32>() as i32,
0,
);
gl.vertex_attrib_pointer_with_i32(
color_attrib,
3,
WebGl::FLOAT,
false,
6 * size_of::<f32>() as i32,
3 * size_of::<f32>() as i32,
);
// vertex attributes are disabled by default. thus, we need to enable them with
// glEnableVertexAttribArray giving the vertex attribute location as its argument.
gl.enable_vertex_attrib_array(coord_attrib);
gl.enable_vertex_attrib_array(color_attrib);
// specify the color to clear the screen with. this will act as the background color of our
// canvas. arguments formatted as (r, g, b, a), each represented using floats. glClearColor
// is a state-setting function, and its state is to be used by the glClear function.
gl.clear_color(0.0, 0.0, 0.0, 1.0);
// clear the screen. if the screen is not cleared, the contents of the previous frame would
// still be visible. the parameter specifies which buffers should be cleared. in this case,
// only the color buffer should be cleared. other options include depth and stencil buffer.
// glClear is a state-using function that uses the sate set by functions such as
// glClearColor.
gl.clear(WebGl::COLOR_BUFFER_BIT);
// tell opengl about the size of the rendering window (canvas) so that opengl knows how to
// display the data and coordinates with respect to the window. the first two parameters
// set the location of the lower left corner of the window. the third and fourth parameter
// set the width and height of the rendering window in pixels, in this case the size of the
// canvas element. behind the scenes, opengl uses the data specified by glViewport to
// transform the 2d coordinates it processed (ranging from -1.0 to 1.0) to coordinates on
// the screen. since the canvas will not be resized in this application, this only needs to
// be called once.
gl.viewport(
0,
0,
self.canvas.width() as i32,
self.canvas.height() as i32,
);
// draw the rectangle. the second parameter defines the number of vertices to draw, the
// third parameter specifies the offset within the element buffer object, which in this
// case is zero.
gl.draw_elements_with_i32(
WebGl::TRIANGLES,
indices.len() as i32,
WebGl::UNSIGNED_SHORT,
0,
);
Ok(())
}
/// compile an opengl shader
fn compile_shader(&self, shader_type: u32, source: &str) -> Result<WebGlShader, String> {
let gl = &self.gl;
// create the shader object. the type of shader (e.g. VERTEX_SHADER, FRAGMENT_SHADER) is
// provided as the first argument to glCreateShader.
let shader = gl
.create_shader(shader_type)
.ok_or("unable to create shader object")?;
// attach the shader source code to the shader object, and then compile it.
gl.shader_source(&shader, source);
gl.compile_shader(&shader);
// since the compilation might not be successful, check for any errors that occured during
// compilation of of the shader. return the error message in case of an error.
if gl
.get_shader_parameter(&shader, WebGl::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 vertex and fragment shader into one opengl shader program
fn link_program(
&self,
vert_shader: &WebGlShader,
frag_shader: &WebGlShader,
) -> Result<WebGlProgram, String> {
let gl = &self.gl;
// create a shader program object using the glCreateProgram function
let program = gl
.create_program()
.ok_or("unable to create shader program")?;
// attach both vertex and fragment shader to the previously created program using the
// glAttachShader function, then link the program using glLinkProgram.
gl.attach_shader(&program, vert_shader);
gl.attach_shader(&program, frag_shader);
gl.link_program(&program);
// since linking the program might not be successful, check for any errors that occured
// during the linking. return the error message in case of an error.
if gl
.get_program_parameter(&program, WebGl::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")))
}
}
}
/// run the game
pub fn run() {
match Game::new() {
Ok(game) => {
game.draw().unwrap_or_else(|err| utils::alert_err(&err));
}
Err(err) => {
utils::alert_err(&err);
}
}
}