init: ready, set, go!

This commit is contained in:
thetek 2023-06-18 16:36:20 +02:00
commit 04e65b1ea3
16 changed files with 6015 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
/bin
/pkg
**/*.rs.bk
Cargo.lock
wasm-pack.log

27
Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "rust-webgl-rectangle"
version = "0.1.0"
authors = ["thetek <git@thetek.de>"]
description = "drawing a colored rectangle using rust, webassembly and webgl"
repository = "https://git.tjdev.de/thetek/rust-webgl-rectangle"
license = "MIT"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.84"
console_error_panic_hook = { version = "0.1.7", optional = true } # better debugging for errors, not great for code size
wee_alloc = { version = "0.4.5", optional = true } # smaller, but slower allocator
js-sys = "0.3.61"
[dependencies.web-sys]
version = "0.3.61"
features = ["Document", "Element", "HtmlCanvasElement", "WebGlBuffer", "WebGlProgram", "WebGl2RenderingContext", "WebGlShader", "WebGlVertexArrayObject", "Window"]
[profile.release]
opt-level = "s" # optimize for small code size

25
LICENSE Normal file
View File

@ -0,0 +1,25 @@
Copyright (c) 2023 thetek <git@thetek.de>
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.

1
README.md Normal file
View File

@ -0,0 +1 @@
# Invaders

342
src/draw.rs Normal file
View File

@ -0,0 +1,342 @@
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);
}
}
}

16
src/lib.rs Normal file
View File

@ -0,0 +1,16 @@
use wasm_bindgen::prelude::*;
mod draw;
mod utils;
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
pub fn wasm_main() {
#[cfg(feature = "console_error_panic_hook")]
utils::set_panic_hook();
draw::run();
}

7
src/shader/frag.glsl Normal file
View File

@ -0,0 +1,7 @@
precision mediump float;
varying vec3 vColor;
void main(void) {
gl_FragColor = vec4(vColor, 1.0);
}

11
src/shader/vert.glsl Normal file
View File

@ -0,0 +1,11 @@
precision mediump float;
attribute vec3 aCoord;
attribute vec3 aColor;
varying vec3 vColor;
void main(void) {
gl_Position = vec4(aCoord, 1.0);
vColor = aColor;
}

49
src/utils.rs Normal file
View File

@ -0,0 +1,49 @@
//! various utilities for usage with webassembly and webgl.
use wasm_bindgen::prelude::*;
/// set panic hook to the console_error_panic_hook crate. this allows for better debugging, but it
/// will result in a higher code size. requires the console_error_panic_hook feature.
pub fn set_panic_hook() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
/// prints an error message via alert(). the error message is provided as a JsValue in order to be
/// compatible with errors thrown by the javascript runtime.
pub fn alert_err(msg: &JsValue) {
let mut s = String::from("an error occured! --- ");
s.push_str(
&msg.as_string()
.unwrap_or_else(|| "(unknown error message)".to_string()),
);
web_sys::window().unwrap().alert_with_message(&s).unwrap();
}
/// create a javascript Float32Array from a regular rust array
#[macro_export]
macro_rules! f32_array {
($arr: expr) => {{
let memory_buffer = wasm_bindgen::memory()
.dyn_into::<WebAssembly::Memory>()?
.buffer();
let arr_location = $arr.as_ptr() as u32 / 4;
let array = js_sys::Float32Array::new(&memory_buffer)
.subarray(arr_location, arr_location + $arr.len() as u32);
array
}};
}
/// create a javascript Uint16Array from a regular rust array
#[macro_export]
macro_rules! u16_array {
($arr: expr) => {{
let memory_buffer = wasm_bindgen::memory()
.dyn_into::<WebAssembly::Memory>()?
.buffer();
let arr_location = $arr.as_ptr() as u32 / 2;
let array = js_sys::Uint16Array::new(&memory_buffer)
.subarray(arr_location, arr_location + $arr.len() as u32);
array
}};
}

2
www/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

5
www/bootstrap.js vendored Normal file
View File

@ -0,0 +1,5 @@
// a dependency graph that contains any wasm must all be imported
// asynchronously. this `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import('./index.js')
.catch(e => console.error('error importing `index.js`:', e))

25
www/index.html Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust WebGL Rectangle</title>
<style>
html, body {
background-color: #111;
color: #bbb;
font-family: monospace;
margin: 0;
overflow: hidden;
text-align: center;
}
canvas {
margin: calc(50vh - 300px) auto;
}
</style>
</head>
<body>
<noscript>This page contains WebAssembly and JavaScript content, please enable JavaScript in your browser.</noscript>
<script src="./bootstrap.js"></script>
<canvas id="canvas" width="800" height="600">This page contains WebGL content, please enable WebGL in your browser.</canvas>
</body>
</html>

3
www/index.js Normal file
View File

@ -0,0 +1,3 @@
import * as wasm from 'rust-webgl-rectangle-wasm'
wasm.wasm_main()

5447
www/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
www/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "rust-wasm-rectangle",
"version": "0.1.0",
"description": "drawing a colored rectangle using rust, webassembly and webgl",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack serve"
},
"keywords": ["webassembly", "wasm", "rust", "webpack"],
"author": "thetek <git@thetek.de>",
"license": "MIT",
"dependencies": {
"rust-webgl-rectangle-wasm": "file:../pkg"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.76.2",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.13.1"
}
}

27
www/webpack.config.js Normal file
View File

@ -0,0 +1,27 @@
const CopyWebpackPlugin = require('copy-webpack-plugin')
const path = require('path')
module.exports = {
entry: './bootstrap.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bootstrap.js',
},
mode: 'development',
plugins: [
new CopyWebpackPlugin({
patterns: [{ from: 'index.html' }],
})
],
experiments: {
asyncWebAssembly: true,
syncWebAssembly: true,
},
devServer: {
static: {
directory: path.join(__dirname, "dist"),
},
compress: true,
port: 3000,
},
}