├── .github └── workflows │ ├── demos.yml │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── 01-triangle.png ├── 02-buffer.png ├── 03-uniform.png └── 05-primitives.png ├── examples ├── 01-triangle │ ├── Cargo.toml │ ├── index.html │ ├── shaders │ │ ├── shader.frag │ │ └── shader.vert │ └── src │ │ └── main.rs ├── 02-buffer │ ├── Cargo.toml │ ├── index.html │ ├── shaders │ │ ├── shader.frag │ │ └── shader.vert │ └── src │ │ └── main.rs ├── 03-uniform │ ├── Cargo.toml │ ├── index.html │ ├── shaders │ │ ├── shader.frag │ │ └── shader.vert │ └── src │ │ └── main.rs ├── 04-animate │ ├── Cargo.toml │ ├── index.html │ ├── shaders │ │ ├── shader.frag │ │ └── shader.vert │ └── src │ │ └── main.rs ├── 05-primitives │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── README.md ├── instances │ ├── Cargo.toml │ ├── index.html │ ├── shaders │ │ ├── shader.frag │ │ └── shader.vert │ └── src │ │ └── main.rs ├── pong │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── primitive-scene │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs ├── shaderfun │ ├── Cargo.toml │ ├── index.html │ ├── shaders │ │ ├── shader.frag │ │ └── shader.vert │ └── src │ │ └── main.rs ├── snowflake │ ├── Cargo.toml │ ├── index.html │ └── src │ │ └── main.rs └── zoom-pan │ ├── Cargo.toml │ ├── index.html │ ├── shaders │ ├── shader.frag │ └── shader.vert │ └── src │ └── main.rs ├── limelight ├── Cargo.toml ├── README.md ├── limelight-derive │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── src │ ├── attribute.rs │ ├── buffer.rs │ ├── draw_modes.rs │ ├── lib.rs │ ├── program.rs │ ├── renderer.rs │ ├── shadow_gpu │ │ ├── buffer.rs │ │ ├── mod.rs │ │ ├── program.rs │ │ ├── state.rs │ │ ├── uniforms.rs │ │ └── vao.rs │ ├── state │ │ ├── blending.rs │ │ ├── culling.rs │ │ ├── depth.rs │ │ ├── enable.rs │ │ └── mod.rs │ ├── uniform.rs │ └── webgl │ │ ├── buffer.rs │ │ ├── error.rs │ │ ├── mod.rs │ │ └── types.rs └── tests │ └── test_vertex_attribute_derive.rs ├── primitives ├── Cargo.toml ├── README.md └── src │ ├── circle │ ├── mod.rs │ ├── shader.frag │ └── shader.vert │ ├── color.rs │ ├── common.rs │ ├── hairline │ ├── mod.rs │ ├── shader.frag │ └── shader.vert │ ├── lib.rs │ ├── line │ ├── mod.rs │ ├── shader.frag │ └── shader.vert │ ├── line3d │ ├── mod.rs │ ├── shader.frag │ └── shader.vert │ └── rect │ ├── mod.rs │ ├── shader.frag │ └── shader.vert ├── transform ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── yew ├── Cargo.toml ├── README.md └── src ├── key_event.rs └── lib.rs /.github/workflows/demos.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Demos 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Install Trunk 17 | run: wget -qO- https://github.com/thedodd/trunk/releases/download/v0.14.0/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- 18 | - name: Install wasm32 target 19 | run: rustup target add wasm32-unknown-unknown 20 | - name: Install wasm-pack 21 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 22 | - name: Build demos 23 | run: | 24 | mkdir dist 25 | cd examples 26 | for DEMO_DIR in $(ls -d */) 27 | do 28 | cd "${DEMO_DIR}" 29 | ../../trunk build --release --public-url="/limelight/${DEMO_DIR}/" 30 | mv dist "../../dist/${DEMO_DIR}" 31 | cd ../ 32 | done 33 | - name: Deploy 🚀 34 | uses: JamesIves/github-pages-deploy-action@4.1.4 35 | with: 36 | branch: gh-pages 37 | folder: dist 38 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | dist 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "limelight", 5 | "transform", 6 | "primitives", 7 | "yew", 8 | "examples/*", 9 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Paul Butler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Limelight 2 | 3 | [![GitHub Repo stars](https://img.shields.io/github/stars/drifting-in-space/limelight?style=social)](https://github.com/drifting-in-space/limelight) 4 | [![crates.io](https://img.shields.io/crates/v/limelight.svg)](https://crates.io/crates/limelight) 5 | [![docs.rs](https://img.shields.io/badge/docs-release-brightgreen)](https://docs.rs/limelight/) 6 | [![Rust](https://github.com/drifting-in-space/limelight/actions/workflows/rust.yml/badge.svg)](https://github.com/drifting-in-space/limelight/actions/workflows/rust.yml) 7 | 8 | Limelight is a `WebGL2` wrapper with a focus on making high-performance WebAssembly graphics code easier to write and maintain. 9 | 10 | https://user-images.githubusercontent.com/46173/146214698-784404f2-633c-4180-acda-1e8d64189e76.mov 11 | 12 | [live demo](https://drifting-in-space.github.io/limelight/primitive-scene/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/primitive-scene)) 13 | 14 | Specifically, `limelight`: 15 | - Provides a functional interface that **abstracts away the statefulness of WebGL**. 16 | It accomplishes this by using a *shadow GPU* that tracks the GPU's state, diffs it with the 17 | desired state, and sends only the necessary instructions to WebGL. 18 | - Provides abstractions for buffers and uniforms that **defer GPU data transfer until the next draw cycle**. 19 | - Provides a **typed interface to uniforms and buffers**, and automatically generates bindings 20 | between shader attributes and Rust `struct`s through a derive macro. 21 | - Provides an **interface for transforms** like zoom and pan through [`limelight-transform`](https://github.com/drifting-in-space/limelight/tree/main/transform). 22 | - Provides 2D **shape primitives** like circles and lines through [`limelight-primitives`](https://github.com/drifting-in-space/limelight/tree/main/primitives). 23 | 24 | # Getting started 25 | 26 | Abstract art made from circles and rectangles. 27 | 28 | ([full code](https://github.com/drifting-in-space/limelight/tree/main/examples/05-primitives), 29 | [demo](https://drifting-in-space.github.io/limelight/05-primitives/)) 30 | 31 | This example uses [`limelight-primitives`](https://github.com/drifting-in-space/limelight/tree/main/primitives) 32 | and [`limelight-yew`](https://github.com/drifting-in-space/limelight/tree/main/yew) to construct a basic, static image 33 | made from circles and rectangles. 34 | 35 | ```rust 36 | use anyhow::Result; 37 | use limelight::{renderer::Drawable, Renderer}; 38 | use limelight_primitives::{Circle, CircleLayer, Rect, RectLayer}; 39 | use limelight_yew::{LimelightComponent, LimelightController}; 40 | 41 | struct Primitives { 42 | rects: RectLayer, 43 | circles: CircleLayer, 44 | } 45 | 46 | impl LimelightController for Primitives { 47 | fn draw( 48 | &mut self, 49 | renderer: &mut Renderer, 50 | _ts: f64, 51 | ) -> Result { 52 | self.rects.draw(renderer)?; 53 | self.circles.draw(renderer)?; 54 | 55 | Ok(false) 56 | } 57 | } 58 | 59 | impl Default for Primitives { 60 | fn default() -> Self { 61 | let rects = RectLayer::new(); 62 | let circles = CircleLayer::new(); 63 | 64 | rects.buffer().set_data(vec![ 65 | Rect { 66 | lower_right: [0.4, 0.1], 67 | upper_left: [-0.8, 0.2], 68 | color: palette::named::TOMATO.into(), 69 | }, 70 | Rect { 71 | lower_right: [0.4, 0.25], 72 | upper_left: [-0.6, 0.5], 73 | color: palette::named::SLATEBLUE.into(), 74 | }, 75 | ]); 76 | 77 | circles.buffer().set_data(vec![ 78 | Circle { 79 | position: [0., 0.25], 80 | radius: 0.2, 81 | color: palette::named::WHITE.into(), 82 | }, 83 | Circle { 84 | position: [0., 0.25], 85 | radius: 0.1, 86 | color: palette::named::ORANGERED.into(), 87 | }, 88 | ]); 89 | 90 | Primitives { rects, circles } 91 | } 92 | } 93 | 94 | fn main() { 95 | console_error_panic_hook::set_once(); 96 | wasm_logger::init(wasm_logger::Config::default()); 97 | yew::start_app::>(); 98 | } 99 | ``` 100 | 101 | # More Examples 102 | 103 | - [Instancing](https://drifting-in-space.github.io/limelight/instances/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/instances)) 104 | - [Pong](https://drifting-in-space.github.io/limelight/pong/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/pong)) 105 | - [Zooming / panning](https://drifting-in-space.github.io/limelight/zoom-pan/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/zoom-pan)) 106 | - [Full-canvas shader](https://drifting-in-space.github.io/limelight/shaderfun/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/shaderfun)) 107 | - [Primitive scene](https://drifting-in-space.github.io/limelight/primitive-scene/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/primitive-scene)) 108 | - [3D snowflake](https://drifting-in-space.github.io/limelight/snowflake/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/snowflake)) 109 | -------------------------------------------------------------------------------- /assets/01-triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamsocket/limelight/22467f93b1bf5486e5bc3b947b86fab16c95c1f7/assets/01-triangle.png -------------------------------------------------------------------------------- /assets/02-buffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamsocket/limelight/22467f93b1bf5486e5bc3b947b86fab16c95c1f7/assets/02-buffer.png -------------------------------------------------------------------------------- /assets/03-uniform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamsocket/limelight/22467f93b1bf5486e5bc3b947b86fab16c95c1f7/assets/03-uniform.png -------------------------------------------------------------------------------- /assets/05-primitives.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamsocket/limelight/22467f93b1bf5486e5bc3b947b86fab16c95c1f7/assets/05-primitives.png -------------------------------------------------------------------------------- /examples/01-triangle/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-triangle" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.74" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | -------------------------------------------------------------------------------- /examples/01-triangle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demos 6 | 7 | 8 | 11 | 12 | 13 |
14 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /examples/01-triangle/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | out vec4 color; 6 | in vec2 v_position; 7 | 8 | void main() { 9 | color = vec4(v_position.xy / 1.5 + 0.5, 0.3, 1.0); 10 | } -------------------------------------------------------------------------------- /examples/01-triangle/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | const float PI = 3.141592653589793238; 4 | in vec2 position; 5 | out vec2 v_position; 6 | 7 | void main() { 8 | float angle = (float(gl_VertexID) + (3./4.)) * 2. * PI / 3.; 9 | v_position = vec2(cos(angle) / 1.2, sin(angle) / 1.2); 10 | gl_Position = vec4(v_position, 0., 1.); 11 | } -------------------------------------------------------------------------------- /examples/01-triangle/src/main.rs: -------------------------------------------------------------------------------- 1 | use limelight::{DrawMode, DummyBuffer, Program, Renderer}; 2 | use wasm_bindgen::JsCast; 3 | use web_sys::WebGl2RenderingContext; 4 | 5 | fn render_triangle(gl: WebGl2RenderingContext) { 6 | // limelight doesn't touch the DOM at all. Use your preferred 7 | // framework to create a canvas and create a WebGL2 context 8 | // from it. 9 | 10 | // Create a shader program by passing in GLSL code as strings for 11 | // the fragment and vertex shaders. 12 | let mut program = Program::new( 13 | include_str!("../shaders/shader.vert"), 14 | include_str!("../shaders/shader.frag"), 15 | DrawMode::Triangles, 16 | ); 17 | 18 | // Create a renderer. The renderer becomes the owner of the 19 | // WebGl2RenderingContext, to ensure that its internal representation 20 | // of the GPU state is always accureate. 21 | let mut renderer = Renderer::new(gl); 22 | 23 | // Run the program, rendering the results to the screen. We are 24 | // not passing any vertex attribute data, so we use a `DummyBuffer` 25 | // which renders three vertices: one for each corner of a triangle. 26 | renderer.render(&mut program, &DummyBuffer::new(3)).unwrap(); 27 | } 28 | 29 | fn get_gl() -> WebGl2RenderingContext { 30 | let document = web_sys::window().unwrap().document().unwrap(); 31 | let canvas = document.get_element_by_id("canvas").unwrap(); 32 | let canvas: web_sys::HtmlCanvasElement = 33 | canvas.dyn_into::().unwrap(); 34 | 35 | canvas 36 | .get_context("webgl2") 37 | .unwrap() 38 | .unwrap() 39 | .dyn_into::() 40 | .unwrap() 41 | } 42 | 43 | fn main() { 44 | console_error_panic_hook::set_once(); 45 | let gl = get_gl(); 46 | 47 | render_triangle(gl); 48 | } 49 | -------------------------------------------------------------------------------- /examples/02-buffer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-buffer" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.74" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | bytemuck = { version = "1.7.2", features = ["derive"] } 16 | -------------------------------------------------------------------------------- /examples/02-buffer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demos 6 | 7 | 8 | 11 | 12 | 13 |
14 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /examples/02-buffer/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | out vec4 color; 6 | 7 | void main() { 8 | color = vec4(0.5, 0.0, 0.3, 1.0); 9 | } -------------------------------------------------------------------------------- /examples/02-buffer/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 position; 4 | 5 | void main() { 6 | gl_Position = vec4(position, 0., 1.); 7 | } -------------------------------------------------------------------------------- /examples/02-buffer/src/main.rs: -------------------------------------------------------------------------------- 1 | use limelight::{attribute, Buffer, BufferUsageHint, DrawMode, Program, Renderer}; 2 | use wasm_bindgen::JsCast; 3 | use web_sys::WebGl2RenderingContext; 4 | 5 | // This attribute macro derives a number of traits, including `VertexAttribute`, which 6 | // is required for a type to be used in an `AttributeBuffer`. 7 | #[attribute] 8 | struct VertexDescription { 9 | position: [f32; 2], // field names are mapped to variables in the shader. 10 | } 11 | 12 | impl VertexDescription { 13 | pub fn new(x: f32, y: f32) -> Self { 14 | VertexDescription { position: [x, y] } 15 | } 16 | } 17 | 18 | fn render_triangles(gl: WebGl2RenderingContext) { 19 | let mut program = Program::new( 20 | include_str!("../shaders/shader.vert"), 21 | include_str!("../shaders/shader.frag"), 22 | DrawMode::Triangles, 23 | ); 24 | 25 | let mut renderer = Renderer::new(gl); 26 | 27 | let data = vec![ 28 | // Lower-left triangle. 29 | VertexDescription::new(-0.1, -0.1), 30 | VertexDescription::new(-0.5, -0.1), 31 | VertexDescription::new(-0.5, -0.5), 32 | // Upper-right triangle. 33 | VertexDescription::new(0.1, 0.1), 34 | VertexDescription::new(0.5, 0.1), 35 | VertexDescription::new(0.5, 0.5), 36 | ]; 37 | 38 | let buffer = Buffer::new(data, BufferUsageHint::StaticDraw); 39 | 40 | renderer.render(&mut program, &buffer).unwrap(); 41 | } 42 | 43 | fn get_gl() -> WebGl2RenderingContext { 44 | let document = web_sys::window().unwrap().document().unwrap(); 45 | let canvas = document.get_element_by_id("canvas").unwrap(); 46 | let canvas: web_sys::HtmlCanvasElement = 47 | canvas.dyn_into::().unwrap(); 48 | 49 | canvas 50 | .get_context("webgl2") 51 | .unwrap() 52 | .unwrap() 53 | .dyn_into::() 54 | .unwrap() 55 | } 56 | 57 | fn main() { 58 | console_error_panic_hook::set_once(); 59 | let gl = get_gl(); 60 | 61 | render_triangles(gl); 62 | } 63 | -------------------------------------------------------------------------------- /examples/03-uniform/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-uniform" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.74" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | -------------------------------------------------------------------------------- /examples/03-uniform/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demos 6 | 7 | 8 | 11 | 12 | 13 |
14 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /examples/03-uniform/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | uniform vec3 u_color; 6 | out vec4 color; 7 | 8 | void main() { 9 | color = vec4(u_color, 1.0); 10 | } -------------------------------------------------------------------------------- /examples/03-uniform/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | const float PI = 3.141592653589793238; 4 | in vec2 position; 5 | uniform float u_rotate; 6 | uniform vec2 u_scale; 7 | out vec2 v_position; 8 | 9 | void main() { 10 | float angle = (float(gl_VertexID) + 3./4.) * 2. * PI / 3. + u_rotate; 11 | v_position = vec2(cos(angle) / 1.2, sin(angle) / 1.2) * u_scale; 12 | gl_Position = vec4(v_position, 0., 1.); 13 | } -------------------------------------------------------------------------------- /examples/03-uniform/src/main.rs: -------------------------------------------------------------------------------- 1 | use limelight::{DrawMode, DummyBuffer, Program, Renderer, Uniform}; 2 | use wasm_bindgen::JsCast; 3 | use web_sys::WebGl2RenderingContext; 4 | 5 | fn render_triangles_with_uniform(gl: WebGl2RenderingContext) { 6 | let rotate_uniform = Uniform::new(std::f32::consts::PI / 3.4); 7 | let scale_uniform = Uniform::new([0.5, 0.8]); 8 | let color_uniform = Uniform::new([0.9, 0.2, 0.3]); 9 | 10 | let mut program = Program::new( 11 | include_str!("../shaders/shader.vert"), 12 | include_str!("../shaders/shader.frag"), 13 | DrawMode::Triangles, 14 | ) 15 | .with_uniform("u_rotate", rotate_uniform) 16 | .with_uniform("u_scale", scale_uniform) 17 | .with_uniform("u_color", color_uniform); 18 | 19 | let mut renderer = Renderer::new(gl); 20 | renderer.render(&mut program, &DummyBuffer::new(3)).unwrap(); 21 | } 22 | 23 | fn get_gl() -> WebGl2RenderingContext { 24 | let document = web_sys::window().unwrap().document().unwrap(); 25 | let canvas = document.get_element_by_id("canvas").unwrap(); 26 | let canvas: web_sys::HtmlCanvasElement = 27 | canvas.dyn_into::().unwrap(); 28 | 29 | canvas 30 | .get_context("webgl2") 31 | .unwrap() 32 | .unwrap() 33 | .dyn_into::() 34 | .unwrap() 35 | } 36 | 37 | fn main() { 38 | console_error_panic_hook::set_once(); 39 | let gl = get_gl(); 40 | 41 | render_triangles_with_uniform(gl); 42 | } 43 | -------------------------------------------------------------------------------- /examples/04-animate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-animate" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.51" 8 | bytemuck = "1.7.2" 9 | console_error_panic_hook = "0.1.6" 10 | limelight = { path="../../limelight" } 11 | limelight-yew = {path="../../yew"} 12 | wasm-bindgen = "0.2.78" 13 | wasm-logger = "0.2.0" 14 | web-sys = { version="0.3.55", features=["Window"] } 15 | yew = "0.19.3" 16 | -------------------------------------------------------------------------------- /examples/04-animate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demo 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/04-animate/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | out vec4 color; 6 | uniform vec3 u_color; 7 | 8 | void main() { 9 | color = vec4(u_color, 1.0); 10 | } -------------------------------------------------------------------------------- /examples/04-animate/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 position; 4 | 5 | void main() { 6 | gl_Position = vec4(position * 0.5, 0., 1.); 7 | } -------------------------------------------------------------------------------- /examples/04-animate/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use limelight::{attribute, Buffer, BufferUsageHint, DrawMode, Program, Renderer, Uniform}; 3 | use limelight_yew::{LimelightComponent, LimelightController, ShouldRequestAnimationFrame}; 4 | 5 | #[attribute] 6 | struct VertexDescription { 7 | position: [f32; 2], 8 | } 9 | 10 | impl VertexDescription { 11 | pub fn new(x: f32, y: f32) -> Self { 12 | VertexDescription { position: [x, y] } 13 | } 14 | } 15 | 16 | struct Animation { 17 | program: Program, 18 | buffer: Buffer, 19 | uniform: Uniform<[f32; 3]>, 20 | } 21 | 22 | impl Default for Animation { 23 | fn default() -> Self { 24 | let buffer = Buffer::new(vec![], BufferUsageHint::DynamicDraw); 25 | let uniform = Uniform::new([0., 0., 0.]); 26 | 27 | let program = Program::new( 28 | include_str!("../shaders/shader.vert"), 29 | include_str!("../shaders/shader.frag"), 30 | DrawMode::Triangles, 31 | ) 32 | .with_uniform("u_color", uniform.clone()); 33 | 34 | Animation { 35 | buffer, 36 | program, 37 | uniform, 38 | } 39 | } 40 | } 41 | 42 | impl LimelightController for Animation { 43 | fn draw(&mut self, renderer: &mut Renderer, time: f64) -> Result { 44 | let theta1 = time as f32 / 1000.; 45 | let theta2 = theta1 + (std::f32::consts::TAU / 3.); 46 | let theta3 = theta2 + (std::f32::consts::TAU / 3.); 47 | 48 | self.buffer.set_data(vec![ 49 | VertexDescription::new(theta1.cos(), theta1.sin()), 50 | VertexDescription::new(theta2.cos(), theta2.sin()), 51 | VertexDescription::new(theta3.cos(), theta3.sin()), 52 | ]); 53 | 54 | let r = (time as f32 / 3000.).sin() / 2. + 0.5; 55 | let g = (time as f32 / 5000.).sin() / 2. + 0.5; 56 | let b = (time as f32 / 7000.).sin() / 2. + 0.5; 57 | 58 | self.uniform.set_value([r, g, b]); 59 | 60 | renderer.render(&mut self.program, &self.buffer)?; 61 | 62 | Ok(true) 63 | } 64 | } 65 | 66 | fn main() { 67 | console_error_panic_hook::set_once(); 68 | wasm_logger::init(wasm_logger::Config::default()); 69 | 70 | yew::start_app::>(); 71 | } 72 | -------------------------------------------------------------------------------- /examples/05-primitives/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-primitives" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.74" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | limelight-primitives = {path="../../primitives"} 16 | limelight-transform = {path="../../transform"} 17 | palette = { version = "0.6.0", features = ["named"] } 18 | wasm-logger = "0.2.0" 19 | limelight-yew = {path="../../yew"} 20 | yew = "0.19.3" 21 | anyhow = "1.0.51" 22 | -------------------------------------------------------------------------------- /examples/05-primitives/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demos 6 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/05-primitives/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use limelight::{renderer::Drawable, Renderer}; 3 | use limelight_primitives::{Circle, CircleLayer, Rect, RectLayer}; 4 | use limelight_yew::{LimelightComponent, LimelightController}; 5 | 6 | struct Primitives { 7 | rects: RectLayer, 8 | circles: CircleLayer, 9 | } 10 | 11 | impl LimelightController for Primitives { 12 | fn draw( 13 | &mut self, 14 | renderer: &mut Renderer, 15 | _ts: f64, 16 | ) -> Result { 17 | self.rects.draw(renderer)?; 18 | self.circles.draw(renderer)?; 19 | 20 | Ok(false) 21 | } 22 | } 23 | 24 | impl Default for Primitives { 25 | fn default() -> Self { 26 | let rects = RectLayer::new(); 27 | let circles = CircleLayer::new(); 28 | 29 | rects.buffer().set_data(vec![ 30 | Rect { 31 | lower_right: [0.4, 0.1], 32 | upper_left: [-0.8, 0.2], 33 | color: palette::named::TOMATO.into(), 34 | }, 35 | Rect { 36 | lower_right: [0.4, 0.25], 37 | upper_left: [-0.6, 0.5], 38 | color: palette::named::SLATEBLUE.into(), 39 | }, 40 | ]); 41 | 42 | circles.buffer().set_data(vec![ 43 | Circle { 44 | position: [0., 0.25], 45 | radius: 0.2, 46 | color: palette::named::WHITE.into(), 47 | }, 48 | Circle { 49 | position: [0., 0.25], 50 | radius: 0.1, 51 | color: palette::named::ORANGERED.into(), 52 | }, 53 | ]); 54 | 55 | Primitives { rects, circles } 56 | } 57 | } 58 | 59 | fn main() { 60 | console_error_panic_hook::set_once(); 61 | wasm_logger::init(wasm_logger::Config::default()); 62 | yew::start_app::>(); 63 | } 64 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Use [trunk](https://github.com/thedodd/trunk) to run these examples, as in: 2 | 3 | ```bash 4 | $ cd animate-triangle 5 | $ trunk serve --release 6 | ``` 7 | 8 | `--release` is recommended because WASM code runs pretty slow without it. -------------------------------------------------------------------------------- /examples/instances/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-instances" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.74" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | bytemuck = { version = "1.7.2", features = ["derive"] } 16 | -------------------------------------------------------------------------------- /examples/instances/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demos 6 | 7 | 8 | 11 | 12 | 13 |
14 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /examples/instances/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | out vec4 color; 6 | in vec2 v_position; 7 | in vec2 m_position; 8 | 9 | void main() { 10 | color = vec4(m_position.xy, 0., 1.0); 11 | } -------------------------------------------------------------------------------- /examples/instances/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | const float PI = 3.141592653589793238; 4 | in int instance_index; 5 | in int vertex_index; 6 | out vec2 v_position; 7 | out vec2 m_position; 8 | 9 | void main() { 10 | float angle = (float(instance_index) + (3./4.)) * 2. * PI / 20.; 11 | v_position = vec2(cos(angle) / 2., sin(angle) / 2.); 12 | 13 | float angle2 = (float(vertex_index) + (3./4.)) * 2. * PI / 4.; 14 | v_position += vec2(cos(angle2) / 10., sin(angle2) / 10.); 15 | 16 | gl_PointSize = 10.0; 17 | gl_Position = vec4(v_position, 0., 1.); 18 | m_position = vec2(float(instance_index) / 20., float(vertex_index) / 4.); 19 | } -------------------------------------------------------------------------------- /examples/instances/src/main.rs: -------------------------------------------------------------------------------- 1 | use limelight::{attribute, Buffer, BufferUsageHint, DrawMode, Program, Renderer}; 2 | use wasm_bindgen::JsCast; 3 | use web_sys::WebGl2RenderingContext; 4 | 5 | #[attribute] 6 | struct InstanceAttribute { 7 | instance_index: i32, 8 | } 9 | 10 | #[attribute] 11 | struct VertexAttribute { 12 | vertex_index: i32, 13 | } 14 | 15 | fn render_triangle(gl: WebGl2RenderingContext) { 16 | let mut program = Program::new( 17 | include_str!("../shaders/shader.vert"), 18 | include_str!("../shaders/shader.frag"), 19 | DrawMode::TriangleFan, 20 | ); 21 | 22 | let mut renderer = Renderer::new(gl); 23 | 24 | let instances = Buffer::new( 25 | (0..20) 26 | .map(|instance_index| InstanceAttribute { instance_index }) 27 | .collect(), 28 | BufferUsageHint::StaticDraw, 29 | ); 30 | 31 | let vertices = Buffer::new( 32 | (0..4) 33 | .map(|vertex_index| VertexAttribute { vertex_index }) 34 | .collect(), 35 | BufferUsageHint::StaticDraw, 36 | ); 37 | 38 | renderer 39 | .render_instanced(&mut program, &vertices, &instances) 40 | .unwrap(); 41 | } 42 | 43 | fn get_gl() -> WebGl2RenderingContext { 44 | let document = web_sys::window().unwrap().document().unwrap(); 45 | let canvas = document.get_element_by_id("canvas").unwrap(); 46 | let canvas: web_sys::HtmlCanvasElement = 47 | canvas.dyn_into::().unwrap(); 48 | 49 | canvas 50 | .get_context("webgl2") 51 | .unwrap() 52 | .unwrap() 53 | .dyn_into::() 54 | .unwrap() 55 | } 56 | 57 | fn main() { 58 | console_error_panic_hook::set_once(); 59 | let gl = get_gl(); 60 | 61 | render_triangle(gl); 62 | } 63 | -------------------------------------------------------------------------------- /examples/pong/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pong" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.78" 9 | web-sys = { version="0.3.55", features=["Window"] } 10 | limelight = { path="../../limelight" } 11 | bytemuck = "1.7.2" 12 | limelight-yew = {path="../../yew"} 13 | limelight-primitives = {path="../../primitives"} 14 | yew = "0.19.3" 15 | anyhow = "1.0.51" 16 | wasm-logger = "0.2.0" 17 | log = "0.4.14" 18 | palette = "0.6.0" 19 | -------------------------------------------------------------------------------- /examples/pong/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demo 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/pong/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use limelight::renderer::Drawable; 3 | use limelight::Renderer; 4 | use limelight_primitives::{Circle, CircleLayer, Rect, RectLayer}; 5 | use limelight_yew::{ 6 | KeyCode, LimelightComponent, LimelightController, ShouldCancelEvent, 7 | ShouldRequestAnimationFrame, 8 | }; 9 | 10 | const UPPER_BOUND: f32 = 1.; 11 | const LOWER_BOUND: f32 = -1.; 12 | 13 | const LEFT_BOUND: f32 = -1.; 14 | const RIGHT_BOUND: f32 = 1.; 15 | const PADDLE_BUFFER: f32 = 0.1; 16 | 17 | const PADDLE_HEIGHT: f32 = 0.3; 18 | const BALL_RADIUS: f32 = 0.025; 19 | 20 | const PADDLE_WIDTH: f32 = 0.05; 21 | const PADDLE_SPEED: f32 = 0.01; 22 | 23 | const BALL_SPEED: f32 = 0.01; 24 | 25 | struct GameState { 26 | paddle_position: f32, 27 | ball_position: [f32; 2], 28 | ball_direction: [f32; 2], 29 | last_time: f64, 30 | } 31 | 32 | impl GameState { 33 | fn step(&mut self, ts: f64, paddle_direction: f32) { 34 | let time_delta = ((ts - self.last_time) / 20.) as f32; 35 | self.last_time = ts; 36 | 37 | self.paddle_position += paddle_direction * PADDLE_SPEED * time_delta; 38 | self.ball_position = [ 39 | self.ball_position[0] + self.ball_direction[0] * BALL_SPEED * time_delta, 40 | self.ball_position[1] + self.ball_direction[1] * BALL_SPEED * time_delta, 41 | ]; 42 | 43 | if ((LEFT_BOUND + PADDLE_BUFFER - BALL_SPEED + PADDLE_WIDTH / 2.) 44 | ..=(LEFT_BOUND + PADDLE_BUFFER + BALL_SPEED + PADDLE_WIDTH / 2.)) 45 | .contains(&self.ball_position[0]) 46 | && ((self.paddle_position - PADDLE_HEIGHT / 2.) 47 | ..(self.paddle_position + PADDLE_HEIGHT / 2.)) 48 | .contains(&self.ball_position[1]) 49 | { 50 | self.ball_direction[0] = -self.ball_direction[0]; 51 | } 52 | 53 | if ((RIGHT_BOUND - PADDLE_BUFFER - BALL_SPEED - PADDLE_WIDTH / 2.) 54 | ..=(RIGHT_BOUND - PADDLE_BUFFER + BALL_SPEED - PADDLE_WIDTH / 2.)) 55 | .contains(&self.ball_position[0]) 56 | && ((-self.paddle_position - PADDLE_HEIGHT / 2.) 57 | ..(-self.paddle_position + PADDLE_HEIGHT / 2.)) 58 | .contains(&self.ball_position[1]) 59 | { 60 | self.ball_direction[0] = -self.ball_direction[0]; 61 | } 62 | 63 | if self.ball_position[1] + BALL_RADIUS > UPPER_BOUND { 64 | self.ball_position[1] = UPPER_BOUND - BALL_RADIUS; 65 | self.ball_direction[1] = -self.ball_direction[1]; 66 | } 67 | 68 | if self.ball_position[1] - BALL_RADIUS < LOWER_BOUND { 69 | self.ball_position[1] = LOWER_BOUND + BALL_RADIUS; 70 | self.ball_direction[1] = -self.ball_direction[1]; 71 | } 72 | 73 | // Losing Conditions 74 | if self.ball_position[0] > RIGHT_BOUND + BALL_RADIUS 75 | || self.ball_position[0] < LEFT_BOUND - BALL_RADIUS 76 | { 77 | self.ball_position = [0., 0.]; 78 | self.ball_direction = [-self.ball_direction[0], self.ball_direction[1]]; 79 | } 80 | } 81 | } 82 | 83 | impl Default for GameState { 84 | fn default() -> Self { 85 | GameState { 86 | paddle_position: 0., 87 | ball_direction: [1., 1.], 88 | ball_position: [0., 0.], 89 | last_time: 0., 90 | } 91 | } 92 | } 93 | 94 | struct PongGame { 95 | paddles: RectLayer, 96 | ball: CircleLayer, 97 | state: GameState, 98 | paddle_direction: f32, 99 | } 100 | 101 | impl LimelightController for PongGame { 102 | fn draw(&mut self, renderer: &mut Renderer, ts: f64) -> Result { 103 | self.state.step(ts, self.paddle_direction); 104 | 105 | let left_paddle_left = LEFT_BOUND + PADDLE_BUFFER - PADDLE_WIDTH / 2.; 106 | let left_paddle_right = LEFT_BOUND + PADDLE_BUFFER + PADDLE_WIDTH / 2.; 107 | let left_paddle_top = self.state.paddle_position + PADDLE_HEIGHT / 2.; 108 | let left_paddle_bottom = self.state.paddle_position - PADDLE_HEIGHT / 2.; 109 | 110 | let right_paddle_left = RIGHT_BOUND - PADDLE_BUFFER - PADDLE_WIDTH / 2.; 111 | let right_paddle_right = RIGHT_BOUND - PADDLE_BUFFER + PADDLE_WIDTH / 2.; 112 | let right_paddle_top = -self.state.paddle_position + PADDLE_HEIGHT / 2.; 113 | let right_paddle_bottom = -self.state.paddle_position - PADDLE_HEIGHT / 2.; 114 | 115 | self.paddles.buffer().set_data(vec![ 116 | Rect { 117 | upper_left: [left_paddle_left, left_paddle_top], 118 | lower_right: [left_paddle_right, left_paddle_bottom], 119 | color: palette::named::ORANGERED.into(), 120 | }, 121 | Rect { 122 | upper_left: [right_paddle_left, right_paddle_top], 123 | lower_right: [right_paddle_right, right_paddle_bottom], 124 | color: palette::named::ORANGERED.into(), 125 | }, 126 | ]); 127 | 128 | self.ball.buffer().set_data(vec![Circle { 129 | position: self.state.ball_position, 130 | radius: BALL_RADIUS, 131 | color: palette::named::NAVY.into(), 132 | }]); 133 | 134 | self.paddles.draw(renderer)?; 135 | self.ball.draw(renderer)?; 136 | 137 | Ok(true) 138 | } 139 | 140 | fn handle_key_down( 141 | &mut self, 142 | key: KeyCode, 143 | ) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 144 | match key { 145 | KeyCode::ArrowUp => self.paddle_direction = -1., 146 | KeyCode::ArrowDown => self.paddle_direction = 1., 147 | _ => return (false, false), 148 | } 149 | 150 | (false, true) 151 | } 152 | 153 | fn handle_key_up(&mut self, key: KeyCode) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 154 | match key { 155 | KeyCode::ArrowUp | KeyCode::ArrowDown => self.paddle_direction = 0., 156 | _ => return (false, false), 157 | } 158 | 159 | (false, true) 160 | } 161 | } 162 | 163 | impl Default for PongGame { 164 | fn default() -> Self { 165 | Self { 166 | state: GameState::default(), 167 | paddle_direction: 0., 168 | ball: CircleLayer::new(), 169 | paddles: RectLayer::new(), 170 | } 171 | } 172 | } 173 | 174 | fn main() { 175 | console_error_panic_hook::set_once(); 176 | wasm_logger::init(wasm_logger::Config::default()); 177 | yew::start_app::>(); 178 | } 179 | -------------------------------------------------------------------------------- /examples/primitive-scene/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-primitive-scene" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.74" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | limelight-primitives = {path="../../primitives"} 16 | limelight-transform = {path="../../transform"} 17 | palette = { version = "0.6.0", features = ["named"] } 18 | wasm-logger = "0.2.0" 19 | limelight-yew = {path="../../yew"} 20 | yew = "0.19.3" 21 | anyhow = "1.0.51" 22 | -------------------------------------------------------------------------------- /examples/primitive-scene/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demos 6 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/primitive-scene/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use limelight::{renderer::Drawable, Renderer}; 3 | use limelight_primitives::{ 4 | Circle, CircleLayer, Color, Hairline, HairlineLayer, Line, LineLayer, Orientation, Rect, 5 | RectLayer, 6 | }; 7 | use limelight_transform::TransformUniform; 8 | use limelight_yew::{ 9 | LimelightComponent, LimelightController, ShouldCancelEvent, ShouldRequestAnimationFrame, 10 | }; 11 | 12 | struct Primitives { 13 | layers: Vec>, 14 | fg_transform: TransformUniform, 15 | bg_transform: TransformUniform, 16 | } 17 | 18 | impl LimelightController for Primitives { 19 | fn draw(&mut self, renderer: &mut Renderer, _ts: f64) -> Result { 20 | for layer in self.layers.iter_mut() { 21 | layer.draw(renderer)?; 22 | } 23 | 24 | Ok(false) 25 | } 26 | 27 | fn handle_drag(&mut self, x: f32, y: f32) -> ShouldRequestAnimationFrame { 28 | self.fg_transform.pan((x, y)); 29 | self.bg_transform.shear((x, y)); 30 | true 31 | } 32 | 33 | fn handle_scroll( 34 | &mut self, 35 | _x_amount: f32, 36 | y_amount: f32, 37 | x_position: f32, 38 | y_position: f32, 39 | ) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 40 | self.fg_transform 41 | .scale(1. + y_amount as f32 / 3., (x_position, y_position)); 42 | self.bg_transform 43 | .scale(1. + y_amount as f32 / 3., (x_position, y_position)); 44 | (true, true) 45 | } 46 | } 47 | 48 | impl Default for Primitives { 49 | fn default() -> Self { 50 | let mut layers: Vec> = Vec::new(); 51 | let fg_transform = TransformUniform::new(); 52 | let bg_transform = TransformUniform::new(); 53 | 54 | let lines = LineLayer::new_transform(bg_transform.uniform()); 55 | let rects = RectLayer::new_transform(bg_transform.uniform()); 56 | let circles = CircleLayer::new_transform(fg_transform.uniform()); 57 | let hairlines = HairlineLayer::new_transform(fg_transform.uniform()); 58 | 59 | lines.buffer().set_data(vec![ 60 | Line { 61 | start: [0., 0.], 62 | end: [0.4, 0.9], 63 | width: 0.03, 64 | color: palette::named::GOLD.into(), 65 | }, 66 | Line { 67 | start: [-0.3, -0.3], 68 | end: [0.4, 0.9], 69 | width: 0.01, 70 | color: palette::named::FIREBRICK.into(), 71 | }, 72 | ]); 73 | 74 | rects.buffer().set_data(vec![ 75 | Rect { 76 | lower_right: [-0.3, 0.1], 77 | upper_left: [-0.8, 0.2], 78 | color: palette::named::SEAGREEN.into(), 79 | }, 80 | Rect { 81 | lower_right: [-0.3, 0.25], 82 | upper_left: [-0.6, 0.35], 83 | color: palette::named::PALEVIOLETRED.into(), 84 | }, 85 | Rect { 86 | lower_right: [-0.3, 0.4], 87 | upper_left: [-0.4, 0.5], 88 | color: palette::named::ORANGERED.into(), 89 | }, 90 | ]); 91 | 92 | circles.buffer().set_data(vec![ 93 | Circle { 94 | position: [-0.2, 0.3], 95 | radius: 0.2, 96 | color: Color(0x44332266), 97 | }, 98 | Circle { 99 | position: [0.3, 0.3], 100 | radius: 0.1, 101 | color: Color(0x66111111), 102 | }, 103 | ]); 104 | 105 | hairlines.buffer().set_data(vec![ 106 | Hairline { 107 | orientation: Orientation::Horizontal, 108 | location: 0.65, 109 | color: palette::named::RED.into(), 110 | }, 111 | Hairline { 112 | orientation: Orientation::Vertical, 113 | location: 0.65, 114 | color: Color::from(palette::named::DARKBLUE).opacity(0.4), 115 | }, 116 | Hairline { 117 | orientation: Orientation::Vertical, 118 | location: 0.7, 119 | color: Color::from(palette::named::DARKCYAN).opacity(0.3), 120 | }, 121 | Hairline { 122 | orientation: Orientation::Vertical, 123 | location: 0.75, 124 | color: Color::from(palette::named::DARKMAGENTA).opacity(0.2), 125 | }, 126 | ]); 127 | 128 | layers.push(Box::new(lines)); 129 | layers.push(Box::new(rects)); 130 | layers.push(Box::new(circles)); 131 | layers.push(Box::new(hairlines)); 132 | 133 | Primitives { 134 | layers, 135 | fg_transform, 136 | bg_transform, 137 | } 138 | } 139 | } 140 | 141 | fn main() { 142 | console_error_panic_hook::set_once(); 143 | wasm_logger::init(wasm_logger::Config::default()); 144 | yew::start_app::>(); 145 | } 146 | -------------------------------------------------------------------------------- /examples/shaderfun/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shaderfun" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.78" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | bytemuck = "1.7.2" 16 | yew = "0.19.3" 17 | limelight-yew = {path="../../yew"} 18 | anyhow = "1.0.53" 19 | -------------------------------------------------------------------------------- /examples/shaderfun/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demo 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/shaderfun/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | uniform float u_time; 6 | uniform vec2 u_pos; 7 | in vec2 v_pos; 8 | out vec4 color; 9 | 10 | void main() { 11 | if (length(u_pos - v_pos) < (sin(u_time) + 1.) / 10.) { 12 | color = vec4(0.8, u_pos, 1.0); 13 | } else { 14 | color = vec4(u_pos, 0.5, 1.0); 15 | } 16 | } -------------------------------------------------------------------------------- /examples/shaderfun/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec4 position; 4 | out vec2 v_pos; 5 | 6 | const vec2 positions[3] = vec2[3]( 7 | vec2(-1., 1.), 8 | vec2(3., 1.), 9 | vec2(-1., -3.) 10 | ); 11 | 12 | void main() { 13 | v_pos = positions[gl_VertexID]; 14 | gl_Position = vec4(v_pos, 0., 1.); 15 | } 16 | -------------------------------------------------------------------------------- /examples/shaderfun/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use limelight::{DrawMode, DummyBuffer, Program, Renderer, Uniform}; 3 | use limelight_yew::{LimelightComponent, LimelightController, ShouldRequestAnimationFrame}; 4 | 5 | struct Model { 6 | program: Program<(), ()>, 7 | time_uniform: Uniform, 8 | pos_uniform: Uniform<[f32; 2]>, 9 | } 10 | 11 | impl Default for Model { 12 | fn default() -> Model { 13 | let time_uniform = Uniform::new(0.0); 14 | let pos_uniform = Uniform::new([0.0, 0.0]); 15 | 16 | let program = Program::new( 17 | include_str!("../shaders/shader.vert"), 18 | include_str!("../shaders/shader.frag"), 19 | DrawMode::Triangles, 20 | ) 21 | .with_uniform("u_time", time_uniform.clone()) 22 | .with_uniform("u_pos", pos_uniform.clone()); 23 | 24 | Self { 25 | program, 26 | time_uniform, 27 | pos_uniform, 28 | } 29 | } 30 | } 31 | 32 | impl LimelightController for Model { 33 | fn draw(&mut self, renderer: &mut Renderer, ts: f64) -> Result { 34 | let ts = ts / 1000.; 35 | self.time_uniform.set_value(ts as _); 36 | 37 | renderer 38 | .render(&mut self.program, &DummyBuffer::new(3)) 39 | .unwrap(); 40 | 41 | Ok(true) 42 | } 43 | 44 | fn handle_mousemove(&mut self, x: f32, y: f32) -> ShouldRequestAnimationFrame { 45 | self.pos_uniform.set_value([x, y]); 46 | 47 | true 48 | } 49 | } 50 | 51 | fn main() { 52 | console_error_panic_hook::set_once(); 53 | yew::start_app::>(); 54 | } 55 | -------------------------------------------------------------------------------- /examples/snowflake/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-snowflake" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.74" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | limelight-primitives = {path="../../primitives"} 16 | limelight-transform = {path="../../transform"} 17 | palette = { version = "0.6.0", features = ["named"] } 18 | wasm-logger = "0.2.0" 19 | limelight-yew = {path="../../yew"} 20 | yew = "0.19.3" 21 | anyhow = "1.0.51" 22 | nalgebra = "0.30.1" 23 | rand = {version="0.8.4", default_features=false, features=["std_rng"]} 24 | getrandom = { version = "0.2.3", features = ["js"] } 25 | -------------------------------------------------------------------------------- /examples/snowflake/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | limelight demos 7 | 8 | 9 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/snowflake/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use anyhow::Result; 5 | use limelight::{renderer::Drawable, Renderer}; 6 | use limelight_primitives::{Line3D, Line3DLayer, Color}; 7 | use limelight_yew::{ 8 | LimelightComponent, LimelightComponentProps, LimelightController, ShouldRequestAnimationFrame, 9 | }; 10 | use palette::{Gradient, Hsl, Srgb}; 11 | use rand::prelude::ThreadRng; 12 | use rand::{Rng, thread_rng}; 13 | use std::f32::consts::TAU; 14 | use palette::chromatic_adaptation::AdaptInto; 15 | use rand::seq::SliceRandom; 16 | 17 | const MAX_LINES: usize = 500; 18 | 19 | struct Snowflake { 20 | snowflake: Line3DLayer, 21 | lines: Vec, 22 | rng: ThreadRng, 23 | colors: Vec, 24 | } 25 | 26 | impl LimelightController for Snowflake { 27 | fn draw(&mut self, renderer: &mut Renderer, _ts: f64) -> Result { 28 | if self.lines.len() < MAX_LINES { 29 | let start_on_line = self.rng.gen_range(0. .. 0.8); 30 | let rot = self.rng.gen_range(0. ..TAU); 31 | let z_delta = self.rng.gen_range(-0.1 .. 0.1); 32 | let size = self.rng.gen_range(0. .. 0.02f32).sqrt(); 33 | let color = self.colors.choose(&mut self.rng).unwrap(); 34 | let cc = Color(*limelight::bytemuck::from_bytes(&[ 35 | (color.red * 255.) as u8, 36 | (color.green * 255.) as u8, 37 | (color.blue * 255.) as u8, 38 | 0xff, 39 | ])); 40 | 41 | for i in 0..6 { 42 | for j in [-1., 1.] { 43 | let theta = TAU * (i as f32 / 6.); 44 | let xx = start_on_line * theta.cos(); 45 | let yy = start_on_line * theta.sin(); 46 | 47 | let theta2 = theta + rot * j; 48 | let xx_delta = theta2.cos() * size; 49 | let yy_delta = theta2.sin() * size; 50 | 51 | self.lines.push(Line3D { 52 | start: [xx, yy, 0.], 53 | end: [xx + xx_delta, yy + yy_delta, z_delta], 54 | width: 0.003, 55 | color: cc, 56 | }); 57 | } 58 | } 59 | 60 | self.snowflake.buffer().set_data(self.lines.clone()); 61 | } 62 | 63 | self.snowflake.draw(renderer)?; 64 | 65 | Ok(true) 66 | } 67 | 68 | fn handle_mousemove(&mut self, x: f32, y: f32) -> ShouldRequestAnimationFrame { 69 | let rot = nalgebra::Rotation3::from_euler_angles(-y / 2., x / 2., 0.); 70 | 71 | self.snowflake.transform().set_value(rot.to_homogeneous().into()); 72 | 73 | true 74 | } 75 | } 76 | 77 | impl Snowflake { 78 | fn new() -> Rc> { 79 | let mut rng = thread_rng(); 80 | let c1 = Hsl::new(rng.gen_range(0. .. 360.), rng.gen_range(0.6 .. 1.), rng.gen_range(0.6 .. 0.8)); 81 | let c2 = Hsl::new(rng.gen_range(0. .. 360.), rng.gen_range(0. .. 1.), rng.gen_range(0.6 .. 1.)); 82 | let c3 = Hsl::new(rng.gen_range(0. .. 360.), rng.gen_range(0.6 .. 1.), rng.gen_range(0.6 .. 0.7)); 83 | let colors: Vec = Gradient::new(vec![c1, c2, c3]).take(10).map(|d| d.adapt_into()).collect(); 84 | 85 | Rc::new(RefCell::new(Snowflake { 86 | snowflake: Line3DLayer::default(), 87 | lines: Vec::new(), 88 | rng, 89 | colors, 90 | })) 91 | } 92 | } 93 | 94 | pub fn main() { 95 | console_error_panic_hook::set_once(); 96 | yew::start_app_with_props::>(LimelightComponentProps { 97 | controller: Snowflake::new(), 98 | height: 500, 99 | width: 500, 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /examples/zoom-pan/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-example-zoom-pan" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | console_error_panic_hook = "0.1.6" 8 | wasm-bindgen = "0.2.78" 9 | web-sys = { version="0.3.55", features = [ 10 | 'HtmlCanvasElement', 11 | 'Window', 12 | 'Document', 13 | ] } 14 | limelight = { path="../../limelight" } 15 | limelight-transform = { path="../../transform" } 16 | bytemuck = "1.7.2" 17 | wasm-logger = "0.2.0" 18 | limelight-yew = {path="../../yew"} 19 | anyhow = "1.0.51" 20 | yew = "0.19.3" 21 | -------------------------------------------------------------------------------- /examples/zoom-pan/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | limelight demo 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/zoom-pan/shaders/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | out vec4 color; 6 | 7 | void main() { 8 | color = vec4(0., 0., 0., 1.0); 9 | } -------------------------------------------------------------------------------- /examples/zoom-pan/shaders/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 position; 4 | uniform mat4 u_transform; 5 | 6 | void main() { 7 | gl_Position = vec4(position * 0.5, 0., 1.) * u_transform; 8 | } -------------------------------------------------------------------------------- /examples/zoom-pan/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use limelight::{attribute, Buffer, BufferUsageHint, DrawMode, Program, Renderer}; 3 | use limelight_transform::TransformUniform; 4 | use limelight_yew::{ 5 | LimelightComponent, LimelightController, ShouldCancelEvent, ShouldRequestAnimationFrame, 6 | }; 7 | 8 | struct ZoomPan { 9 | program: Program, 10 | buffer: Buffer, 11 | transform: TransformUniform, 12 | } 13 | 14 | impl Default for ZoomPan { 15 | fn default() -> Self { 16 | let theta1 = 0.; 17 | let theta2 = theta1 + (std::f32::consts::TAU / 3.); 18 | let theta3 = theta2 + (std::f32::consts::TAU / 3.); 19 | 20 | let data = vec![ 21 | VertexDescription::new(theta1.cos(), theta1.sin()), 22 | VertexDescription::new(theta2.cos(), theta2.sin()), 23 | VertexDescription::new(theta3.cos(), theta3.sin()), 24 | ]; 25 | 26 | let buffer = Buffer::new(data, BufferUsageHint::DynamicDraw); 27 | let transform = TransformUniform::new(); 28 | 29 | let program = Program::new( 30 | include_str!("../shaders/shader.vert"), 31 | include_str!("../shaders/shader.frag"), 32 | DrawMode::Triangles, 33 | ) 34 | .with_uniform("u_transform", transform.uniform()); 35 | 36 | ZoomPan { 37 | buffer, 38 | program, 39 | transform, 40 | } 41 | } 42 | } 43 | 44 | impl LimelightController for ZoomPan { 45 | fn draw( 46 | &mut self, 47 | renderer: &mut Renderer, 48 | _ts: f64, 49 | ) -> Result { 50 | renderer.render(&mut self.program, &self.buffer)?; 51 | Ok(false) 52 | } 53 | 54 | fn handle_drag(&mut self, x: f32, y: f32) -> limelight_yew::ShouldRequestAnimationFrame { 55 | self.transform.pan((x, y)); 56 | true 57 | } 58 | 59 | fn handle_scroll( 60 | &mut self, 61 | x_amount: f32, 62 | y_amount: f32, 63 | _x_position: f32, 64 | _y_position: f32, 65 | ) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 66 | self.transform.pan((x_amount, y_amount)); 67 | (true, true) 68 | } 69 | 70 | fn handle_pinch( 71 | &mut self, 72 | amount: f32, 73 | x: f32, 74 | y: f32, 75 | ) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 76 | let scale_factor = 1. + amount / 100.; 77 | self.transform.scale(scale_factor, (x, y)); 78 | (true, true) 79 | } 80 | } 81 | 82 | #[attribute] 83 | struct VertexDescription { 84 | position: [f32; 2], 85 | } 86 | 87 | impl VertexDescription { 88 | pub fn new(x: f32, y: f32) -> Self { 89 | VertexDescription { position: [x, y] } 90 | } 91 | } 92 | 93 | fn main() { 94 | console_error_panic_hook::set_once(); 95 | wasm_logger::init(wasm_logger::Config::default()); 96 | yew::start_app::>(); 97 | } 98 | -------------------------------------------------------------------------------- /limelight/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight" 3 | version = "0.1.3" 4 | edition = "2021" 5 | repository = "https://github.com/drifting-in-space/limelight" 6 | description = "WebGL2 wrapper with a focus on making high-performance graphics code easier to write and maintain" 7 | license = "MIT" 8 | readme = "README.md" 9 | keywords = ["webgl"] 10 | 11 | [dependencies] 12 | bytemuck = { version = "1.7.2", features = ["derive"] } 13 | js-sys = "0.3.55" 14 | wasm-bindgen = "0.2.78" 15 | web-sys = { version="0.3.55", features = [ 16 | 'WebGlActiveInfo', 17 | 'WebGlBuffer', 18 | 'WebGl2RenderingContext', 19 | 'WebGlVertexArrayObject', 20 | 'WebGlUniformLocation', 21 | 'WebGlProgram', 22 | 'WebGlShader', 23 | 'WebGlTexture', 24 | ] } 25 | limelight-derive = { version="0.1.1", path="./limelight-derive" } 26 | anyhow = "1.0.48" 27 | slice-of-array = "0.3.1" 28 | log = "0.4.14" 29 | -------------------------------------------------------------------------------- /limelight/README.md: -------------------------------------------------------------------------------- 1 | # `limelight` 2 | 3 | [![GitHub Repo stars](https://img.shields.io/github/stars/drifting-in-space/limelight?style=social)](https://github.com/drifting-in-space/limelight) 4 | [![crates.io](https://img.shields.io/crates/v/limelight.svg)](https://crates.io/crates/limelight) 5 | [![docs.rs](https://img.shields.io/badge/docs-release-brightgreen)](https://docs.rs/limelight/) 6 | [![Rust](https://github.com/drifting-in-space/limelight/actions/workflows/rust.yml/badge.svg)](https://github.com/drifting-in-space/limelight/actions/workflows/rust.yml) 7 | 8 | This crate provides two layers of abstraction on top of WebGL. The first is the **shadow GPU** layer, 9 | which abstracts away the statefulness of WebGL to provide a more function- and data-oriented interface. 10 | The second layer is the `Renderer` API, which provides a typed interface on top of the (untyped) 11 | shadow GPU. 12 | 13 | If you don't want to use custom shaders and just want to draw lots of shapes, you can ignore the 14 | examples below and look at the examples in the [primitives](https://github.com/drifting-in-space/limelight/tree/main/primitives) crate ([docs](https://docs.rs/limelight-primitives/0.1.3/limelight_primitives/)). 15 | 16 | ## Getting started 17 | 18 | See the [examples](https://github.com/drifting-in-space/limelight/tree/main/examples) directory for 19 | runnable examples. 20 | 21 | This tutorial assumes you're familiar with basic WebGL terminology, like vertex and fragment shaders, 22 | uniforms, and buffers. 23 | 24 | ### Drawing a triangle 25 | 26 | ([full code](https://github.com/drifting-in-space/limelight/tree/main/examples/01-triangle), 27 | [demo](https://drifting-in-space.github.io/limelight/01-triangle/)) 28 | 29 | [![A colorful triangle](https://github.com/drifting-in-space/limelight/raw/main/assets/01-triangle.png)](https://drifting-in-space.github.io/limelight/01-triangle/) 30 | 31 | This example demonstrates the three main steps to produce an image with limelight: 32 | 1. Create a `Program` object. A `Program` in limelight contains the vertex and fragment shader pair 33 | (a `WebGLProgram` object), and also contains program-specific state. 34 | 2. Create a `Renderer`. After we have initialized all of our programs with the GL context, we transfer ownership 35 | of the GL context into a `Renderer`, which then becomes responsible for all GL-side state transitions. 36 | 3. We call `renderer.render(program, buffer)`, which causes the triangle to be drawn. We have not attached a 37 | vertex attribute buffer in this example, and instead use the vertex shader to generate the vertices. We 38 | still need to tell WebGL *how many* vertices (3) we want to generate, so we pass in a `DummyBuffer` of size `3`. 39 | 40 | ```rust 41 | use web_sys::WebGl2RenderingContext; 42 | use limelight::{Program, Renderer, DummyBuffer, DrawMode}; 43 | 44 | fn render_triangle(gl: WebGl2RenderingContext) { 45 | // limelight doesn't touch the DOM at all. Use your preferred 46 | // framework to create a canvas and create a WebGL2 context 47 | // from it. 48 | 49 | // Create a shader program by passing in GLSL code as strings for 50 | // the fragment and vertex shaders. 51 | let mut program = Program::new( 52 | include_str!("../../examples/01-triangle/shaders/shader.vert"), 53 | include_str!("../../examples/01-triangle/shaders/shader.frag"), 54 | DrawMode::Triangles 55 | ); 56 | 57 | // Create a renderer. The renderer becomes the owner of the 58 | // WebGl2RenderingContext, to ensure that its internal representation 59 | // of the GPU state is always accureate. 60 | let mut renderer = Renderer::new(gl); 61 | 62 | // Run the program, rendering the results to the screen. We are 63 | // not passing any vertex attribute data, so we use a `DummyBuffer` 64 | // which renders three vertices: one for each corner of a triangle. 65 | renderer.render(&mut program, &DummyBuffer::new(3)).unwrap(); 66 | } 67 | ``` 68 | 69 | ### Using buffers 70 | 71 | ([full code](https://github.com/drifting-in-space/limelight/tree/main/examples/02-buffer), 72 | [demo](https://drifting-in-space.github.io/limelight/02-buffer/)) 73 | 74 | [![Two small triangles](https://github.com/drifting-in-space/limelight/raw/main/assets/02-buffer.png)](https://drifting-in-space.github.io/limelight/02-buffer/) 75 | 76 | Buffers enable arbitrary vertex attribute data to be passed into the shaders. Limelight provides a 77 | procedural macro (`attribute`) for mapping from a Rust-side `struct` to a GPU-side set of 78 | vertex attributes. To use this macro, your crate will also have to depend on [`bytemuck`](https://docs.rs/bytemuck/latest/bytemuck/) and its `derive` feature. 79 | 80 | ```rust 81 | use web_sys::WebGl2RenderingContext; 82 | use limelight::{Program, Renderer, Buffer, DrawMode, BufferUsageHint, attribute}; 83 | 84 | // This attribute macro derives a number of traits, including `VertexAttribute`, which 85 | // is required for a type to be used in an `Buffer`. 86 | #[attribute] 87 | struct VertexDescription { 88 | position: [f32; 2], // field names are mapped to variables in the shader. 89 | } 90 | 91 | impl VertexDescription { 92 | pub fn new(x: f32, y: f32) -> Self { 93 | VertexDescription { position: [x, y] } 94 | } 95 | } 96 | 97 | fn render_triangles(gl: WebGl2RenderingContext) { 98 | let mut program = Program::new( 99 | include_str!("../../examples/02-buffer/shaders/shader.vert"), 100 | include_str!("../../examples/02-buffer/shaders/shader.frag"), 101 | DrawMode::Triangles 102 | ); 103 | 104 | let mut renderer = Renderer::new(gl); 105 | 106 | let data = vec![ 107 | // Lower-left triangle. 108 | VertexDescription::new(-0.1, -0.1), 109 | VertexDescription::new(-0.5, -0.1), 110 | VertexDescription::new(-0.5, -0.5), 111 | // Upper-right triangle. 112 | VertexDescription::new(0.1, 0.1), 113 | VertexDescription::new(0.5, 0.1), 114 | VertexDescription::new(0.5, 0.5), 115 | ]; 116 | 117 | // Declare a buffer. 118 | let mut buffer: Buffer = 119 | Buffer::new(data, BufferUsageHint::StaticDraw); 120 | 121 | renderer.render(&mut program, &buffer).unwrap(); 122 | } 123 | ``` 124 | 125 | ### Uniforms 126 | 127 | ([full code](https://github.com/drifting-in-space/limelight/tree/main/examples/03-uniform), 128 | [demo](https://drifting-in-space.github.io/limelight/03-uniform/)) 129 | 130 | [![A scaled and rotated triangle](https://github.com/drifting-in-space/limelight/raw/main/assets/03-uniform.png)](https://drifting-in-space.github.io/limelight/03-uniform/) 131 | 132 | Uniforms are values that can be used in both shader and fragment programs. They can vary 133 | between `render` calls, but for a given render call each uniform has a constant value 134 | across all vertices and fragments. 135 | 136 | ```rust 137 | use limelight::{DrawMode, DummyBuffer, Program, Renderer, Uniform}; 138 | use web_sys::WebGl2RenderingContext; 139 | 140 | fn render_triangles_with_uniform(gl: WebGl2RenderingContext) { 141 | // This will correspond to "uniform float u_rotate" in the vertex shader. 142 | let rotate_uniform = Uniform::new(std::f32::consts::PI / 3.4); 143 | 144 | // This will correspond to "uniform vec2 u_scale" in the vertex shader. 145 | let scale_uniform = Uniform::new([0.5, 0.8]); 146 | 147 | // This will correspond to "uniform vec3 u_color" in the fragment shader. 148 | let color_uniform = Uniform::new([0.9, 0.2, 0.3]); 149 | 150 | let mut program = Program::new( 151 | include_str!("../../examples/03-uniform/shaders/shader.vert"), 152 | include_str!("../../examples/03-uniform/shaders/shader.frag"), 153 | DrawMode::Triangles, 154 | ) 155 | // We need to map the uniforms when we create the program. 156 | // The GPU-side types are automatically inferred from the Rust types. 157 | .with_uniform("u_rotate", rotate_uniform) 158 | .with_uniform("u_scale", scale_uniform) 159 | .with_uniform("u_color", color_uniform); 160 | 161 | let mut renderer = Renderer::new(gl); 162 | renderer.render(&mut program, &DummyBuffer::new(3)).unwrap(); 163 | } 164 | ``` 165 | 166 | ### Animation 167 | 168 | ([full code](https://github.com/drifting-in-space/limelight/tree/main/examples/04-animate), 169 | [demo](https://drifting-in-space.github.io/limelight/04-animate/)) 170 | 171 | The previous examples have rendered static images, so we haven't had a need to separate code 172 | that sets up the initial data structures from code that updates GPU-side data and triggers an 173 | animation. In this example, we separate the code into a `new()` method that is called once, 174 | and a `render` method that is called on every frame. 175 | 176 | limelight is not a framework, and in order to integrate with other frameworks, it is not opinionated 177 | as to how you structure your code. This example shows one way you might choose to structure code for 178 | a simple animation (see the [full code](https://github.com/drifting-in-space/limelight/tree/main/examples/04-animate) 179 | to see how it can be integrated with the [Yew](https://yew.rs/) web framework). 180 | 181 | `buffer.set_data` and `uniform.set_data` are *lazy*: they do not result in any GPU activity until 182 | the next time the buffer is used in a render call. (See [WebGL Insights](http://www.webglinsights.com/) 183 | section 14.2, *Deferring until the Draw Cycle*.) If a buffer or uniform is unchanged between render 184 | calls, it is not re-written to the GPU. 185 | 186 | ```rust 187 | use limelight::{Buffer, BufferUsageHint, DrawMode, Program, Renderer, Uniform, attribute}; 188 | use web_sys::WebGl2RenderingContext; 189 | 190 | struct Animation { 191 | program: Program, 192 | buffer: Buffer, 193 | uniform: Uniform<[f32; 3]>, 194 | } 195 | 196 | impl Animation { 197 | pub fn new(gl: &WebGl2RenderingContext) -> Self { 198 | let buffer = Buffer::new(vec![], BufferUsageHint::DynamicDraw); 199 | let uniform = Uniform::new([0., 0., 0.]); 200 | 201 | let program = Program::new( 202 | include_str!("../../examples/04-animate/shaders/shader.vert"), 203 | include_str!("../../examples/04-animate/shaders/shader.frag"), 204 | DrawMode::Triangles, 205 | ) 206 | // Note that we clone uniform, so that we can retain a handle to it. 207 | // Cloning a `Uniform` results in a reference-counted pointer to the 208 | // same uniform. 209 | .with_uniform("u_color", uniform.clone()); 210 | 211 | Animation { 212 | buffer, 213 | program, 214 | uniform 215 | } 216 | } 217 | 218 | pub fn render(&mut self, time: f64, renderer: &mut Renderer) { 219 | let theta1 = time as f32 / 1000.; 220 | let theta2 = theta1 + (std::f32::consts::TAU / 3.); 221 | let theta3 = theta2 + (std::f32::consts::TAU / 3.); 222 | 223 | self.buffer.set_data(vec![ 224 | VertexDescription::new(theta1.cos(), theta1.sin()), 225 | VertexDescription::new(theta2.cos(), theta2.sin()), 226 | VertexDescription::new(theta3.cos(), theta3.sin()), 227 | ]); 228 | 229 | let r = (time as f32 / 3000.).sin() / 2. + 0.5; 230 | let g = (time as f32 / 5000.).sin() / 2. + 0.5; 231 | let b = (time as f32 / 7000.).sin() / 2. + 0.5; 232 | 233 | self.uniform.set_value([r, g, b]); 234 | 235 | renderer.render(&mut self.program, &self.buffer).unwrap(); 236 | } 237 | } 238 | 239 | #[attribute] 240 | struct VertexDescription { 241 | position: [f32; 2], 242 | } 243 | 244 | impl VertexDescription { 245 | pub fn new(x: f32, y: f32) -> Self { 246 | VertexDescription { position: [x, y] } 247 | } 248 | } 249 | ``` 250 | -------------------------------------------------------------------------------- /limelight/limelight-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-derive" 3 | version = "0.1.1" 4 | edition = "2021" 5 | repository = "https://github.com/drifting-in-space/limelight" 6 | description = "Derive macro for limelight" 7 | license = "MIT" 8 | readme = "README.md" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | inflections = "1.1.1" 15 | proc-macro2 = "1.0.32" 16 | quote = "1.0.10" 17 | syn = { version = "1.0.81", features = ["full"] } 18 | -------------------------------------------------------------------------------- /limelight/limelight-derive/README.md: -------------------------------------------------------------------------------- 1 | This crate is just a derive macro and should not be used directly. 2 | 3 | See [limelight](https://github.com/drifting-in-space/limelight). -------------------------------------------------------------------------------- /limelight/limelight-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | use syn::ItemStruct; 6 | 7 | #[proc_macro_attribute] 8 | pub fn attribute( 9 | _attr: proc_macro::TokenStream, 10 | item: proc_macro::TokenStream, 11 | ) -> proc_macro::TokenStream { 12 | let item: TokenStream = item.into(); 13 | 14 | let r = quote! { 15 | #[repr(C)] 16 | #[derive(Clone, Copy, limelight::Attribute, limelight::bytemuck::Pod, limelight::bytemuck::Zeroable)] 17 | #item 18 | }; 19 | 20 | r.into() 21 | } 22 | 23 | #[proc_macro_derive(Attribute)] 24 | pub fn vertex_attribute_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 25 | impl_vertex_attribute_derive(input.into()).into() 26 | } 27 | 28 | fn bind(field: &syn::Field) -> TokenStream { 29 | let name = field.ident.as_ref().unwrap().to_string(); 30 | let kind = &field.ty; 31 | 32 | quote! { 33 | limelight::AttributeBinding { 34 | variable_name: (#name).to_string(), 35 | kind: <#kind as limelight::AsSizedDataType>::as_sized_data_type(), 36 | } 37 | } 38 | } 39 | 40 | fn impl_vertex_attribute_derive(input: TokenStream) -> TokenStream { 41 | let ast: ItemStruct = syn::parse2(input).expect("Should decorate a struct."); 42 | 43 | let name = &ast.ident; 44 | 45 | let bindings: Vec = match &ast.fields { 46 | syn::Fields::Named(fields) => fields.named.iter().map(bind).collect(), 47 | _ => panic!("Only structs with named fields can derive StateMachine currently."), 48 | }; 49 | 50 | quote! { 51 | impl limelight::Attribute for #name { 52 | fn describe() -> Vec { 53 | vec![ 54 | #(#bindings),* 55 | ] 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /limelight/src/attribute.rs: -------------------------------------------------------------------------------- 1 | use crate::webgl::types::{DataType, SizedDataType}; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub struct AttributeBinding { 5 | pub variable_name: String, 6 | pub kind: SizedDataType, 7 | } 8 | 9 | impl AttributeBinding { 10 | pub fn new(name: &str, data_type: DataType, size: i32) -> Self { 11 | AttributeBinding { 12 | variable_name: name.to_string(), 13 | kind: SizedDataType::new(data_type, size), 14 | } 15 | } 16 | } 17 | 18 | pub trait Attribute: bytemuck::Pod + bytemuck::Zeroable { 19 | fn describe() -> Vec; 20 | } 21 | 22 | impl Attribute for () { 23 | fn describe() -> Vec { 24 | Vec::new() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /limelight/src/buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::{shadow_gpu::BufferHandle, webgl::buffer::BufferUsageHint, Attribute}; 2 | use std::marker::PhantomData; 3 | 4 | #[allow(clippy::len_without_is_empty)] 5 | pub trait BufferLike { 6 | fn get_buffer(&self) -> Option; 7 | 8 | fn len(&self) -> usize; 9 | } 10 | 11 | #[derive(Clone)] 12 | pub struct Buffer { 13 | handle: BufferHandle, 14 | _ph: PhantomData, 15 | } 16 | 17 | impl Buffer { 18 | pub fn new(data: Vec, usage_hint: BufferUsageHint) -> Self { 19 | let handle = BufferHandle::new(usage_hint); 20 | handle.set_data(data); 21 | 22 | Buffer { 23 | handle, 24 | _ph: PhantomData::default(), 25 | } 26 | } 27 | 28 | pub fn new_empty(usage_hint: BufferUsageHint) -> Self { 29 | Self::new(Vec::new(), usage_hint) 30 | } 31 | 32 | pub fn set_data(&self, data: Vec) { 33 | self.handle.set_data(data); 34 | } 35 | } 36 | 37 | impl BufferLike for Buffer { 38 | fn get_buffer(&self) -> Option { 39 | Some(self.handle.clone()) 40 | } 41 | 42 | fn len(&self) -> usize { 43 | self.handle.len() 44 | } 45 | } 46 | 47 | pub struct DummyBuffer { 48 | size: usize, 49 | } 50 | 51 | impl DummyBuffer { 52 | pub fn new(size: usize) -> Self { 53 | DummyBuffer { size } 54 | } 55 | } 56 | 57 | impl BufferLike<()> for DummyBuffer { 58 | fn len(&self) -> usize { 59 | self.size 60 | } 61 | 62 | fn get_buffer(&self) -> Option { 63 | None 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /limelight/src/draw_modes.rs: -------------------------------------------------------------------------------- 1 | #[repr(u32)] 2 | #[derive(Clone, Copy, Debug)] 3 | pub enum DrawMode { 4 | Points = 0x0000, 5 | Lines = 0x0001, 6 | LineLoop = 0x0002, 7 | LineStrip = 0x0003, 8 | Triangles = 0x0004, 9 | TriangleStrip = 0x0005, 10 | TriangleFan = 0x0006, 11 | } 12 | -------------------------------------------------------------------------------- /limelight/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod attribute; 4 | pub mod buffer; 5 | pub mod draw_modes; 6 | pub mod program; 7 | pub mod renderer; 8 | pub mod shadow_gpu; 9 | pub mod state; 10 | pub mod uniform; 11 | pub mod webgl; 12 | 13 | pub use bytemuck; 14 | pub use limelight_derive::{attribute, Attribute}; 15 | 16 | pub use attribute::{Attribute, AttributeBinding}; 17 | pub use buffer::{Buffer, DummyBuffer}; 18 | pub use draw_modes::DrawMode; 19 | pub use program::Program; 20 | pub use renderer::Renderer; 21 | pub use uniform::Uniform; 22 | pub use webgl::buffer::{BufferBindPoint, BufferUsageHint}; 23 | pub use webgl::types::AsSizedDataType; 24 | 25 | // #[allow(unused)] 26 | // macro_rules! console_log { 27 | // ($($x: expr), +) => ( 28 | // web_sys::console::log_1(&wasm_bindgen::JsValue::from( 29 | // format!($($x),+))); 30 | // ) 31 | // } 32 | 33 | // #[allow(unused)] 34 | // pub(crate) use console_log; 35 | -------------------------------------------------------------------------------- /limelight/src/program.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::{ 3 | collections::{hash_map::Entry, HashMap}, 4 | marker::PhantomData, 5 | }; 6 | 7 | use crate::{ 8 | shadow_gpu::{AttributeInfo, ProgramHandle, ShadowGpu, UniformHandle, UniformValueType}, 9 | state::StateDescriptor, 10 | uniform::GenericUniform, 11 | Attribute, DrawMode, Uniform, 12 | }; 13 | 14 | pub trait ProgramLike { 15 | fn get_program(&mut self, gpu: &ShadowGpu) -> Result<&BoundProgram>; 16 | 17 | fn globals(&self) -> StateDescriptor; 18 | 19 | fn draw_mode(&self) -> DrawMode; 20 | } 21 | 22 | pub struct BoundProgram { 23 | handle: ProgramHandle, 24 | pub uniforms: Vec<(UniformHandle, Box)>, 25 | draw_mode: DrawMode, 26 | state: StateDescriptor, 27 | _ph: PhantomData, 28 | _phi: PhantomData, 29 | } 30 | 31 | impl BoundProgram { 32 | pub fn handle(&self) -> ProgramHandle { 33 | self.handle.clone() 34 | } 35 | 36 | pub fn attributes(&self) -> &HashMap { 37 | &self.handle.attributes 38 | } 39 | } 40 | 41 | pub struct UnboundProgram { 42 | fragment_shader_source: String, 43 | vertex_shader_source: String, 44 | uniforms: HashMap>, 45 | draw_mode: DrawMode, 46 | state: StateDescriptor, 47 | _ph: PhantomData, 48 | _phi: PhantomData, 49 | } 50 | 51 | impl UnboundProgram { 52 | pub fn with_uniform( 53 | &mut self, 54 | name: &str, 55 | uniform: Uniform, 56 | ) -> &mut Self { 57 | match self.uniforms.entry(name.to_string()) { 58 | Entry::Occupied(_) => panic!("Tried to set uniform {} more than once.", name), 59 | Entry::Vacant(e) => e.insert(Box::new(uniform)), 60 | }; 61 | 62 | self 63 | } 64 | 65 | fn new_dummy() -> Self { 66 | UnboundProgram { 67 | _ph: PhantomData::default(), 68 | _phi: PhantomData::default(), 69 | fragment_shader_source: "".to_string(), 70 | vertex_shader_source: "".to_string(), 71 | uniforms: HashMap::new(), 72 | state: StateDescriptor::default(), 73 | draw_mode: DrawMode::Triangles, 74 | } 75 | } 76 | 77 | pub fn bind(self, gpu: &ShadowGpu) -> Result> { 78 | let vertex_shader = gpu.compile_vertex_shader(&self.vertex_shader_source)?; 79 | let fragment_shader = gpu.compile_fragment_shader(&self.fragment_shader_source)?; 80 | let program = gpu.link_program(&fragment_shader, &vertex_shader)?; 81 | 82 | let mut bound_uniforms = Vec::with_capacity(self.uniforms.len()); 83 | 84 | for (name, uniform) in self.uniforms { 85 | let loc = gpu.get_uniform_handle(&program, &name)?; 86 | bound_uniforms.push((loc, uniform)); 87 | } 88 | 89 | Ok(BoundProgram { 90 | handle: program, 91 | uniforms: bound_uniforms, 92 | draw_mode: self.draw_mode, 93 | state: self.state, 94 | _ph: PhantomData::default(), 95 | _phi: PhantomData::default(), 96 | }) 97 | } 98 | } 99 | 100 | pub enum Program { 101 | Unbound(UnboundProgram), 102 | Bound(BoundProgram), 103 | } 104 | 105 | impl Program { 106 | pub fn new( 107 | vertex_shader_source: &str, 108 | fragment_shader_source: &str, 109 | draw_mode: DrawMode, 110 | ) -> Self { 111 | Program::Unbound(UnboundProgram { 112 | fragment_shader_source: fragment_shader_source.to_string(), 113 | vertex_shader_source: vertex_shader_source.to_string(), 114 | uniforms: HashMap::new(), 115 | draw_mode, 116 | state: StateDescriptor::default(), 117 | _ph: PhantomData::default(), 118 | _phi: PhantomData::default(), 119 | }) 120 | } 121 | } 122 | 123 | impl ProgramLike for BoundProgram { 124 | fn get_program(&mut self, _gpu: &ShadowGpu) -> Result<&BoundProgram> { 125 | Ok(self) 126 | } 127 | 128 | fn globals(&self) -> StateDescriptor { 129 | self.state.clone() 130 | } 131 | 132 | fn draw_mode(&self) -> DrawMode { 133 | self.draw_mode 134 | } 135 | } 136 | 137 | impl Program { 138 | pub fn with_uniform(mut self, name: &str, uniform: Uniform) -> Self { 139 | match &mut self { 140 | Program::Bound(_) => { 141 | panic!("Tried calling with_uniform on a program that is already bound.") 142 | } 143 | Program::Unbound(p) => { 144 | p.with_uniform(name, uniform); 145 | } 146 | } 147 | 148 | self 149 | } 150 | 151 | pub fn with_state(mut self, state: StateDescriptor) -> Self { 152 | match &mut self { 153 | Program::Bound(_) => { 154 | panic!("Tried calling with_uniform on a program that is already bound.") 155 | } 156 | Program::Unbound(p) => { 157 | p.state = state; 158 | } 159 | } 160 | 161 | self 162 | } 163 | } 164 | 165 | impl ProgramLike for Program { 166 | fn get_program(&mut self, gpu: &ShadowGpu) -> Result<&BoundProgram> { 167 | match self { 168 | Program::Bound(p) => Ok(p), 169 | Program::Unbound(p) => { 170 | let mut dummy_program = UnboundProgram::new_dummy(); 171 | std::mem::swap(&mut dummy_program, p); 172 | 173 | let result = dummy_program.bind(gpu)?; 174 | *self = Program::Bound(result); 175 | 176 | match self { 177 | Program::Bound(result) => Ok(result), 178 | _ => panic!(), 179 | } 180 | } 181 | } 182 | } 183 | 184 | fn globals(&self) -> StateDescriptor { 185 | match self { 186 | Program::Bound(b) => b.state.clone(), 187 | Program::Unbound(b) => b.state.clone(), 188 | } 189 | } 190 | 191 | fn draw_mode(&self) -> DrawMode { 192 | match self { 193 | Program::Bound(p) => p.draw_mode, 194 | Program::Unbound(p) => p.draw_mode, 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /limelight/src/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | 3 | use crate::{ 4 | attribute::Attribute, 5 | buffer::BufferLike, 6 | program::ProgramLike, 7 | shadow_gpu::{AttributeInfo, BufferBinding, BufferHandle, GpuState, ShadowGpu}, 8 | }; 9 | use anyhow::Result; 10 | use web_sys::WebGl2RenderingContext; 11 | 12 | pub struct Renderer { 13 | gpu: ShadowGpu, 14 | } 15 | 16 | enum DrawCall { 17 | DrawArrays { 18 | first: usize, 19 | count: usize, 20 | }, 21 | DrawArraysInstanced { 22 | first: usize, 23 | count: usize, 24 | instances: usize, 25 | }, 26 | } 27 | 28 | struct BufferBindingGroup { 29 | bindings: BTreeMap>, 30 | attributes: HashMap, 31 | } 32 | 33 | impl BufferBindingGroup { 34 | fn new(attributes: HashMap) -> Self { 35 | Self { 36 | attributes, 37 | bindings: BTreeMap::new(), 38 | } 39 | } 40 | 41 | fn add_buffer(&mut self, buffer: &impl BufferLike, divisor: u32) { 42 | if let Some(buffer) = buffer.get_buffer() { 43 | let stride = T::describe().into_iter().map(|d| d.kind.byte_size()).sum(); 44 | let mut offset = 0; 45 | let mut bindings = Vec::new(); 46 | 47 | for attribute in T::describe() { 48 | if let Some(program_binding) = self.attributes.get(&attribute.variable_name) { 49 | if attribute.kind != program_binding.kind.as_sized_type() { 50 | log::warn!( 51 | "The variable {} has type {:?} as an attribute, \ 52 | but {:?} in the program definition.", 53 | attribute.variable_name, 54 | attribute.kind, 55 | program_binding.kind 56 | ); 57 | } 58 | 59 | bindings.push(BufferBinding { 60 | kind: attribute.kind, 61 | divisor, 62 | location: program_binding.location as _, 63 | normalized: false, 64 | offset, 65 | stride, 66 | }); 67 | 68 | offset += attribute.kind.byte_size(); 69 | } else { 70 | log::warn!( 71 | "Attribute has variable {}, which isn't used in the program.", 72 | attribute.variable_name 73 | ); 74 | } 75 | } 76 | 77 | if bindings.is_empty() { 78 | log::warn!("No attributes in the buffer overlapped with the program."); 79 | } else { 80 | self.bindings.insert(buffer, bindings); 81 | } 82 | } 83 | } 84 | } 85 | 86 | impl Renderer { 87 | pub fn new(gl: WebGl2RenderingContext) -> Self { 88 | let gpu = ShadowGpu::new(gl); 89 | Renderer { gpu } 90 | } 91 | 92 | fn render_impl( 93 | &mut self, 94 | draw_call: DrawCall, 95 | program: &mut impl ProgramLike, 96 | 97 | buffers: BTreeMap>, 98 | ) -> Result<()> { 99 | let bound_program = program.get_program(&self.gpu)?; 100 | 101 | let mut uniforms = HashMap::new(); 102 | for (uniform_handle, uniform) in &bound_program.uniforms { 103 | uniforms.insert(uniform_handle.clone(), uniform.get_value()); 104 | } 105 | 106 | let state: GpuState = GpuState { 107 | program: Some(bound_program.handle()), 108 | buffers, 109 | uniforms, 110 | globals: program.globals(), 111 | }; 112 | 113 | match draw_call { 114 | DrawCall::DrawArrays { count, first } => { 115 | self.gpu 116 | .draw_arrays(&state, program.draw_mode(), first as _, count as _)? 117 | } 118 | DrawCall::DrawArraysInstanced { 119 | first, 120 | count, 121 | instances, 122 | } => self.gpu.draw_arrays_instanced( 123 | &state, 124 | program.draw_mode(), 125 | first as _, 126 | count as _, 127 | instances as _, 128 | )?, 129 | } 130 | 131 | Ok(()) 132 | } 133 | 134 | pub fn render( 135 | &mut self, 136 | program: &mut impl ProgramLike, 137 | vertex_buffer: &impl BufferLike, 138 | ) -> Result<()> { 139 | let bound_program = program.get_program(&self.gpu)?; 140 | let program_attributes = bound_program.attributes(); 141 | 142 | let mut bg = BufferBindingGroup::new(program_attributes.clone()); 143 | bg.add_buffer(vertex_buffer, 0); 144 | 145 | self.render_impl( 146 | DrawCall::DrawArrays { 147 | first: 0, 148 | count: vertex_buffer.len(), 149 | }, 150 | program, 151 | bg.bindings, 152 | ) 153 | } 154 | 155 | pub fn render_instanced( 156 | &mut self, 157 | program: &mut impl ProgramLike, 158 | vertex_buffer: &impl BufferLike, 159 | instance_buffer: &impl BufferLike, 160 | ) -> Result<()> { 161 | let bound_program = program.get_program(&self.gpu)?; 162 | let program_attributes = bound_program.attributes(); 163 | 164 | let mut bg = BufferBindingGroup::new(program_attributes.clone()); 165 | bg.add_buffer(vertex_buffer, 0); 166 | bg.add_buffer(instance_buffer, 1); 167 | 168 | self.render_impl( 169 | DrawCall::DrawArraysInstanced { 170 | first: 0, 171 | count: vertex_buffer.len(), 172 | instances: instance_buffer.len(), 173 | }, 174 | program, 175 | bg.bindings, 176 | ) 177 | } 178 | } 179 | 180 | pub trait Drawable { 181 | fn draw(&mut self, renderer: &mut Renderer) -> Result<()>; 182 | } 183 | -------------------------------------------------------------------------------- /limelight/src/shadow_gpu/buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::webgl::buffer::{BufferBindPoint, BufferUsageHint}; 2 | use anyhow::{anyhow, Result}; 3 | use bytemuck::Pod; 4 | use std::{cell::RefCell, hash::Hash, rc::Rc}; 5 | use web_sys::{WebGl2RenderingContext, WebGlBuffer}; 6 | 7 | pub enum BindResult { 8 | BoundExisting, 9 | BoundNew, 10 | } 11 | 12 | struct BufferGlObjects { 13 | buffer: WebGlBuffer, 14 | capacity: usize, 15 | } 16 | 17 | trait AsBytes { 18 | fn as_bytes(&self) -> &[u8]; 19 | 20 | fn byte_len(&self) -> usize; 21 | } 22 | 23 | impl AsBytes for Vec { 24 | fn as_bytes(&self) -> &[u8] { 25 | bytemuck::cast_slice(self) 26 | } 27 | 28 | fn byte_len(&self) -> usize { 29 | self.len() * std::mem::size_of::() / std::mem::size_of::() 30 | } 31 | } 32 | 33 | struct DataWithMarker { 34 | data: Box, 35 | length: usize, 36 | dirty: bool, 37 | } 38 | 39 | impl Default for DataWithMarker { 40 | fn default() -> Self { 41 | DataWithMarker { 42 | data: Box::new(Vec::::new()), 43 | length: 0, 44 | dirty: true, 45 | } 46 | } 47 | } 48 | 49 | pub struct BufferHandleInner { 50 | gl_objects: RefCell>, 51 | data: RefCell, 52 | usage_hint: BufferUsageHint, 53 | } 54 | 55 | #[derive(Clone)] 56 | pub struct BufferHandle(Rc); 57 | 58 | impl PartialOrd for BufferHandle { 59 | fn partial_cmp(&self, other: &Self) -> Option { 60 | Some(self.cmp(other)) 61 | } 62 | } 63 | 64 | impl Ord for BufferHandle { 65 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 66 | let s = self.0.as_ref() as *const BufferHandleInner; 67 | let o = other.0.as_ref() as *const BufferHandleInner; 68 | 69 | s.cmp(&o) 70 | } 71 | } 72 | 73 | impl Hash for BufferHandle { 74 | fn hash(&self, state: &mut H) { 75 | // TODO: is Pin needed for correctness? 76 | (self.0.as_ref() as *const BufferHandleInner).hash(state) 77 | } 78 | } 79 | 80 | impl PartialEq for BufferHandle { 81 | fn eq(&self, other: &Self) -> bool { 82 | Rc::ptr_eq(&self.0, &other.0) 83 | } 84 | } 85 | 86 | impl Eq for BufferHandle {} 87 | 88 | impl BufferHandle { 89 | fn new_impl(usage_hint: BufferUsageHint) -> BufferHandle { 90 | BufferHandle(Rc::new(BufferHandleInner { 91 | gl_objects: RefCell::new(None), 92 | data: RefCell::new(DataWithMarker::default()), 93 | usage_hint, 94 | })) 95 | } 96 | 97 | pub fn new(usage_hint: BufferUsageHint) -> BufferHandle { 98 | Self::new_impl(usage_hint) 99 | } 100 | 101 | pub fn set_data(&self, data: Vec) { 102 | *self.0.data.borrow_mut() = DataWithMarker { 103 | length: data.len(), 104 | data: Box::new(data), 105 | dirty: true, 106 | }; 107 | } 108 | 109 | pub fn len(&self) -> usize { 110 | self.0.data.borrow().length 111 | } 112 | 113 | pub fn is_empty(&self) -> bool { 114 | self.0.data.borrow().length == 0 115 | } 116 | 117 | fn create( 118 | gl: &WebGl2RenderingContext, 119 | data: &[u8], 120 | usage_hint: BufferUsageHint, 121 | ) -> Result { 122 | let buffer = gl 123 | .create_buffer() 124 | .ok_or_else(|| anyhow!("Couldn't create buffer."))?; 125 | 126 | gl.bind_buffer(BufferBindPoint::ArrayBuffer as _, Some(&buffer)); 127 | gl.buffer_data_with_u8_array(BufferBindPoint::ArrayBuffer as _, data, usage_hint as _); 128 | 129 | Ok(BufferGlObjects { 130 | buffer, 131 | capacity: data.len(), 132 | }) 133 | } 134 | 135 | pub fn bind(&self, gl: &WebGl2RenderingContext) -> Result { 136 | let inner = &self.0; 137 | 138 | // The buffer handle has local data, so we need to write it. 139 | let mut gl_objects = inner.gl_objects.borrow_mut(); 140 | let mut data = inner.data.borrow_mut(); 141 | let dirty = data.dirty; 142 | data.dirty = false; 143 | 144 | if let Some(gl_objects) = &mut *gl_objects { 145 | if dirty { 146 | if gl_objects.capacity >= data.data.byte_len() { 147 | gl.bind_buffer(BufferBindPoint::ArrayBuffer as _, Some(&gl_objects.buffer)); 148 | gl.buffer_sub_data_with_i32_and_u8_array( 149 | BufferBindPoint::ArrayBuffer as _, 150 | 0, 151 | data.data.as_bytes(), 152 | ); 153 | Ok(BindResult::BoundExisting) 154 | } else { 155 | // The current buffer isn't big enough, need to discard it and create a new one. 156 | log::info!( 157 | "The old buffer could fit {} bytes, but {} are needed; recreating.", 158 | gl_objects.capacity, 159 | data.data.byte_len() 160 | ); 161 | gl.delete_buffer(Some(&gl_objects.buffer)); 162 | 163 | *gl_objects = Self::create(gl, data.data.as_bytes(), inner.usage_hint)?; 164 | Ok(BindResult::BoundNew) 165 | } 166 | } else { 167 | gl.bind_buffer(BufferBindPoint::ArrayBuffer as _, Some(&gl_objects.buffer)); 168 | Ok(BindResult::BoundExisting) 169 | } 170 | } else { 171 | // We have not created this buffer before. 172 | log::info!( 173 | "Buffer used for the first time, creating with {} bytes.", 174 | data.data.byte_len() 175 | ); 176 | *gl_objects = Some(Self::create(gl, data.data.as_bytes(), inner.usage_hint)?); 177 | 178 | Ok(BindResult::BoundNew) 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /limelight/src/shadow_gpu/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::buffer::BufferHandle; 2 | pub use self::state::BufferBinding; 3 | use self::vao::VaoHandle; 4 | pub use self::{program::ProgramHandle, state::GpuState}; 5 | use crate::webgl::buffer::BufferUsageHint; 6 | use crate::webgl::types::GlSizedDataType; 7 | use crate::DrawMode; 8 | use anyhow::{anyhow, Result}; 9 | use std::collections::BTreeMap; 10 | use std::{collections::HashMap, rc::Rc}; 11 | pub use uniforms::{UniformHandle, UniformValue, UniformValueType}; 12 | use web_sys::{WebGl2RenderingContext, WebGlShader}; 13 | 14 | mod buffer; 15 | mod program; 16 | mod state; 17 | mod uniforms; 18 | mod vao; 19 | 20 | pub trait GpuBind { 21 | fn gpu_bind(&self, gl: &WebGl2RenderingContext) -> Result<()>; 22 | } 23 | 24 | pub struct FragmentShader(WebGlShader); 25 | pub struct VertexShader(WebGlShader); 26 | 27 | #[derive(Clone, Debug)] 28 | pub struct AttributeInfo { 29 | pub location: usize, 30 | pub kind: GlSizedDataType, 31 | } 32 | 33 | pub struct ShadowGpu { 34 | gl: WebGl2RenderingContext, 35 | state: GpuState, 36 | vaos: HashMap>, VaoHandle>, 37 | } 38 | 39 | impl ShadowGpu { 40 | pub fn new(gl: WebGl2RenderingContext) -> Self { 41 | ShadowGpu { 42 | gl, 43 | state: GpuState::default(), 44 | vaos: HashMap::default(), 45 | } 46 | } 47 | 48 | pub fn draw_arrays( 49 | &mut self, 50 | state: &GpuState, 51 | mode: DrawMode, 52 | first: i32, 53 | count: i32, 54 | ) -> Result<()> { 55 | self.set_state(state)?; 56 | self.gl.draw_arrays(mode as _, first, count); 57 | Ok(()) 58 | } 59 | 60 | pub fn draw_arrays_instanced( 61 | &mut self, 62 | state: &GpuState, 63 | mode: DrawMode, 64 | first: i32, 65 | count: i32, 66 | instance_count: i32, 67 | ) -> Result<()> { 68 | self.set_state(state)?; 69 | self.gl 70 | .draw_arrays_instanced(mode as _, first, count, instance_count); 71 | Ok(()) 72 | } 73 | 74 | pub fn get_uniform_handle(&self, program: &ProgramHandle, name: &str) -> Result { 75 | let location = self 76 | .gl 77 | .get_uniform_location(&program.program, name) 78 | .ok_or_else(|| anyhow!("Uniform {} not found.", name))?; 79 | 80 | Ok(UniformHandle::new(location)) 81 | } 82 | 83 | fn set_state(&mut self, new_state: &GpuState) -> Result<()> { 84 | // Program 85 | if self.state.program != new_state.program { 86 | new_state.program.gpu_bind(&self.gl)?; 87 | self.state.program = new_state.program.clone(); 88 | } 89 | 90 | // Globals 91 | if self.state.globals.blend_func != new_state.globals.blend_func { 92 | new_state.globals.blend_func.gpu_bind(&self.gl)?; 93 | self.state.globals.blend_func = new_state.globals.blend_func.clone(); 94 | } 95 | 96 | let vao = if let Some(vao) = self.vaos.get_mut(&new_state.buffers) { 97 | vao 98 | } else { 99 | // Create VAO with bindings. 100 | let vao = VaoHandle { 101 | buffers: new_state.buffers.clone(), 102 | vao: None, 103 | }; 104 | self.vaos.insert(new_state.buffers.clone(), vao); 105 | self.vaos.get_mut(&new_state.buffers).unwrap() 106 | }; 107 | 108 | vao.gpu_bind(&self.gl)?; 109 | 110 | // Uniforms 111 | for (location, value) in &new_state.uniforms { 112 | if let Some(v) = self.state.uniforms.get(location) { 113 | if v == value { 114 | continue; 115 | } 116 | } 117 | 118 | self.state.uniforms.insert(location.clone(), *value); 119 | value.bind(&self.gl, location); 120 | } 121 | 122 | Ok(()) 123 | } 124 | 125 | pub fn create_buffer(&mut self, usage_hint: BufferUsageHint) -> BufferHandle { 126 | BufferHandle::new(usage_hint) 127 | } 128 | 129 | pub fn link_program( 130 | &self, 131 | frag_shader: &FragmentShader, 132 | vertex_shader: &VertexShader, 133 | ) -> Result { 134 | let gl_program = self 135 | .gl 136 | .create_program() 137 | .ok_or_else(|| anyhow!("Error creating program."))?; 138 | 139 | self.gl.attach_shader(&gl_program, &frag_shader.0); 140 | self.gl.attach_shader(&gl_program, &vertex_shader.0); 141 | self.gl.link_program(&gl_program); 142 | 143 | let active_attributes = self 144 | .gl 145 | .get_program_parameter(&gl_program, WebGl2RenderingContext::ACTIVE_ATTRIBUTES) 146 | .as_f64() 147 | .expect("ACTIVE_ATTRIBUTES should be numeric.") as u32; 148 | let mut attributes = HashMap::new(); 149 | for i in 0..active_attributes { 150 | let attr_info = self 151 | .gl 152 | .get_active_attrib(&gl_program, i) 153 | .expect("Expected attribute info."); 154 | let attribute_name = attr_info.name(); 155 | let attribute_type = attr_info.type_(); 156 | let attribute_size = attr_info.size(); 157 | if attribute_size > 1 { 158 | panic!("Attribute size for {} is {}. Attribute sizes greater than 1 are not yet implemented.", attribute_name, attribute_size); 159 | } 160 | let location = self.gl.get_attrib_location(&gl_program, &attribute_name) as _; 161 | 162 | let kind = GlSizedDataType::try_from(attribute_type)?; 163 | 164 | attributes.insert(attribute_name, AttributeInfo { location, kind }); 165 | } 166 | 167 | if !self 168 | .gl 169 | .get_program_parameter(&gl_program, WebGl2RenderingContext::LINK_STATUS) 170 | { 171 | if let Some(info) = self.gl.get_program_info_log(&gl_program) { 172 | return Err(anyhow!("Encountered error linking program: {}", info)); 173 | } else { 174 | return Err(anyhow!("Encountered unknown error linking program.")); 175 | } 176 | } 177 | 178 | Ok(ProgramHandle { 179 | program: Rc::new(gl_program), 180 | attributes, 181 | }) 182 | } 183 | 184 | fn compile_shader(&self, shader_type: ShaderType, source: &str) -> Result { 185 | let shader = self 186 | .gl 187 | .create_shader(shader_type as _) 188 | .ok_or_else(|| anyhow!("Could not create shader."))?; 189 | self.gl.shader_source(&shader, source); 190 | self.gl.compile_shader(&shader); 191 | 192 | if self 193 | .gl 194 | .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS) 195 | .as_bool() 196 | .unwrap_or(false) 197 | { 198 | Ok(shader) 199 | } else { 200 | Err(self 201 | .gl 202 | .get_shader_info_log(&shader) 203 | .map(|d| anyhow!("Error compiling shader {}", d)) 204 | .unwrap_or_else(|| anyhow!("Unknown error compiling shader."))) 205 | } 206 | } 207 | 208 | pub fn compile_fragment_shader(&self, source: &str) -> Result { 209 | Ok(FragmentShader( 210 | self.compile_shader(ShaderType::FragmentShader, source)?, 211 | )) 212 | } 213 | 214 | pub fn compile_vertex_shader(&self, source: &str) -> Result { 215 | Ok(VertexShader( 216 | self.compile_shader(ShaderType::VertexShader, source)?, 217 | )) 218 | } 219 | 220 | pub fn get_error(&self) -> Result<()> { 221 | let error = self.gl.get_error(); 222 | if error != WebGl2RenderingContext::NO_ERROR { 223 | Err(anyhow!("WebGL Error: {:?}", error)) 224 | } else { 225 | Ok(()) 226 | } 227 | } 228 | } 229 | 230 | #[derive(Copy, Clone)] 231 | #[repr(u32)] 232 | enum ShaderType { 233 | FragmentShader = 0x8B30, 234 | VertexShader = 0x8B31, 235 | } 236 | -------------------------------------------------------------------------------- /limelight/src/shadow_gpu/program.rs: -------------------------------------------------------------------------------- 1 | use super::{AttributeInfo, GpuBind}; 2 | use anyhow::Result; 3 | use std::borrow::Borrow; 4 | use std::{collections::HashMap, rc::Rc}; 5 | use web_sys::{WebGl2RenderingContext, WebGlProgram}; 6 | 7 | #[derive(Clone)] 8 | pub struct ProgramHandle { 9 | pub program: Rc, 10 | 11 | /// A map from attribute name to attribute location in the program. 12 | pub attributes: HashMap, 13 | } 14 | 15 | impl GpuBind for Option { 16 | fn gpu_bind(&self, gl: &WebGl2RenderingContext) -> Result<()> { 17 | if let Some(ProgramHandle { program, .. }) = &self { 18 | gl.use_program(Some(program.borrow())); 19 | } else { 20 | gl.use_program(None); 21 | } 22 | 23 | Ok(()) 24 | } 25 | } 26 | 27 | impl PartialEq for ProgramHandle { 28 | fn eq(&self, other: &Self) -> bool { 29 | Rc::ptr_eq(&self.program, &other.program) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /limelight/src/shadow_gpu/state.rs: -------------------------------------------------------------------------------- 1 | use crate::{state::StateDescriptor, webgl::types::SizedDataType}; 2 | use std::collections::{BTreeMap, HashMap}; 3 | 4 | use super::{program::ProgramHandle, BufferHandle, UniformHandle, UniformValue}; 5 | 6 | #[derive(Hash, PartialEq, Eq, Clone, Debug)] 7 | pub struct BufferBinding { 8 | // pub name: String, 9 | pub kind: SizedDataType, 10 | pub location: u32, 11 | pub normalized: bool, 12 | pub stride: i32, 13 | pub offset: i32, 14 | pub divisor: u32, 15 | //pub buffer: BufferHandle, 16 | } 17 | 18 | #[derive(Default)] 19 | pub struct GpuState { 20 | pub program: Option, 21 | pub buffers: BTreeMap>, 22 | pub uniforms: HashMap, 23 | pub globals: StateDescriptor, 24 | } 25 | -------------------------------------------------------------------------------- /limelight/src/shadow_gpu/uniforms.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Borrow, hash::Hash, rc::Rc}; 2 | 3 | use slice_of_array::SliceFlatExt; 4 | use web_sys::{WebGl2RenderingContext, WebGlUniformLocation}; 5 | 6 | #[derive(Clone)] 7 | pub struct UniformHandle(Rc); 8 | 9 | impl UniformHandle { 10 | pub fn new(location: WebGlUniformLocation) -> Self { 11 | UniformHandle(Rc::new(location)) 12 | } 13 | } 14 | 15 | impl Hash for UniformHandle { 16 | fn hash(&self, state: &mut H) { 17 | Rc::as_ptr(&self.0).hash(state) 18 | } 19 | } 20 | 21 | impl PartialEq for UniformHandle { 22 | fn eq(&self, other: &Self) -> bool { 23 | Rc::ptr_eq(&self.0, &other.0) 24 | } 25 | } 26 | 27 | impl Eq for UniformHandle {} 28 | 29 | #[derive(Clone, Copy, PartialEq)] 30 | pub enum UniformValue { 31 | Float(f32), 32 | Vec2([f32; 2]), 33 | Vec3([f32; 3]), 34 | Vec4([f32; 4]), 35 | Mat2([[f32; 2]; 2]), 36 | Mat3([[f32; 3]; 3]), 37 | Mat4([[f32; 4]; 4]), 38 | Int(i32), 39 | IntVec2([i32; 2]), 40 | IntVec3([i32; 3]), 41 | IntVec4([i32; 4]), 42 | UnsignedInt(u32), 43 | UnsignedIntVec2([u32; 2]), 44 | UnsignedIntVec3([u32; 3]), 45 | UnsignedIntVec4([u32; 4]), 46 | // TODO: non-square matrices are supported by WebGL2: 47 | // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/uniformMatrix 48 | } 49 | 50 | impl UniformValue { 51 | pub fn bind(&self, gl: &WebGl2RenderingContext, handle: &UniformHandle) { 52 | let location = handle.0.borrow(); 53 | match self { 54 | UniformValue::Float(v) => gl.uniform1f(Some(location), *v), 55 | UniformValue::Vec2(v) => gl.uniform2fv_with_f32_array(Some(location), v), 56 | UniformValue::Vec3(v) => gl.uniform3fv_with_f32_array(Some(location), v), 57 | UniformValue::Vec4(v) => gl.uniform4fv_with_f32_array(Some(location), v), 58 | 59 | UniformValue::Int(v) => gl.uniform1i(Some(location), *v), 60 | UniformValue::IntVec2(v) => gl.uniform2iv_with_i32_array(Some(location), v), 61 | UniformValue::IntVec3(v) => gl.uniform3iv_with_i32_array(Some(location), v), 62 | UniformValue::IntVec4(v) => gl.uniform4iv_with_i32_array(Some(location), v), 63 | 64 | UniformValue::UnsignedInt(v) => gl.uniform1ui(Some(location), *v), 65 | UniformValue::UnsignedIntVec2(v) => gl.uniform2uiv_with_u32_array(Some(location), v), 66 | UniformValue::UnsignedIntVec3(v) => gl.uniform3uiv_with_u32_array(Some(location), v), 67 | UniformValue::UnsignedIntVec4(v) => gl.uniform4uiv_with_u32_array(Some(location), v), 68 | 69 | UniformValue::Mat2(v) => { 70 | gl.uniform_matrix2fv_with_f32_array(Some(location), false, v.flat()) 71 | } 72 | UniformValue::Mat3(v) => { 73 | gl.uniform_matrix3fv_with_f32_array(Some(location), false, v.flat()) 74 | } 75 | UniformValue::Mat4(v) => { 76 | gl.uniform_matrix4fv_with_f32_array(Some(location), false, v.flat()) 77 | } 78 | } 79 | } 80 | } 81 | 82 | pub trait UniformValueType: Clone + 'static { 83 | fn into_uniform_value(v: &Self) -> UniformValue; 84 | } 85 | 86 | impl UniformValueType for f32 { 87 | fn into_uniform_value(v: &f32) -> UniformValue { 88 | UniformValue::Float(*v) 89 | } 90 | } 91 | 92 | impl UniformValueType for [f32; 2] { 93 | fn into_uniform_value(v: &[f32; 2]) -> UniformValue { 94 | UniformValue::Vec2(*v) 95 | } 96 | } 97 | 98 | impl UniformValueType for [f32; 3] { 99 | fn into_uniform_value(v: &[f32; 3]) -> UniformValue { 100 | UniformValue::Vec3(*v) 101 | } 102 | } 103 | 104 | impl UniformValueType for [f32; 4] { 105 | fn into_uniform_value(v: &[f32; 4]) -> UniformValue { 106 | UniformValue::Vec4(*v) 107 | } 108 | } 109 | 110 | impl UniformValueType for [[f32; 2]; 2] { 111 | fn into_uniform_value(v: &[[f32; 2]; 2]) -> UniformValue { 112 | UniformValue::Mat2(*v) 113 | } 114 | } 115 | 116 | impl UniformValueType for [[f32; 3]; 3] { 117 | fn into_uniform_value(v: &[[f32; 3]; 3]) -> UniformValue { 118 | UniformValue::Mat3(*v) 119 | } 120 | } 121 | 122 | impl UniformValueType for [[f32; 4]; 4] { 123 | fn into_uniform_value(v: &[[f32; 4]; 4]) -> UniformValue { 124 | UniformValue::Mat4(*v) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /limelight/src/shadow_gpu/vao.rs: -------------------------------------------------------------------------------- 1 | use crate::webgl::types::DataType; 2 | use std::collections::BTreeMap; 3 | 4 | use super::{buffer::BindResult, state::BufferBinding, BufferHandle}; 5 | use anyhow::anyhow; 6 | use web_sys::WebGlVertexArrayObject; 7 | 8 | pub struct VaoHandle { 9 | pub vao: Option, 10 | pub buffers: BTreeMap>, 11 | } 12 | 13 | impl VaoHandle { 14 | pub fn gpu_bind(&mut self, gl: &web_sys::WebGl2RenderingContext) -> anyhow::Result<()> { 15 | let create = if let Some(vao) = &self.vao { 16 | gl.bind_vertex_array(Some(vao)); 17 | false 18 | } else { 19 | log::info!("Creating Vertex Array."); 20 | let vao = gl 21 | .create_vertex_array() 22 | .ok_or_else(|| anyhow!("Couldn't create vertex array."))?; 23 | gl.bind_vertex_array(Some(&vao)); 24 | self.vao = Some(vao); 25 | true 26 | }; 27 | 28 | for (buffer, bindings) in &self.buffers { 29 | let upsized_buffer = match buffer.bind(gl)? { 30 | BindResult::BoundExisting => false, 31 | BindResult::BoundNew => true, 32 | }; 33 | 34 | if !create && !upsized_buffer { 35 | // If this is not a new VAO, and the buffer already existed, 36 | // we don't need to update the bindings. 37 | continue; 38 | } 39 | log::info!("Updating or creating initial bindings: {:?}", bindings); 40 | 41 | for binding in bindings { 42 | match binding.kind.data_type() { 43 | DataType::Float => gl.vertex_attrib_pointer_with_i32( 44 | binding.location, 45 | binding.kind.size(), 46 | binding.kind.data_type() as _, 47 | binding.normalized, 48 | binding.stride, 49 | binding.offset, 50 | ), 51 | _ => gl.vertex_attrib_i_pointer_with_i32( 52 | binding.location, 53 | binding.kind.size(), 54 | binding.kind.data_type() as _, 55 | binding.stride, 56 | binding.offset, 57 | ), 58 | } 59 | 60 | if binding.divisor != 0 { 61 | gl.vertex_attrib_divisor(binding.location, binding.divisor); 62 | } 63 | 64 | gl.enable_vertex_attrib_array(binding.location); 65 | } 66 | } 67 | 68 | Ok(()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /limelight/src/state/blending.rs: -------------------------------------------------------------------------------- 1 | use web_sys::WebGl2RenderingContext; 2 | 3 | use crate::shadow_gpu::GpuBind; 4 | 5 | #[derive(Copy, Clone, Debug, PartialEq)] 6 | #[repr(u32)] 7 | pub enum BlendingFactorDest { 8 | Zero = 0, 9 | One = 1, 10 | SrcColor = 0x0300, 11 | OneMinusSrcColor = 0x0301, 12 | SrcAlpha = 0x0302, 13 | OneMinusSrcAlpha = 0x0303, 14 | DstAlpha = 0x0304, 15 | OneMinusDstAlpha = 0x0305, 16 | } 17 | 18 | impl Default for BlendingFactorDest { 19 | fn default() -> Self { 20 | BlendingFactorDest::Zero 21 | } 22 | } 23 | 24 | #[derive(Copy, Clone, Debug, PartialEq)] 25 | #[repr(u32)] 26 | pub enum BlendingFactorSrc { 27 | Zero = 0, 28 | One = 1, 29 | DstColor = 0x0306, 30 | OneMinusDstColor = 0x0307, 31 | SrcAlphaSaturate = 0x0308, 32 | SrcAlpha = 0x0302, 33 | OneMinusSrcAlpha = 0x0303, 34 | DstAlpha = 0x0304, 35 | OneMinusDstAlpha = 0x0305, 36 | } 37 | 38 | impl Default for BlendingFactorSrc { 39 | fn default() -> Self { 40 | BlendingFactorSrc::One 41 | } 42 | } 43 | 44 | #[derive(Copy, Clone, Debug, PartialEq)] 45 | #[repr(u32)] 46 | pub enum BlendEquation { 47 | Add = 0x8006, 48 | BlendEquation = 0x8009, 49 | BlendEquationAlpha = 0x883d, 50 | Subtract = 0x800a, 51 | ReverseSubtract = 0x800b, 52 | } 53 | 54 | impl Default for BlendEquation { 55 | fn default() -> Self { 56 | BlendEquation::Add 57 | } 58 | } 59 | 60 | #[derive(Default, Clone, PartialEq)] 61 | pub struct BlendFunction { 62 | pub source_factor: BlendingFactorSrc, 63 | pub dst_factor: BlendingFactorDest, 64 | pub equation: BlendEquation, 65 | } 66 | 67 | impl GpuBind for Option { 68 | fn gpu_bind(&self, gl: &web_sys::WebGl2RenderingContext) -> anyhow::Result<()> { 69 | match self { 70 | Some(blend) => { 71 | gl.blend_func(blend.source_factor as _, blend.dst_factor as _); 72 | gl.blend_equation(blend.equation as _); 73 | gl.enable(WebGl2RenderingContext::BLEND); 74 | } 75 | None => gl.disable(WebGl2RenderingContext::BLEND), 76 | } 77 | 78 | Ok(()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /limelight/src/state/culling.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone)] 2 | #[repr(u32)] 3 | pub enum CullingMode { 4 | Front = 0x0404, 5 | Back = 0x0405, 6 | FrontAndBack = 0x0408, 7 | } 8 | -------------------------------------------------------------------------------- /limelight/src/state/depth.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone)] 2 | #[repr(u32)] 3 | pub enum DepthFunction { 4 | Never = 0x0200, 5 | Less = 0x0201, 6 | Equal = 0x0202, 7 | LessOrEqual = 0x0203, 8 | Greater = 0x0204, 9 | NotEqual = 0x0205, 10 | GreaterOrEqual = 0x0206, 11 | Always = 0x0207, 12 | } 13 | -------------------------------------------------------------------------------- /limelight/src/state/enable.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug)] 2 | #[repr(u32)] 3 | pub enum EnableCap { 4 | CullFace = 0x0B44, 5 | Blend = 0x0BE2, 6 | Dither = 0x0BD0, 7 | StencilTest = 0x0B90, 8 | DepthTest = 0x0B71, 9 | ScissorTest = 0x0C11, 10 | PolygonOffsetFill = 0x8037, 11 | SampleAlphaToCoverage = 0x809E, 12 | SampleCoverage = 0x80A0, 13 | } 14 | -------------------------------------------------------------------------------- /limelight/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | use self::{blending::BlendFunction, culling::CullingMode, depth::DepthFunction}; 2 | 3 | pub mod blending; 4 | pub mod culling; 5 | pub mod depth; 6 | pub mod enable; 7 | 8 | #[derive(Default, Clone)] 9 | pub struct StateDescriptor { 10 | pub blend_func: Option, 11 | pub culling: Option, 12 | pub depth_func: Option, 13 | } 14 | -------------------------------------------------------------------------------- /limelight/src/uniform.rs: -------------------------------------------------------------------------------- 1 | use crate::shadow_gpu::{UniformValue, UniformValueType}; 2 | use std::{cell::RefCell, fmt::Debug, rc::Rc}; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct Uniform { 6 | value: Rc>, 7 | } 8 | 9 | impl Uniform<[[f32; 4]; 4]> { 10 | pub fn identity() -> Uniform<[[f32; 4]; 4]> { 11 | let value = [ 12 | [1., 0., 0., 0.], 13 | [0., 1., 0., 0.], 14 | [0., 0., 1., 0.], 15 | [0., 0., 0., 1.], 16 | ]; 17 | 18 | Uniform::new(value) 19 | } 20 | } 21 | 22 | impl Uniform { 23 | pub fn new(value: T) -> Uniform { 24 | Uniform { 25 | value: Rc::new(RefCell::new(value)), 26 | } 27 | } 28 | 29 | pub fn set_value(&self, value: T) { 30 | *self.value.borrow_mut() = value 31 | } 32 | } 33 | 34 | pub trait GenericUniform { 35 | fn get_value(&self) -> UniformValue; 36 | } 37 | 38 | impl GenericUniform for Uniform { 39 | fn get_value(&self) -> UniformValue { 40 | UniformValueType::into_uniform_value(&*self.value.borrow()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /limelight/src/webgl/buffer.rs: -------------------------------------------------------------------------------- 1 | /// Enum of WebGL Bind Points. 2 | /// 3 | /// Each bind point is a global bind point in WebGL that can have an 4 | /// array bound to it. 5 | #[derive(Clone, Copy)] 6 | #[repr(u32)] 7 | pub enum BufferBindPoint { 8 | ArrayBuffer = 0x8892, 9 | ElementArrayBuffer = 0x8893, 10 | } 11 | 12 | /// Usage hint to tell WebGL how a buffer will be used. 13 | /// 14 | /// These hints are non-binding; you can read/write from a 15 | /// buffer as much as you like regardless of the hint. However, 16 | /// a driver may use the hint to optimize memory layout. 17 | #[derive(Clone, Copy)] 18 | #[repr(u32)] 19 | pub enum BufferUsageHint { 20 | /// Hint that a buffer is written to once and read once. 21 | StreamDraw = 0x88E0, 22 | 23 | /// Hint that a buffer is written to once and ready many times. 24 | StaticDraw = 0x88E4, 25 | 26 | /// Hint that a buffer is written and read many times. 27 | DynamicDraw = 0x88E8, 28 | } 29 | -------------------------------------------------------------------------------- /limelight/src/webgl/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::type_name, 3 | error::Error, 4 | fmt::{Debug, Display}, 5 | marker::PhantomData, 6 | }; 7 | 8 | struct UnexpectedValue(u32, PhantomData); 9 | 10 | impl Display for UnexpectedValue { 11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 12 | write!( 13 | f, 14 | "Unexpected value for type {:?}: {}", 15 | type_name::(), 16 | self.0 17 | ) 18 | } 19 | } 20 | 21 | impl Debug for UnexpectedValue { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | write!(f, "UnexpectedValue<{}>({})", type_name::(), self.0) 24 | } 25 | } 26 | 27 | impl Error for UnexpectedValue {} 28 | -------------------------------------------------------------------------------- /limelight/src/webgl/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod buffer; 2 | pub mod error; 3 | pub mod types; 4 | -------------------------------------------------------------------------------- /limelight/src/webgl/types.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] 4 | #[repr(u32)] 5 | pub enum DataType { 6 | Byte = 0x1400, 7 | UnsignedByte = 0x1401, 8 | Short = 0x1402, 9 | UnsignedShort = 0x1403, 10 | Int = 0x1404, 11 | UnsignedInt = 0x1405, 12 | Float = 0x1406, 13 | } 14 | 15 | impl TryFrom for DataType { 16 | type Error = anyhow::Error; 17 | 18 | fn try_from(value: u32) -> Result { 19 | match value { 20 | 0x1400 => Ok(DataType::Byte), 21 | 0x1401 => Ok(DataType::UnsignedByte), 22 | 0x1402 => Ok(DataType::Short), 23 | 0x1403 => Ok(DataType::UnsignedShort), 24 | 0x1404 => Ok(DataType::Int), 25 | 0x1405 => Ok(DataType::UnsignedInt), 26 | 0x1406 => Ok(DataType::Float), 27 | _ => Err(anyhow!("Invalid DataType.")), 28 | } 29 | } 30 | } 31 | 32 | #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] 33 | pub struct SizedDataType { 34 | data_type: DataType, 35 | size: i32, 36 | } 37 | 38 | impl SizedDataType { 39 | pub fn new(data_type: DataType, size: i32) -> Self { 40 | if !(1..=4).contains(&size) { 41 | panic!("Tried to create SizedDataType with size {} but glsl only supports vec{{2,3,4}} and scalars.", size); 42 | } 43 | 44 | SizedDataType { data_type, size } 45 | } 46 | 47 | pub fn byte_size(&self) -> i32 { 48 | self.data_type.size() * self.size 49 | } 50 | 51 | pub fn size(&self) -> i32 { 52 | self.size 53 | } 54 | 55 | pub fn data_type(&self) -> DataType { 56 | self.data_type 57 | } 58 | } 59 | 60 | impl DataType { 61 | pub fn size(&self) -> i32 { 62 | match self { 63 | DataType::Byte | DataType::UnsignedByte => 1, 64 | DataType::Short | DataType::UnsignedShort => 2, 65 | DataType::Int | DataType::UnsignedInt => 4, 66 | DataType::Float => 4, 67 | } 68 | } 69 | } 70 | 71 | pub trait AsSizedDataType { 72 | fn as_sized_data_type() -> SizedDataType; 73 | } 74 | 75 | impl AsSizedDataType for f32 { 76 | fn as_sized_data_type() -> SizedDataType { 77 | SizedDataType { 78 | data_type: DataType::Float, 79 | size: 1, 80 | } 81 | } 82 | } 83 | 84 | impl AsSizedDataType for [f32; N] { 85 | fn as_sized_data_type() -> SizedDataType { 86 | SizedDataType { 87 | data_type: DataType::Float, 88 | size: N as _, 89 | } 90 | } 91 | } 92 | 93 | impl AsSizedDataType for i32 { 94 | fn as_sized_data_type() -> SizedDataType { 95 | SizedDataType { 96 | data_type: DataType::Int, 97 | size: 1, 98 | } 99 | } 100 | } 101 | 102 | impl AsSizedDataType for [i32; N] { 103 | fn as_sized_data_type() -> SizedDataType { 104 | SizedDataType { 105 | data_type: DataType::Int, 106 | size: N as _, 107 | } 108 | } 109 | } 110 | 111 | impl AsSizedDataType for u32 { 112 | fn as_sized_data_type() -> SizedDataType { 113 | SizedDataType { 114 | data_type: DataType::UnsignedInt, 115 | size: 1, 116 | } 117 | } 118 | } 119 | 120 | impl AsSizedDataType for [u32; N] { 121 | fn as_sized_data_type() -> SizedDataType { 122 | SizedDataType { 123 | data_type: DataType::UnsignedInt, 124 | size: N as _, 125 | } 126 | } 127 | } 128 | 129 | impl AsSizedDataType for i16 { 130 | fn as_sized_data_type() -> SizedDataType { 131 | SizedDataType { 132 | data_type: DataType::Short, 133 | size: 1, 134 | } 135 | } 136 | } 137 | 138 | impl AsSizedDataType for [i16; N] { 139 | fn as_sized_data_type() -> SizedDataType { 140 | SizedDataType { 141 | data_type: DataType::Short, 142 | size: N as _, 143 | } 144 | } 145 | } 146 | 147 | impl AsSizedDataType for u16 { 148 | fn as_sized_data_type() -> SizedDataType { 149 | SizedDataType { 150 | data_type: DataType::UnsignedShort, 151 | size: 1, 152 | } 153 | } 154 | } 155 | 156 | impl AsSizedDataType for [u16; N] { 157 | fn as_sized_data_type() -> SizedDataType { 158 | SizedDataType { 159 | data_type: DataType::UnsignedShort, 160 | size: N as _, 161 | } 162 | } 163 | } 164 | 165 | impl AsSizedDataType for u8 { 166 | fn as_sized_data_type() -> SizedDataType { 167 | SizedDataType { 168 | data_type: DataType::UnsignedByte, 169 | size: 1, 170 | } 171 | } 172 | } 173 | 174 | impl AsSizedDataType for [u8; N] { 175 | fn as_sized_data_type() -> SizedDataType { 176 | SizedDataType { 177 | data_type: DataType::UnsignedByte, 178 | size: N as _, 179 | } 180 | } 181 | } 182 | 183 | impl AsSizedDataType for i8 { 184 | fn as_sized_data_type() -> SizedDataType { 185 | SizedDataType { 186 | data_type: DataType::Byte, 187 | size: 1, 188 | } 189 | } 190 | } 191 | 192 | impl AsSizedDataType for [i8; N] { 193 | fn as_sized_data_type() -> SizedDataType { 194 | SizedDataType { 195 | data_type: DataType::Byte, 196 | size: N as _, 197 | } 198 | } 199 | } 200 | 201 | #[derive(Clone, Copy, Debug)] 202 | #[repr(u32)] 203 | pub enum GlSizedDataType { 204 | Byte = 0x1400, 205 | UnsignedByte = 0x1401, 206 | Short = 0x1402, 207 | UnsignedShort = 0x1403, 208 | Int = 0x1404, 209 | UnsignedInt = 0x1405, 210 | Float = 0x1406, 211 | FloatVec2 = 0x8B50, 212 | FloatVec3 = 0x8B51, 213 | FloatVec4 = 0x8B52, 214 | IntVec2 = 0x8B53, 215 | IntVec3 = 0x8B54, 216 | IntVec4 = 0x8B55, 217 | Bool = 0x8B56, 218 | BoolVec2 = 0x8B57, 219 | BoolVec3 = 0x8B58, 220 | BoolVec4 = 0x8B59, 221 | FloatMat2 = 0x8B5A, 222 | FloatMat3 = 0x8B5B, 223 | FloatMat4 = 0x8B5C, 224 | Sampler2D = 0x8B5E, 225 | SamplerCube = 0x8B60, 226 | } 227 | 228 | impl GlSizedDataType { 229 | pub fn as_sized_type(&self) -> SizedDataType { 230 | match self { 231 | GlSizedDataType::Float => SizedDataType::new(DataType::Float, 1), 232 | GlSizedDataType::FloatVec2 => SizedDataType::new(DataType::Float, 2), 233 | GlSizedDataType::FloatVec3 => SizedDataType::new(DataType::Float, 3), 234 | GlSizedDataType::FloatVec4 => SizedDataType::new(DataType::Float, 4), 235 | GlSizedDataType::IntVec2 => todo!(), 236 | GlSizedDataType::IntVec3 => todo!(), 237 | GlSizedDataType::IntVec4 => todo!(), 238 | GlSizedDataType::Bool => todo!(), 239 | GlSizedDataType::BoolVec2 => todo!(), 240 | GlSizedDataType::BoolVec3 => todo!(), 241 | GlSizedDataType::BoolVec4 => todo!(), 242 | GlSizedDataType::FloatMat2 => todo!(), 243 | GlSizedDataType::FloatMat3 => todo!(), 244 | GlSizedDataType::FloatMat4 => todo!(), 245 | GlSizedDataType::Sampler2D => todo!(), 246 | GlSizedDataType::SamplerCube => todo!(), 247 | GlSizedDataType::Byte => todo!(), 248 | GlSizedDataType::UnsignedByte => todo!(), 249 | GlSizedDataType::Short => todo!(), 250 | GlSizedDataType::UnsignedShort => todo!(), 251 | GlSizedDataType::Int => SizedDataType::new(DataType::Int, 1), 252 | GlSizedDataType::UnsignedInt => SizedDataType::new(DataType::UnsignedInt, 1), 253 | } 254 | } 255 | } 256 | 257 | impl TryFrom for GlSizedDataType { 258 | type Error = anyhow::Error; 259 | 260 | fn try_from(value: u32) -> Result { 261 | match value { 262 | 0x1400 => Ok(GlSizedDataType::Byte), 263 | 0x1401 => Ok(GlSizedDataType::UnsignedByte), 264 | 0x1402 => Ok(GlSizedDataType::Short), 265 | 0x1403 => Ok(GlSizedDataType::UnsignedShort), 266 | 0x1404 => Ok(GlSizedDataType::Int), 267 | 0x1405 => Ok(GlSizedDataType::UnsignedInt), 268 | 0x1406 => Ok(GlSizedDataType::Float), 269 | 0x8B50 => Ok(GlSizedDataType::FloatVec2), 270 | 0x8B51 => Ok(GlSizedDataType::FloatVec3), 271 | 0x8B52 => Ok(GlSizedDataType::FloatVec4), 272 | 0x8B53 => Ok(GlSizedDataType::IntVec2), 273 | 0x8B54 => Ok(GlSizedDataType::IntVec3), 274 | 0x8B55 => Ok(GlSizedDataType::IntVec4), 275 | 0x8B56 => Ok(GlSizedDataType::Bool), 276 | 0x8B57 => Ok(GlSizedDataType::BoolVec2), 277 | 0x8B58 => Ok(GlSizedDataType::BoolVec3), 278 | 0x8B59 => Ok(GlSizedDataType::BoolVec4), 279 | 0x8B5A => Ok(GlSizedDataType::FloatMat2), 280 | 0x8B5B => Ok(GlSizedDataType::FloatMat3), 281 | 0x8B5C => Ok(GlSizedDataType::FloatMat4), 282 | 0x8B5E => Ok(GlSizedDataType::Sampler2D), 283 | 0x8B60 => Ok(GlSizedDataType::SamplerCube), 284 | _ => Err(anyhow::anyhow!("Unexpected uniform type: {}", value)), 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /limelight/tests/test_vertex_attribute_derive.rs: -------------------------------------------------------------------------------- 1 | use limelight::webgl::types::{DataType, SizedDataType}; 2 | use limelight::{attribute, Attribute, AttributeBinding}; 3 | 4 | #[attribute] 5 | struct SimpleStruct { 6 | data: i32, 7 | } 8 | 9 | #[test] 10 | fn test_describe_simple_derive() { 11 | assert_eq!( 12 | vec![AttributeBinding { 13 | variable_name: "data".to_string(), 14 | kind: SizedDataType::new(DataType::Int, 1) 15 | }], 16 | SimpleStruct::describe() 17 | ); 18 | } 19 | 20 | #[attribute] 21 | struct TwoFieldStruct { 22 | data1: f32, 23 | data2: u16, 24 | data3: i16, 25 | } 26 | 27 | #[test] 28 | fn test_describe_two_field_derive() { 29 | assert_eq!( 30 | vec![ 31 | AttributeBinding::new("data1", DataType::Float, 1), 32 | AttributeBinding::new("data2", DataType::UnsignedShort, 1), 33 | AttributeBinding::new("data3", DataType::Short, 1), 34 | ], 35 | TwoFieldStruct::describe() 36 | ); 37 | } 38 | 39 | #[attribute] 40 | struct ArrayStruct { 41 | array: [f32; 4], 42 | } 43 | 44 | #[test] 45 | fn test_describe_array_field_derive() { 46 | assert_eq!( 47 | vec![AttributeBinding::new("array", DataType::Float, 4)], 48 | ArrayStruct::describe() 49 | ); 50 | } 51 | 52 | #[attribute] 53 | struct MultipleArrayStruct { 54 | a1: [u16; 4], 55 | b1: [i16; 4], 56 | c1: u8, 57 | d1: [i8; 3], 58 | e1: [u16; 2], 59 | } 60 | 61 | #[test] 62 | fn test_describe_multiple_array_field_derive() { 63 | assert_eq!( 64 | vec![ 65 | AttributeBinding::new("a1", DataType::UnsignedShort, 4), 66 | AttributeBinding::new("b1", DataType::Short, 4), 67 | AttributeBinding::new("c1", DataType::UnsignedByte, 1), 68 | AttributeBinding::new("d1", DataType::Byte, 3), 69 | AttributeBinding::new("e1", DataType::UnsignedShort, 2), 70 | ], 71 | MultipleArrayStruct::describe() 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /primitives/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-primitives" 3 | version = "0.1.3" 4 | edition = "2021" 5 | repository = "https://github.com/drifting-in-space/limelight" 6 | description = "2D shape primatives implemented with limelight." 7 | license = "MIT" 8 | readme = "README.md" 9 | keywords = ["webgl"] 10 | 11 | [dependencies] 12 | limelight = {version="0.1.3", path="../limelight"} 13 | bytemuck = "1.7.2" 14 | palette = "0.6.0" 15 | anyhow = "1.0.51" 16 | -------------------------------------------------------------------------------- /primitives/README.md: -------------------------------------------------------------------------------- 1 | # `limelight-primitives` 2 | 3 | This crate implements a number of 2D shapes that can be drawn with a 4 | limelight [`Renderer`](https://docs.rs/limelight/latest/limelight/renderer/struct.Renderer.html). 5 | 6 | Each primitive comes with two parts: a data structure (like `Rect` or `Circle`) representing 7 | the raw shape data that is sent to the GPU, and a layer (like `RectLayer` and `CircleLayer`) 8 | that implements [`Drawable`](https://docs.rs/limelight/latest/limelight/renderer/trait.Drawable.html) 9 | and can draw itself when passed a `Renderer` instance. 10 | 11 | All layers are capable of drawing multiple instances of the shape they represent. 12 | 13 | ```rust 14 | use anyhow::Result; 15 | use limelight_primitives::{Circle, CircleLayer}; 16 | use limelight::Renderer; 17 | use limelight::renderer::Drawable; 18 | 19 | fn draw_circles(renderer: &mut Renderer) -> Result<()> { 20 | let mut circles = CircleLayer::new(); 21 | circles.buffer().set_data(vec![ 22 | Circle { 23 | position: [0., 0.25], 24 | radius: 0.2, 25 | color: palette::named::WHITE.into(), 26 | }, 27 | Circle { 28 | position: [0., 0.25], 29 | radius: 0.1, 30 | color: palette::named::ORANGERED.into(), 31 | }, 32 | ]); 33 | 34 | circles.draw(renderer)?; 35 | Ok(()) 36 | } 37 | ``` 38 | 39 | The `.buffer()` method returns a [`Buffer`](https://docs.rs/limelight/latest/limelight/buffer/struct.Buffer.html) of the relevant type, e.g. `RectLayer::buffer()` returns a `Buffer`, which you 40 | can use to update the rectangle data at any time. 41 | 42 | Layers also expose a `Uniform<[[f32; 4]; 4]>` that acts as a [transformation matrix](https://en.wikipedia.org/wiki/Transformation_matrix) on the points. 43 | 44 | For an example that uses uniforms, see the [primitive scene demo](https://drifting-in-space.github.io/limelight/primitive-scene/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/primitive-scene)). 45 | 46 | ## Primitives 47 | 48 | - `Circle`: filled circles. 49 | - `Rect`: filled rectangle. 50 | - `Line`: straight line of arbitrary (scaled) thickness. 51 | - `Hairline`: axis-aligned line with unscaled thickness (i.e. thickness is independent of zoom level; useful for grids and axes). 52 | -------------------------------------------------------------------------------- /primitives/src/circle/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{color::Color, common::{RelativePosition, identity_quad}}; 2 | use anyhow::Result; 3 | use limelight::{ 4 | attribute, 5 | renderer::Drawable, 6 | state::{ 7 | blending::{BlendFunction, BlendingFactorDest, BlendingFactorSrc}, 8 | StateDescriptor, 9 | }, 10 | Buffer, BufferUsageHint, DrawMode, Program, Uniform, 11 | }; 12 | 13 | #[attribute] 14 | pub struct Circle { 15 | pub position: [f32; 2], 16 | pub radius: f32, 17 | pub color: Color, 18 | } 19 | 20 | pub struct CircleLayer { 21 | circles: Buffer, 22 | positions: Buffer, 23 | program: Program, 24 | transform: Uniform<[[f32; 4]; 4]>, 25 | } 26 | 27 | impl Default for CircleLayer { 28 | fn default() -> Self { 29 | CircleLayer::new() 30 | } 31 | } 32 | 33 | impl CircleLayer { 34 | pub fn new() -> Self { 35 | Self::new_transform(Uniform::identity()) 36 | } 37 | 38 | pub fn new_transform(transform: Uniform<[[f32; 4]; 4]>) -> Self { 39 | let program = Program::new( 40 | include_str!("shader.vert"), 41 | include_str!("shader.frag"), 42 | DrawMode::TriangleStrip, 43 | ) 44 | .with_state(StateDescriptor { 45 | blend_func: Some(BlendFunction { 46 | source_factor: BlendingFactorSrc::One, 47 | dst_factor: BlendingFactorDest::OneMinusSrcAlpha, 48 | ..Default::default() 49 | }), 50 | ..Default::default() 51 | }) 52 | .with_uniform("u_transform", transform.clone()); 53 | 54 | CircleLayer { 55 | circles: Buffer::new_empty(BufferUsageHint::DynamicDraw), 56 | positions: Buffer::new(identity_quad(), BufferUsageHint::StaticDraw), 57 | program, 58 | transform, 59 | } 60 | } 61 | 62 | pub fn buffer(&self) -> Buffer { 63 | self.circles.clone() 64 | } 65 | 66 | pub fn transform(&self) -> Uniform<[[f32; 4]; 4]> { 67 | self.transform.clone() 68 | } 69 | } 70 | 71 | impl Drawable for CircleLayer { 72 | fn draw(&mut self, renderer: &mut limelight::Renderer) -> Result<()> { 73 | renderer.render_instanced(&mut self.program, &self.positions, &self.circles)?; 74 | 75 | Ok(()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /primitives/src/circle/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | flat in uint v_color; 6 | in vec2 v_coord; 7 | 8 | out vec4 f_color; 9 | 10 | void main() { 11 | float r = dot(v_coord, v_coord); 12 | float delta = fwidth(r); 13 | 14 | float alpha = 1.0 - smoothstep(1.0 - delta*2., 1.0, r); 15 | 16 | if (alpha < 0.01) { 17 | discard; 18 | } 19 | 20 | f_color = vec4( 21 | float((v_color & 0x000000FFu)) / 255., 22 | float((v_color & 0x0000FF00u) >> 8) / 255., 23 | float((v_color & 0x00FF0000u) >> 16) / 255., 24 | float((v_color & 0xFF000000u) >> 24) / 255.) * alpha; 25 | } -------------------------------------------------------------------------------- /primitives/src/circle/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 position; 4 | in uint color; 5 | in float radius; 6 | in vec2 relative_position; 7 | 8 | flat out uint v_color; 9 | out vec2 v_edge; 10 | out vec2 v_coord; 11 | 12 | uniform mat4 u_transform; 13 | 14 | void main() { 15 | // switch (gl_VertexID) { 16 | // case 0: 17 | // gl_Position = vec4(position.x - radius, position.y - radius, 0., 1.); 18 | // v_coord = vec2(-1., -1.); 19 | // break; 20 | // case 1: 21 | // gl_Position = vec4(position.x + radius, position.y - radius, 0., 1.); 22 | // v_coord = vec2(1., -1.); 23 | // break; 24 | // case 2: 25 | // gl_Position = vec4(position.x - radius, position.y + radius, 0., 1.); 26 | // v_coord = vec2(-1., 1.); 27 | // break; 28 | // case 3: 29 | // gl_Position = vec4(position.x + radius, position.y + radius, 0., 1.); 30 | // v_coord = vec2(1., 1.); 31 | // } 32 | 33 | v_coord = relative_position; 34 | gl_Position = vec4(position + radius * relative_position, 0., 1.) * u_transform; 35 | 36 | v_color = color; 37 | } 38 | -------------------------------------------------------------------------------- /primitives/src/color.rs: -------------------------------------------------------------------------------- 1 | use limelight::{ 2 | webgl::types::{DataType, SizedDataType}, 3 | AsSizedDataType, 4 | }; 5 | use palette::{Srgb, Srgba}; 6 | 7 | #[repr(C)] 8 | #[derive(Copy, Clone, Debug, bytemuck::Zeroable, bytemuck::Pod)] 9 | pub struct Color(pub u32); 10 | 11 | impl Color { 12 | pub fn opacity(&self, opacity: f32) -> Self { 13 | let opacity = ((255. * opacity) as u32).min(255); 14 | Color(self.0 & (0x00ffffff + (opacity << 24))) 15 | } 16 | } 17 | 18 | impl AsSizedDataType for Color { 19 | fn as_sized_data_type() -> SizedDataType { 20 | SizedDataType::new(DataType::UnsignedInt, 1) 21 | } 22 | } 23 | 24 | impl From> for Color { 25 | fn from(srgb: Srgb) -> Self { 26 | Color(*bytemuck::from_bytes(&[ 27 | srgb.red, srgb.green, srgb.blue, 0xff, 28 | ])) 29 | } 30 | } 31 | 32 | impl From> for Color { 33 | fn from(srgba: Srgba) -> Self { 34 | Color(*bytemuck::from_bytes(&[ 35 | srgba.red, 36 | srgba.green, 37 | srgba.blue, 38 | srgba.alpha, 39 | ])) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /primitives/src/common.rs: -------------------------------------------------------------------------------- 1 | use limelight::attribute; 2 | 3 | #[attribute] 4 | pub struct RelativePosition { 5 | relative_position: [f32; 2], 6 | } 7 | 8 | pub fn identity_quad() -> Vec { 9 | vec![ 10 | RelativePosition { 11 | relative_position: [-1., 1.], 12 | }, 13 | RelativePosition { 14 | relative_position: [1., 1.], 15 | }, 16 | RelativePosition { 17 | relative_position: [-1., -1.], 18 | }, 19 | RelativePosition { 20 | relative_position: [1., -1.], 21 | }, 22 | ] 23 | } 24 | 25 | #[attribute] 26 | pub struct RectPosition { 27 | rect_position: [f32; 2], 28 | } 29 | 30 | pub fn identity_rect() -> Vec { 31 | vec![ 32 | RectPosition { 33 | rect_position: [0., 0.] 34 | }, 35 | RectPosition { 36 | rect_position: [0., 1.] 37 | }, 38 | RectPosition { 39 | rect_position: [1., 0.] 40 | }, 41 | RectPosition { 42 | rect_position: [1., 1.] 43 | }, 44 | ] 45 | } 46 | 47 | 48 | #[attribute] 49 | pub struct LinePosition { 50 | // position along line (0=start, 1=end); position perpendicular to line 51 | line_position: [f32; 2], 52 | line_edge: [f32; 2], 53 | } 54 | 55 | pub fn identity_line() -> Vec { 56 | vec![ 57 | LinePosition { 58 | line_position: [0., -1.], 59 | line_edge: [0., 0.] 60 | }, 61 | LinePosition { 62 | line_position: [0., 1.], 63 | line_edge: [1., 0.] 64 | }, 65 | LinePosition { 66 | line_position: [1., -1.], 67 | line_edge: [0., 1.] 68 | }, 69 | LinePosition { 70 | line_position: [1., 1.], 71 | line_edge: [0., 0.] 72 | }, 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /primitives/src/hairline/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{color::Color, common::{RelativePosition, identity_quad}}; 2 | use anyhow::Result; 3 | use limelight::{ 4 | attribute, 5 | renderer::Drawable, 6 | state::{ 7 | blending::{BlendFunction, BlendingFactorDest, BlendingFactorSrc}, 8 | StateDescriptor, 9 | }, 10 | webgl::types::{DataType, SizedDataType}, 11 | AsSizedDataType, Buffer, BufferUsageHint, DrawMode, Program, Uniform, 12 | }; 13 | 14 | #[repr(u32)] 15 | #[derive(Clone, Copy)] 16 | pub enum Orientation { 17 | Horizontal = 0x0, 18 | Vertical = 0x1, 19 | } 20 | 21 | unsafe impl bytemuck::Pod for Orientation {} 22 | unsafe impl bytemuck::Zeroable for Orientation {} 23 | 24 | impl AsSizedDataType for Orientation { 25 | fn as_sized_data_type() -> SizedDataType { 26 | SizedDataType::new(DataType::UnsignedInt, 1) 27 | } 28 | } 29 | 30 | #[attribute] 31 | pub struct Hairline { 32 | pub location: f32, 33 | pub color: Color, 34 | pub orientation: Orientation, 35 | } 36 | 37 | impl Default for HairlineLayer { 38 | fn default() -> Self { 39 | HairlineLayer::new() 40 | } 41 | } 42 | 43 | pub struct HairlineLayer { 44 | lines: Buffer, 45 | positions: Buffer, 46 | program: Program, 47 | transform: Uniform<[[f32; 4]; 4]>, 48 | } 49 | 50 | impl HairlineLayer { 51 | pub fn new() -> Self { 52 | Self::new_transform(Uniform::identity()) 53 | } 54 | 55 | pub fn new_transform(transform: Uniform<[[f32; 4]; 4]>) -> Self { 56 | let program = Program::new( 57 | include_str!("shader.vert"), 58 | include_str!("shader.frag"), 59 | DrawMode::TriangleStrip, 60 | ) 61 | .with_state(StateDescriptor { 62 | blend_func: Some(BlendFunction { 63 | source_factor: BlendingFactorSrc::One, 64 | dst_factor: BlendingFactorDest::OneMinusSrcAlpha, 65 | ..Default::default() 66 | }), 67 | ..Default::default() 68 | }) 69 | .with_uniform("u_transform", transform.clone()); 70 | 71 | HairlineLayer { 72 | lines: Buffer::new_empty(BufferUsageHint::DynamicDraw), 73 | positions: Buffer::new(identity_quad(), BufferUsageHint::StaticDraw), 74 | program, 75 | transform, 76 | } 77 | } 78 | 79 | pub fn buffer(&self) -> Buffer { 80 | self.lines.clone() 81 | } 82 | 83 | pub fn transform(&self) -> Uniform<[[f32; 4]; 4]> { 84 | self.transform.clone() 85 | } 86 | } 87 | 88 | impl Drawable for HairlineLayer { 89 | fn draw(&mut self, renderer: &mut limelight::Renderer) -> Result<()> { 90 | renderer.render_instanced(&mut self.program, &self.positions, &self.lines)?; 91 | 92 | Ok(()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /primitives/src/hairline/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | flat in uint v_color; 6 | out vec4 f_color; 7 | 8 | void main() { 9 | float alpha = float((v_color & 0xFF000000u) >> 24) / 255.; 10 | f_color = vec4( 11 | alpha * float((v_color & 0x000000FFu)) / 255., 12 | alpha * float((v_color & 0x0000FF00u) >> 8) / 255., 13 | alpha * float((v_color & 0x00FF0000u) >> 16) / 255., 14 | alpha); 15 | } -------------------------------------------------------------------------------- /primitives/src/hairline/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in float location; 4 | in uint orientation; 5 | in uint color; 6 | in vec2 relative_position; 7 | 8 | flat out uint v_color; 9 | uniform mat4 u_transform; 10 | 11 | const float THICKNESS = 0.002; 12 | 13 | void main() { 14 | vec4 scaled = vec4(location, location, 0.0, 1.0) * u_transform; 15 | 16 | if (orientation == 0u) { 17 | /* Horizontal */ 18 | gl_Position = vec4( 19 | relative_position.x, 20 | scaled.y - relative_position.y * THICKNESS, 21 | 0., 22 | 1. 23 | ); 24 | } else { 25 | /* Vertical */ 26 | gl_Position = vec4( 27 | scaled.x - relative_position.x * THICKNESS, 28 | relative_position.y, 29 | 0., 30 | 1. 31 | ); 32 | } 33 | 34 | v_color = color; 35 | } -------------------------------------------------------------------------------- /primitives/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod circle; 4 | mod color; 5 | mod common; 6 | mod hairline; 7 | mod line; 8 | mod rect; 9 | mod line3d; 10 | 11 | pub use circle::{Circle, CircleLayer}; 12 | pub use color::Color; 13 | pub use hairline::{Hairline, HairlineLayer, Orientation}; 14 | pub use line::{Line, LineLayer}; 15 | pub use rect::{Rect, RectLayer}; 16 | pub use line3d::{Line3D, Line3DLayer}; 17 | -------------------------------------------------------------------------------- /primitives/src/line/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{color::Color, common::{LinePosition, identity_line}}; 2 | use anyhow::Result; 3 | use limelight::{ 4 | attribute, 5 | renderer::Drawable, 6 | state::{ 7 | blending::{BlendFunction, BlendingFactorDest, BlendingFactorSrc}, 8 | StateDescriptor, 9 | }, 10 | Buffer, BufferUsageHint, DrawMode, Program, Uniform, 11 | }; 12 | 13 | #[attribute] 14 | pub struct Line { 15 | pub start: [f32; 2], 16 | pub end: [f32; 2], 17 | pub width: f32, 18 | pub color: Color, 19 | } 20 | 21 | pub struct LineLayer { 22 | lines: Buffer, 23 | positions: Buffer, 24 | program: Program, 25 | transform: Uniform<[[f32; 4]; 4]>, 26 | } 27 | 28 | impl Default for LineLayer { 29 | fn default() -> Self { 30 | LineLayer::new() 31 | } 32 | } 33 | 34 | impl LineLayer { 35 | pub fn new() -> Self { 36 | Self::new_transform(Uniform::identity()) 37 | } 38 | 39 | pub fn new_transform(transform: Uniform<[[f32; 4]; 4]>) -> Self { 40 | let program = Program::new( 41 | include_str!("shader.vert"), 42 | include_str!("shader.frag"), 43 | DrawMode::TriangleStrip, 44 | ) 45 | .with_state(StateDescriptor { 46 | blend_func: Some(BlendFunction { 47 | source_factor: BlendingFactorSrc::One, 48 | dst_factor: BlendingFactorDest::OneMinusSrcAlpha, 49 | ..Default::default() 50 | }), 51 | ..Default::default() 52 | }) 53 | .with_uniform("u_transform", transform.clone()); 54 | 55 | LineLayer { 56 | lines: Buffer::new_empty(BufferUsageHint::DynamicDraw), 57 | positions: Buffer::new(identity_line(), BufferUsageHint::StaticDraw), 58 | program, 59 | transform, 60 | } 61 | } 62 | 63 | pub fn transform(&self) -> Uniform<[[f32; 4]; 4]> { 64 | self.transform.clone() 65 | } 66 | 67 | pub fn buffer(&self) -> Buffer { 68 | self.lines.clone() 69 | } 70 | } 71 | 72 | impl Drawable for LineLayer { 73 | fn draw(&mut self, renderer: &mut limelight::Renderer) -> Result<()> { 74 | renderer.render_instanced(&mut self.program, &self.positions, &self.lines)?; 75 | 76 | Ok(()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /primitives/src/line/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | flat in uint v_color; 6 | in vec2 v_edge; 7 | 8 | out vec4 f_color; 9 | 10 | void main() { 11 | float dx = fwidth(v_edge.x); 12 | float dy = fwidth(v_edge.y); 13 | 14 | float xcov = min(clamp(0., 1., v_edge.x / dx), clamp(0., 1., (1. - v_edge.x) / dx)); 15 | float ycov = min(clamp(0., 1., v_edge.y / dy), clamp(0., 1., (1. - v_edge.y) / dy)); 16 | float alpha = xcov * ycov; 17 | 18 | f_color = vec4( 19 | float((v_color & 0x000000FFu)) / 255., 20 | float((v_color & 0x0000FF00u) >> 8) / 255., 21 | float((v_color & 0x00FF0000u) >> 16) / 255., 22 | float((v_color & 0xFF000000u) >> 24) / 255.); 23 | } -------------------------------------------------------------------------------- /primitives/src/line/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 start; 4 | in vec2 end; 5 | in uint color; 6 | in float width; 7 | in vec2 line_position; 8 | in vec2 line_edge; 9 | uniform mat4 u_transform; 10 | 11 | flat out uint v_color; 12 | out vec2 v_edge; 13 | 14 | void main() { 15 | vec2 line = normalize(end - start); 16 | vec2 perp = vec2(line.y, -line.x); 17 | 18 | v_edge = line_edge; 19 | vec2 pos = (line_position.x * end) + ((1.-line_position.x) * start) + perp * width * line_position.y; 20 | 21 | gl_Position = vec4(pos, 0., 1.) * u_transform; 22 | v_edge = line_edge; 23 | 24 | v_color = color; 25 | } -------------------------------------------------------------------------------- /primitives/src/line3d/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{color::Color, common::{LinePosition, identity_line}}; 2 | use anyhow::Result; 3 | use limelight::{ 4 | attribute, 5 | renderer::Drawable, 6 | state::{ 7 | blending::{BlendFunction, BlendingFactorDest, BlendingFactorSrc}, 8 | StateDescriptor, 9 | }, 10 | Buffer, BufferUsageHint, DrawMode, Program, Uniform, 11 | }; 12 | 13 | #[attribute] 14 | pub struct Line3D { 15 | pub start: [f32; 3], 16 | pub end: [f32; 3], 17 | pub width: f32, 18 | pub color: Color, 19 | } 20 | 21 | pub struct Line3DLayer { 22 | lines: Buffer, 23 | positions: Buffer, 24 | program: Program, 25 | transform: Uniform<[[f32; 4]; 4]>, 26 | } 27 | 28 | impl Default for Line3DLayer { 29 | fn default() -> Self { 30 | Line3DLayer::new() 31 | } 32 | } 33 | 34 | impl Line3DLayer { 35 | pub fn new() -> Self { 36 | Self::new_transform(Uniform::identity()) 37 | } 38 | 39 | pub fn new_transform(transform: Uniform<[[f32; 4]; 4]>) -> Self { 40 | let program = Program::new( 41 | include_str!("shader.vert"), 42 | include_str!("shader.frag"), 43 | DrawMode::TriangleStrip, 44 | ) 45 | .with_state(StateDescriptor { 46 | blend_func: Some(BlendFunction { 47 | source_factor: BlendingFactorSrc::One, 48 | dst_factor: BlendingFactorDest::OneMinusSrcAlpha, 49 | ..Default::default() 50 | }), 51 | ..Default::default() 52 | }) 53 | .with_uniform("u_transform", transform.clone()); 54 | 55 | Line3DLayer { 56 | lines: Buffer::new_empty(BufferUsageHint::DynamicDraw), 57 | program, 58 | transform, 59 | positions: Buffer::new(identity_line(), BufferUsageHint::StaticDraw), 60 | } 61 | } 62 | 63 | pub fn transform(&self) -> Uniform<[[f32; 4]; 4]> { 64 | self.transform.clone() 65 | } 66 | 67 | pub fn buffer(&self) -> Buffer { 68 | self.lines.clone() 69 | } 70 | } 71 | 72 | impl Drawable for Line3DLayer { 73 | fn draw(&mut self, renderer: &mut limelight::Renderer) -> Result<()> { 74 | renderer.render_instanced(&mut self.program, &self.positions, &self.lines)?; 75 | 76 | Ok(()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /primitives/src/line3d/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | flat in uint v_color; 6 | in vec2 v_edge; 7 | 8 | out vec4 f_color; 9 | 10 | void main() { 11 | float dx = fwidth(v_edge.x); 12 | float dy = fwidth(v_edge.y); 13 | 14 | float xcov = min(clamp(0., 1., v_edge.x / dx), clamp(0., 1., (1. - v_edge.x) / dx)); 15 | float ycov = min(clamp(0., 1., v_edge.y / dy), clamp(0., 1., (1. - v_edge.y) / dy)); 16 | float alpha = xcov * ycov; 17 | 18 | f_color = vec4( 19 | float((v_color & 0x000000FFu)) / 255., 20 | float((v_color & 0x0000FF00u) >> 8) / 255., 21 | float((v_color & 0x00FF0000u) >> 16) / 255., 22 | float((v_color & 0xFF000000u) >> 24) / 255.); 23 | } -------------------------------------------------------------------------------- /primitives/src/line3d/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec3 start; 4 | in vec3 end; 5 | in uint color; 6 | in float width; 7 | in vec2 line_position; 8 | in vec2 line_edge; 9 | uniform mat4 u_transform; 10 | 11 | flat out uint v_color; 12 | out vec2 v_edge; 13 | 14 | void main() { 15 | vec3 line = normalize(end - start); 16 | // TODO: should find a perpendicular line in post-transform space instead of pre? 17 | vec3 perp = vec3(line.y, -line.x, 0.); 18 | 19 | v_edge = line_edge; 20 | vec3 pos = (line_position.x * end) + ((1.-line_position.x) * start) + perp * width * line_position.y; 21 | 22 | gl_Position = vec4(pos, 1.0) * u_transform; 23 | 24 | v_color = color; 25 | } -------------------------------------------------------------------------------- /primitives/src/rect/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use limelight::{ 3 | attribute, 4 | renderer::Drawable, 5 | state::{ 6 | blending::{BlendFunction, BlendingFactorDest, BlendingFactorSrc}, 7 | StateDescriptor, 8 | }, 9 | Buffer, BufferUsageHint, DrawMode, Program, Uniform, 10 | }; 11 | 12 | use crate::{color::Color, common::{RectPosition, identity_rect}}; 13 | 14 | #[attribute] 15 | pub struct Rect { 16 | pub upper_left: [f32; 2], 17 | pub lower_right: [f32; 2], 18 | pub color: Color, 19 | } 20 | 21 | pub struct RectLayer { 22 | rects: Buffer, 23 | positions: Buffer, 24 | program: Program, 25 | transform: Uniform<[[f32; 4]; 4]>, 26 | } 27 | 28 | impl Default for RectLayer { 29 | fn default() -> Self { 30 | RectLayer::new() 31 | } 32 | } 33 | 34 | impl RectLayer { 35 | pub fn new() -> Self { 36 | Self::new_transform(Uniform::identity()) 37 | } 38 | 39 | pub fn new_transform(transform: Uniform<[[f32; 4]; 4]>) -> Self { 40 | let program = Program::new( 41 | include_str!("shader.vert"), 42 | include_str!("shader.frag"), 43 | DrawMode::TriangleStrip, 44 | ) 45 | .with_state(StateDescriptor { 46 | blend_func: Some(BlendFunction { 47 | source_factor: BlendingFactorSrc::One, 48 | dst_factor: BlendingFactorDest::OneMinusSrcAlpha, 49 | ..Default::default() 50 | }), 51 | ..Default::default() 52 | }) 53 | .with_uniform("u_transform", transform.clone()); 54 | 55 | RectLayer { 56 | rects: Buffer::new_empty(BufferUsageHint::DynamicDraw), 57 | positions: Buffer::new(identity_rect(), BufferUsageHint::StaticDraw), 58 | program, 59 | transform, 60 | } 61 | } 62 | 63 | pub fn transform(&self) -> Uniform<[[f32; 4]; 4]> { 64 | self.transform.clone() 65 | } 66 | 67 | pub fn buffer(&self) -> Buffer { 68 | self.rects.clone() 69 | } 70 | } 71 | 72 | impl Drawable for RectLayer { 73 | fn draw(&mut self, renderer: &mut limelight::Renderer) -> Result<()> { 74 | renderer.render_instanced(&mut self.program, &self.positions, &self.rects)?; 75 | 76 | Ok(()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /primitives/src/rect/shader.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | precision highp float; 4 | 5 | flat in uint v_color; 6 | out vec4 f_color; 7 | 8 | void main() { 9 | f_color = vec4( 10 | float((v_color & 0x000000FFu)) / 255., 11 | float((v_color & 0x0000FF00u) >> 8) / 255., 12 | float((v_color & 0x00FF0000u) >> 16) / 255., 13 | float((v_color & 0xFF000000u) >> 24) / 255.); 14 | } -------------------------------------------------------------------------------- /primitives/src/rect/shader.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | 3 | in vec2 upper_left; 4 | in vec2 lower_right; 5 | in uint color; 6 | in vec2 rect_position; 7 | 8 | flat out uint v_color; 9 | uniform mat4 u_transform; 10 | 11 | void main() { 12 | gl_Position = vec4( 13 | upper_left.x * (1. - rect_position.x) + lower_right.x * rect_position.x, 14 | upper_left.y * (1. - rect_position.y) + lower_right.y * rect_position.y, 15 | 0., 16 | 1. 17 | ); 18 | 19 | gl_Position = gl_Position * u_transform; 20 | 21 | v_color = color; 22 | } -------------------------------------------------------------------------------- /transform/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-transform" 3 | version = "0.1.0" 4 | edition = "2021" 5 | repository = "https://github.com/drifting-in-space/limelight" 6 | description = "A transformation matrix helper for limelight." 7 | license = "MIT" 8 | readme = "README.md" 9 | keywords = ["webgl"] 10 | 11 | [dependencies] 12 | limelight = {version="0.1.3", path="../limelight"} 13 | -------------------------------------------------------------------------------- /transform/README.md: -------------------------------------------------------------------------------- 1 | # `limelight-transform` 2 | 3 | This crate provides `TransformUniform`, which wraps a [`Uniform<[[f32; 4]; 4]>`](https://docs.rs/limelight/latest/limelight/uniform/struct.Uniform.html) with methods that allow 4 | transform operations like zooming and panning. 5 | 6 | See the [zoom-pan](https://drifting-in-space.github.io/limelight/zoom-pan/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/zoom-pan)) and [primitive scene](https://drifting-in-space.github.io/limelight/primitive-scene/) ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/primitive-scene)) demos for usage examples. 7 | 8 | -------------------------------------------------------------------------------- /transform/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use limelight::Uniform; 4 | 5 | pub struct TransformUniform { 6 | scale: (f32, f32), 7 | center: (f32, f32), 8 | uniform: Uniform<[[f32; 4]; 4]>, 9 | shear: (f32, f32), 10 | } 11 | 12 | fn scale_center_to_matrix( 13 | (scale_x, scale_y): (f32, f32), 14 | (center_x, center_y): (f32, f32), 15 | (shear_x, shear_y): (f32, f32), 16 | ) -> [[f32; 4]; 4] { 17 | [ 18 | [scale_x, 0., 0., -center_x], 19 | [0., scale_y, 0., -center_y], 20 | [0., 0., 1., 0.], 21 | [shear_x, shear_y, 0., 1.], 22 | ] 23 | } 24 | 25 | impl Default for TransformUniform { 26 | fn default() -> Self { 27 | Self::new() 28 | } 29 | } 30 | 31 | impl TransformUniform { 32 | pub fn new() -> Self { 33 | let scale = (1., 1.); 34 | let center = (0., 0.); 35 | let shear = (0., 0.); 36 | let uniform = Uniform::new(scale_center_to_matrix(scale, center, shear)); 37 | TransformUniform { 38 | scale, 39 | center, 40 | shear, 41 | uniform, 42 | } 43 | } 44 | 45 | pub fn uniform(&self) -> Uniform<[[f32; 4]; 4]> { 46 | self.uniform.clone() 47 | } 48 | 49 | fn update_uniform(&self) { 50 | self.uniform 51 | .set_value(scale_center_to_matrix(self.scale, self.center, self.shear)); 52 | } 53 | 54 | /// Multiply the current scale, in such a way that the given point 55 | /// remains stationary. 56 | /// 57 | /// i.e. if `v * self.uniform.value = scale_center` is true before the 58 | /// scale is applied, it should also be true after. 59 | pub fn scale(&mut self, scale_factor: f32, scale_center: (f32, f32)) { 60 | let old_scale_x = self.scale.0; 61 | let old_scale_y = self.scale.1; 62 | 63 | self.scale.0 *= scale_factor; 64 | self.scale.1 *= scale_factor; 65 | 66 | self.center.0 = 67 | (self.scale.0 / old_scale_x) * (scale_center.0 + self.center.0) - scale_center.0; 68 | self.center.1 = 69 | (self.scale.1 / old_scale_y) * (scale_center.1 + self.center.1) - scale_center.1; 70 | 71 | self.update_uniform(); 72 | } 73 | 74 | // Pan by the given amount, provided in destination units. 75 | pub fn pan(&mut self, vector: (f32, f32)) { 76 | self.center.0 -= vector.0; 77 | self.center.1 -= vector.1; 78 | self.update_uniform(); 79 | } 80 | 81 | pub fn shear(&mut self, vector: (f32, f32)) { 82 | self.shear.0 -= vector.0; 83 | self.shear.1 -= vector.1; 84 | self.update_uniform(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /yew/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "limelight-yew" 3 | version = "0.1.0" 4 | edition = "2021" 5 | repository = "https://github.com/drifting-in-space/limelight" 6 | description = "Scaffolding for creating WebGL2-rendered Yew components with limelight" 7 | license = "MIT" 8 | readme = "README.md" 9 | keywords = ["webgl"] 10 | 11 | [dependencies] 12 | yew = "0.19.3" 13 | limelight = {version="0.1.3", path="../limelight"} 14 | web-sys = { version="0.3.55", features=["Window", "HtmlCanvasElement"] } 15 | wasm-bindgen = "0.2.78" 16 | gloo-render = "0.1.0" 17 | log = "0.4.14" 18 | anyhow = "1.0.51" 19 | gloo-events = "0.1.1" 20 | -------------------------------------------------------------------------------- /yew/README.md: -------------------------------------------------------------------------------- 1 | # `limelight-yew` 2 | 3 | Provides scaffolding for building [Yew](https://yew.rs/) components that use 4 | [limelight](https://github.com/drifting-in-space/limelight) to render to a 5 | canvas. Apps can implement `LimelightController`, which can then be wrapped 6 | in `LimelightComponent` and used as a Yew component directly. 7 | 8 | For example, the [primitive scene](https://drifting-in-space.github.io/limelight/primitive-scene/) demo ([code](https://github.com/drifting-in-space/limelight/tree/main/examples/primitive-scene)) 9 | is implemented as a class `Primitives`, which implements [`std::default::Default`](https://doc.rust-lang.org/std/default/trait.Default.html) and `LimelightController`. 10 | 11 | It is then initialized through Yew like so: 12 | 13 | ```rust 14 | yew::start_app::>(); 15 | ``` 16 | 17 | Implementors of `LimelightController` are only required to implement one function, 18 | `fn draw(&mut self, renderer: &mut Renderer, _ts: f64) -> Result`. This function is called every 19 | animation frame to tell the controller to draw its content using the provided [`Renderer`](https://docs.rs/limelight/latest/limelight/renderer/struct.Renderer.html). 20 | 21 | Implementors may optionally implement other functions like `handle_zoom` and `handle_drag` to 22 | respond to mouse interactions with the component. 23 | 24 | All methods return either a `bool` (aliased to `ShouldRequestAnimationFrame`) or a `Result`. 25 | These methods should return `true` if they would like to 26 | trigger a redraw. For `handle_` methods, this usually 27 | means that they modified buffers or uniforms that should 28 | be reflected in the image. For `draw`, returning `Ok(true)` 29 | is the intended way to create an animation loop if the 30 | `draw` call itself updates uniforms or buffers. 31 | -------------------------------------------------------------------------------- /yew/src/key_event.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, PartialEq, Debug)] 2 | pub enum KeyCode { 3 | PrintableCharacter(char), 4 | Unidentified, 5 | Alt, 6 | AltGraph, 7 | ArrowDown, 8 | ArrowLeft, 9 | ArrowRight, 10 | ArrowUp, 11 | Backspace, 12 | CapsLock, 13 | Clear, 14 | Control, 15 | Copy, 16 | CrSel, 17 | Cut, 18 | Delete, 19 | End, 20 | Enter, 21 | EraseEof, 22 | ExSel, 23 | Fn, 24 | FnLock, 25 | Home, 26 | Insert, 27 | Meta, 28 | NumLock, 29 | PageDown, 30 | PageUp, 31 | Paste, 32 | Redo, 33 | ScrollLock, 34 | Shift, 35 | Symbol, 36 | SymbolLock, 37 | Tab, 38 | Undo, 39 | } 40 | 41 | impl From<&str> for KeyCode { 42 | fn from(key: &str) -> Self { 43 | match key { 44 | "Alt" => KeyCode::Alt, 45 | "AltGraph" => KeyCode::AltGraph, 46 | "ArrowDown" => KeyCode::ArrowDown, 47 | "ArrowLeft" => KeyCode::ArrowLeft, 48 | "ArrowRight" => KeyCode::ArrowRight, 49 | "ArrowUp" => KeyCode::ArrowUp, 50 | "Backspace" => KeyCode::Backspace, 51 | "CapsLock" => KeyCode::CapsLock, 52 | "Clear" => KeyCode::Clear, 53 | "Control" => KeyCode::Control, 54 | "Copy" => KeyCode::Copy, 55 | "CrSel" => KeyCode::CrSel, 56 | "Cut" => KeyCode::Cut, 57 | "Delete" => KeyCode::Delete, 58 | "End" => KeyCode::End, 59 | "Enter" => KeyCode::Enter, 60 | "EraseEof" => KeyCode::EraseEof, 61 | "ExSel" => KeyCode::ExSel, 62 | "Fn" => KeyCode::Fn, 63 | "FnLock" => KeyCode::FnLock, 64 | "Home" => KeyCode::Home, 65 | "Insert" => KeyCode::Insert, 66 | "Meta" => KeyCode::Meta, 67 | "NumLock" => KeyCode::NumLock, 68 | "PageDown" => KeyCode::PageDown, 69 | "PageUp" => KeyCode::PageUp, 70 | "Paste" => KeyCode::Paste, 71 | "Redo" => KeyCode::Redo, 72 | "ScrollLock" => KeyCode::ScrollLock, 73 | "Shift" => KeyCode::Shift, 74 | "Symbol" => KeyCode::Symbol, 75 | "SymbolLock" => KeyCode::SymbolLock, 76 | "Tab" => KeyCode::Tab, 77 | "Undo" => KeyCode::Undo, 78 | c if c.len() == 1 => KeyCode::PrintableCharacter(c.chars().next().unwrap()), 79 | _ => KeyCode::Unidentified, 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /yew/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod key_event; 2 | 3 | use anyhow::Result; 4 | use gloo_events::{EventListener, EventListenerOptions}; 5 | use gloo_render::{request_animation_frame, AnimationFrame}; 6 | pub use key_event::KeyCode; 7 | use limelight::renderer::Renderer; 8 | use std::{cell::RefCell, marker::PhantomData, rc::Rc}; 9 | use wasm_bindgen::JsCast; 10 | use web_sys::{window, HtmlCanvasElement, WebGl2RenderingContext}; 11 | use yew::{html, Component, KeyboardEvent, MouseEvent, NodeRef, Properties, WheelEvent}; 12 | 13 | pub type ShouldRequestAnimationFrame = bool; 14 | pub type ShouldCancelEvent = bool; 15 | 16 | #[allow(unused_variables)] 17 | pub trait LimelightController: 'static { 18 | fn draw(&mut self, renderer: &mut Renderer, ts: f64) -> Result; 19 | 20 | fn handle_key_down( 21 | &mut self, 22 | key: KeyCode, 23 | ) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 24 | (false, false) 25 | } 26 | 27 | fn handle_key_up(&mut self, key: KeyCode) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 28 | (false, false) 29 | } 30 | 31 | fn handle_drag(&mut self, x: f32, y: f32) -> ShouldRequestAnimationFrame { 32 | false 33 | } 34 | 35 | fn handle_mousemove(&mut self, x: f32, y: f32) -> ShouldRequestAnimationFrame { 36 | false 37 | } 38 | 39 | fn handle_scroll( 40 | &mut self, 41 | x_amount: f32, 42 | y_amount: f32, 43 | x_position: f32, 44 | y_position: f32, 45 | ) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 46 | (false, false) 47 | } 48 | 49 | fn handle_pinch( 50 | &mut self, 51 | amount: f32, 52 | x: f32, 53 | y: f32, 54 | ) -> (ShouldRequestAnimationFrame, ShouldCancelEvent) { 55 | (false, false) 56 | } 57 | } 58 | 59 | pub struct LimelightComponent { 60 | canvas_ref: NodeRef, 61 | renderer: Option, 62 | render_handle: Option, 63 | keydown_handler: Option, 64 | keyup_handler: Option, 65 | drag_origin: Option<(i32, i32)>, 66 | _ph: PhantomData, 67 | } 68 | 69 | #[derive(Debug)] 70 | pub enum Msg { 71 | Render(f64), 72 | MouseMove(MouseEvent), 73 | MouseDown(MouseEvent), 74 | MouseUp(MouseEvent), 75 | MouseWheel(WheelEvent), 76 | KeyDown(KeyboardEvent), 77 | KeyUp(KeyboardEvent), 78 | } 79 | 80 | #[derive(Properties)] 81 | pub struct LimelightComponentProps { 82 | pub controller: Rc>, 83 | pub height: i32, 84 | pub width: i32, 85 | } 86 | 87 | impl Default for LimelightComponentProps 88 | where 89 | Controller: Default, 90 | { 91 | fn default() -> Self { 92 | Self { 93 | controller: Rc::new(RefCell::new(Controller::default())), 94 | width: 600, 95 | height: 600, 96 | } 97 | } 98 | } 99 | 100 | impl PartialEq for LimelightComponentProps { 101 | fn eq(&self, other: &Self) -> bool { 102 | Rc::ptr_eq(&self.controller, &other.controller) 103 | } 104 | } 105 | 106 | impl LimelightComponent { 107 | fn request_render(&mut self, ctx: &yew::Context) { 108 | let render_callback = ctx.link().callback(Msg::Render); 109 | self.render_handle = Some(request_animation_frame(move |ts| render_callback.emit(ts))); 110 | } 111 | } 112 | 113 | impl Component for LimelightComponent { 114 | type Message = Msg; 115 | 116 | type Properties = LimelightComponentProps; 117 | 118 | fn create(_ctx: &yew::Context) -> Self { 119 | Self { 120 | canvas_ref: NodeRef::default(), 121 | renderer: None, 122 | render_handle: None, 123 | keydown_handler: None, 124 | keyup_handler: None, 125 | drag_origin: None, 126 | _ph: PhantomData::default(), 127 | } 128 | } 129 | 130 | fn update(&mut self, ctx: &yew::Context, msg: Self::Message) -> bool { 131 | match msg { 132 | Msg::Render(ts) => { 133 | if let Some(renderer) = &mut self.renderer { 134 | let should_render = (*ctx.props().controller) 135 | .borrow_mut() 136 | .draw(renderer, ts) 137 | .unwrap(); 138 | 139 | if should_render { 140 | self.request_render(ctx); 141 | } 142 | } 143 | } 144 | Msg::KeyDown(event) => { 145 | let (should_render, should_cancel_event) = (*ctx.props().controller) 146 | .borrow_mut() 147 | .handle_key_down(event.key().as_str().into()); 148 | if should_render { 149 | self.request_render(ctx); 150 | } 151 | if should_cancel_event { 152 | event.prevent_default(); 153 | } 154 | } 155 | Msg::KeyUp(event) => { 156 | let (should_render, should_cancel_event) = (*ctx.props().controller) 157 | .borrow_mut() 158 | .handle_key_up(event.key().as_str().into()); 159 | if should_render { 160 | self.request_render(ctx); 161 | } 162 | if should_cancel_event { 163 | event.prevent_default(); 164 | } 165 | } 166 | Msg::MouseDown(e) => { 167 | self.drag_origin = Some((e.offset_x(), e.offset_y())); 168 | } 169 | Msg::MouseUp(_) => { 170 | self.drag_origin = None; 171 | } 172 | Msg::MouseMove(e) => { 173 | let (new_x, new_y) = (e.offset_x(), e.offset_y()); 174 | 175 | if let Some((origin_x, origin_y)) = self.drag_origin { 176 | let should_render = (*ctx.props().controller).borrow_mut().handle_drag( 177 | 2. * (new_x - origin_x) as f32 / ctx.props().width as f32, 178 | 2. * -(new_y - origin_y) as f32 / ctx.props().height as f32, 179 | ); 180 | 181 | if should_render { 182 | self.request_render(ctx); 183 | } 184 | 185 | self.drag_origin = Some((new_x, new_y)); 186 | } else { 187 | let should_render = (*ctx.props().controller).borrow_mut().handle_mousemove( 188 | 2. * new_x as f32 / ctx.props().width as f32 - 1., 189 | 2. * -new_y as f32 / ctx.props().height as f32 + 1., 190 | ); 191 | 192 | if should_render { 193 | self.request_render(ctx); 194 | } 195 | } 196 | } 197 | Msg::MouseWheel(e) => { 198 | let scroll_amount_y = e.delta_y() as f32; 199 | let scroll_amount_x = e.delta_x() as f32; 200 | 201 | let pin_x = (2 * e.offset_x()) as f32 / ctx.props().width as f32 - 1.; 202 | let pin_y = -((2 * e.offset_y()) as f32 / ctx.props().height as f32 - 1.); 203 | 204 | let (should_render, should_cancel_event) = if e.ctrl_key() { 205 | (*ctx.props().controller).borrow_mut().handle_pinch( 206 | -scroll_amount_y, 207 | pin_x, 208 | pin_y, 209 | ) 210 | } else { 211 | (*ctx.props().controller).borrow_mut().handle_scroll( 212 | -scroll_amount_x as f32 * 2. / ctx.props().width as f32, 213 | scroll_amount_y as f32 * 2. / ctx.props().height as f32, 214 | pin_x, 215 | pin_y, 216 | ) 217 | }; 218 | 219 | if should_render { 220 | self.request_render(ctx); 221 | } 222 | 223 | if should_cancel_event { 224 | e.prevent_default(); 225 | } 226 | } 227 | } 228 | 229 | false 230 | } 231 | 232 | fn view(&self, ctx: &yew::Context) -> yew::Html { 233 | let props = ctx.props(); 234 | let link = ctx.link(); 235 | let device_pixel_ratio = window().unwrap().device_pixel_ratio(); 236 | 237 | html! { 238 | 248 | } 249 | } 250 | 251 | fn rendered(&mut self, ctx: &yew::Context, first_render: bool) { 252 | if first_render { 253 | let canvas = self.canvas_ref.cast::().unwrap(); 254 | let gl: WebGl2RenderingContext = canvas 255 | .get_context("webgl2") 256 | .unwrap() 257 | .unwrap() 258 | .dyn_into() 259 | .unwrap(); 260 | 261 | let options = EventListenerOptions::enable_prevent_default(); 262 | { 263 | let callback = ctx.link().callback(Msg::KeyDown); 264 | self.keydown_handler = Some(EventListener::new_with_options( 265 | &window().unwrap(), 266 | "keydown", 267 | options, 268 | move |event| callback.emit(event.clone().dyn_into().unwrap()), 269 | )); 270 | } 271 | { 272 | let callback = ctx.link().callback(Msg::KeyUp); 273 | self.keyup_handler = Some(EventListener::new_with_options( 274 | &window().unwrap(), 275 | "keyup", 276 | options, 277 | move |event| callback.emit(event.clone().dyn_into().unwrap()), 278 | )); 279 | } 280 | 281 | self.renderer = Some(Renderer::new(gl)); 282 | 283 | self.request_render(ctx); 284 | } 285 | } 286 | } 287 | --------------------------------------------------------------------------------