├── .github └── workflows │ ├── build.yml │ └── format.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── docs ├── index.html ├── load.png ├── particles.js ├── particles.png ├── particles_bg.wasm ├── pong.js ├── pong_bg.wasm ├── start.png ├── storm.js ├── text.js ├── text.png ├── text_bg.wasm ├── texture.js └── texture_bg.wasm ├── examples ├── particles.rs ├── pong.rs ├── resources │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── Roboto-Regular.ttf │ ├── arial.ttf │ ├── boop.flac │ ├── clear.wav │ ├── test.flac │ ├── test2.flac │ └── tetris.ogg ├── text.rs ├── texture.rs ├── triangle.rs └── triangle_shader │ ├── fragment.glsl │ ├── mod.rs │ └── vertex.glsl ├── licenses ├── RSSchermer │ └── std140 │ │ └── LICENSE ├── Ralith │ └── clatter │ │ ├── LICENSE-APACHE │ │ ├── LICENSE-ZLIB │ │ └── README.md └── polyfractal │ └── bounded-spsc-queue │ └── LICENSE ├── particles.bat ├── rustfmt.toml ├── src ├── asset │ ├── asset.rs │ ├── error.rs │ ├── mod.rs │ └── platform │ │ ├── native.rs │ │ └── wasm.rs ├── audio │ ├── control.rs │ ├── instance.rs │ ├── mixer.rs │ ├── mod.rs │ ├── sound.rs │ └── state.rs ├── color │ ├── mod.rs │ ├── r8.rs │ ├── rg8.rs │ ├── rgb8.rs │ └── rgba8.rs ├── engine.rs ├── event │ ├── converter.rs │ ├── event.rs │ └── mod.rs ├── graphics │ ├── blend.rs │ ├── buffer.rs │ ├── frame_buffer.rs │ ├── index_buffer.rs │ ├── mod.rs │ ├── opengl.rs │ ├── shader.rs │ ├── shaders │ │ ├── mod.rs │ │ ├── sprite │ │ │ ├── fragment.glsl │ │ │ ├── mod.rs │ │ │ └── vertex.glsl │ │ └── text │ │ │ ├── data.rs │ │ │ ├── fragment.glsl │ │ │ ├── mod.rs │ │ │ ├── shader.rs │ │ │ └── vertex.glsl │ ├── state.rs │ ├── std140 │ │ ├── cgmath.rs │ │ ├── core.rs │ │ └── mod.rs │ ├── texture.rs │ ├── texture_atlas.rs │ ├── texture_section.rs │ ├── uniform.rs │ ├── vertex_descriptor.rs │ └── window │ │ ├── display_mode.rs │ │ ├── mod.rs │ │ ├── platform │ │ ├── native.rs │ │ └── wasm.rs │ │ └── window_settings.rs ├── image │ ├── image.rs │ ├── mod.rs │ ├── packer.rs │ ├── png.rs │ └── resize.rs ├── lib.rs ├── math │ ├── aabb.rs │ ├── interpolation.rs │ ├── mod.rs │ ├── num │ │ ├── mod.rs │ │ ├── nostd.rs │ │ └── trigonometry.rs │ ├── orthographic.rs │ └── perspective.rs ├── noise │ ├── add.rs │ ├── constant.rs │ ├── fbm.rs │ ├── fill.rs │ ├── mod.rs │ ├── multiply.rs │ └── simplex │ │ ├── dim1.rs │ │ ├── dim2.rs │ │ ├── dim3.rs │ │ ├── dim4.rs │ │ ├── hash.rs │ │ └── mod.rs ├── storm.js ├── sync │ ├── mod.rs │ ├── signal.rs │ └── spsc.rs └── time │ ├── convert.rs │ └── mod.rs ├── storm_macro ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml └── src │ └── lib.rs ├── texture.bat └── web.bat /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | rust: [stable, beta] 9 | steps: 10 | - name: Linux audio support 11 | run: | 12 | sudo apt-get install -y libasound2-dev 13 | - uses: hecrj/setup-rust-action@v1 14 | with: 15 | rust-version: ${{ matrix.rust }} 16 | targets: wasm32-unknown-unknown 17 | - uses: actions/checkout@master 18 | - name: Build native 19 | run: | 20 | cargo build --verbose 21 | - name: Build wasm 22 | run: | 23 | cargo build --verbose --target wasm32-unknown-unknown 24 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | on: [push, pull_request] 3 | jobs: 4 | all: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: hecrj/setup-rust-action@v1 8 | with: 9 | components: rustfmt, clippy 10 | - uses: actions/checkout@master 11 | - name: Check format 12 | run: | 13 | cargo fmt --all -- --check 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Rust 2 | 3 | # Generated by Cargo 4 | # will have compiled files and executables 5 | **/target/ 6 | **/rls/ 7 | 8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 9 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 10 | Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | 15 | ## Mac OS 16 | 17 | # General 18 | .DS_Store 19 | .AppleDouble 20 | .LSOverride 21 | .vscode -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "storm" 3 | version = "0.11.0" 4 | authors = ["Joe Cumbo "] 5 | description = "A personal 2D game engine designed for performance" 6 | license = "MIT" 7 | readme = "README.md" 8 | keywords = ["game", "engine", "2D", "graphics"] 9 | categories = ["game-engines"] 10 | edition = "2018" 11 | documentation = "https://docs.rs/storm" 12 | homepage = "https://github.com/mooman219/storm" 13 | repository = "https://github.com/mooman219/storm" 14 | include = [ 15 | "Cargo.toml", 16 | "src/", 17 | "storm_macro/", 18 | "licenses/", 19 | "README.md", 20 | "LICENSE", 21 | ] 22 | 23 | [badges] 24 | maintenance = { status = "experimental" } 25 | 26 | [target.'cfg(target_arch = "wasm32")'.dependencies] 27 | console_log = { version = "0.2", features = ["color"] } 28 | js-sys = "0.3.55" 29 | web-sys = { version = "0.3.55", features = [ 30 | "HtmlCanvasElement", 31 | "WebGl2RenderingContext", 32 | "Window", 33 | ] } 34 | wasm-bindgen = { version = "0.2.58" } 35 | console_error_panic_hook = "0.1" 36 | cpal = { version = "0.15", features = ["wasm-bindgen"] } 37 | 38 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 39 | glutin = "0.29.1" 40 | cpal = "0.15" 41 | 42 | [dependencies] 43 | storm_macro = { version = "0.1.0", path = "storm_macro" } 44 | winit = "0.27.5" 45 | simplelog = "0.12.1" 46 | instant = "0.1.12" 47 | glow = "0.12.1" 48 | cgmath = "0.18.0" 49 | log = "0.4" 50 | hashbrown = "0.13.2" 51 | fontdue = { version = "0.7.2" } 52 | parking_lot = "0.12" 53 | profiling = "1.0.7" 54 | 55 | # Image format support 56 | png = "0.14" 57 | 58 | # Audio format support 59 | audrey = "0.3" 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joseph Cumbo 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 | # Storm 2 | 3 | [![Test](https://github.com/mooman219/fontdue/workflows/Test/badge.svg)](https://github.com/mooman219/storm/actions) 4 | [![Documentation](https://docs.rs/storm/badge.svg)](https://docs.rs/storm) 5 | [![Crates.io](https://img.shields.io/crates/v/storm.svg)](https://crates.io/crates/storm) 6 | [![License](https://img.shields.io/crates/l/storm.svg)](https://github.com/mooman219/storm/blob/master/LICENSE) 7 | 8 | The storm engine is a simple set of primitives. It currently features a GLES3/GL3.3 backend and supports Windows, Linux, Web, and Mac. It's unstable, buggy, and really just a playground for me. -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 27 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/docs/load.png -------------------------------------------------------------------------------- /docs/particles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/docs/particles.png -------------------------------------------------------------------------------- /docs/particles_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/docs/particles_bg.wasm -------------------------------------------------------------------------------- /docs/pong_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/docs/pong_bg.wasm -------------------------------------------------------------------------------- /docs/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/docs/start.png -------------------------------------------------------------------------------- /docs/storm.js: -------------------------------------------------------------------------------- 1 | var fs_callback; 2 | 3 | export function fs_load_files(index, paths) { 4 | let promises = paths.map(function (path) { 5 | return fetch(path).then(function (response) { 6 | if (response.status < 200 || response.status >= 300) { 7 | return response.status; 8 | } else { 9 | return response.arrayBuffer().then(function (buffer) { 10 | return new Uint8Array(buffer); 11 | }).catch(function (reason) { 12 | return 500; 13 | }); 14 | } 15 | }).catch(function (reason) { 16 | return 500; 17 | }); 18 | }); 19 | Promise.all(promises).then(function (array) { 20 | fs_callback(index, array); 21 | }); 22 | } 23 | 24 | export function fs_init_callback(callback) { 25 | fs_callback = callback; 26 | } 27 | -------------------------------------------------------------------------------- /docs/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/docs/text.png -------------------------------------------------------------------------------- /docs/text_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/docs/text_bg.wasm -------------------------------------------------------------------------------- /docs/texture_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/docs/texture_bg.wasm -------------------------------------------------------------------------------- /examples/resources/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/1.png -------------------------------------------------------------------------------- /examples/resources/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/2.png -------------------------------------------------------------------------------- /examples/resources/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/3.png -------------------------------------------------------------------------------- /examples/resources/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/4.png -------------------------------------------------------------------------------- /examples/resources/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/5.png -------------------------------------------------------------------------------- /examples/resources/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/Roboto-Regular.ttf -------------------------------------------------------------------------------- /examples/resources/arial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/arial.ttf -------------------------------------------------------------------------------- /examples/resources/boop.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/boop.flac -------------------------------------------------------------------------------- /examples/resources/clear.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/clear.wav -------------------------------------------------------------------------------- /examples/resources/test.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/test.flac -------------------------------------------------------------------------------- /examples/resources/test2.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/test2.flac -------------------------------------------------------------------------------- /examples/resources/tetris.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mooman219/storm/82ed04ffab475d91b30eb332b43521fee726dc88/examples/resources/tetris.ogg -------------------------------------------------------------------------------- /examples/text.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | use fontdue::layout::VerticalAlign; 3 | use storm::color::RGBA8; 4 | use storm::event::*; 5 | use storm::fontdue::{layout::LayoutSettings, Font}; 6 | use storm::graphics::{shaders::text::*, ClearMode, DepthTest, DisplayMode, Shader, Vsync, WindowSettings}; 7 | use storm::math::OrthographicCamera; 8 | use storm::*; 9 | 10 | static FONT: &[u8] = include_bytes!("resources/arial.ttf"); 11 | 12 | /// Run with: cargo run --example text --release 13 | fn main() { 14 | // Create the engine context and describe the window. 15 | start::(WindowSettings { 16 | title: String::from("Storm: Text"), 17 | display_mode: DisplayMode::Windowed { 18 | width: 1280, 19 | height: 1024, 20 | resizable: true, 21 | }, 22 | vsync: Vsync::Disabled, 23 | }); 24 | } 25 | 26 | const SIZE: f32 = 40.0; 27 | const COLOR: RGBA8 = RGBA8::WHITE; 28 | 29 | struct TextApp { 30 | is_dragging: bool, 31 | transform: OrthographicCamera, 32 | text_shader: Shader, 33 | text_layer: TextShaderPass, 34 | fonts: [Font; 1], 35 | layout_settings: LayoutSettings, 36 | message: String, 37 | } 38 | 39 | impl App for TextApp { 40 | fn new(ctx: &mut Context) -> Self { 41 | ctx.wait_periodic(Some(Duration::from_secs_f32(1.0 / 144.0))); 42 | let is_dragging = false; 43 | let mut transform = OrthographicCamera::new(ctx.window_logical_size()); 44 | let text_shader = Shader::new(ctx, TEXT_SHADER); 45 | 46 | // Create a Layers to draw on. 47 | let mut text_layer = TextShaderPass::new(ctx, transform.matrix()); 48 | 49 | // Setup the layout for our text. 50 | let fonts = [Font::from_bytes(FONT, Default::default()).unwrap()]; 51 | let layout_settings = LayoutSettings { 52 | x: 100.0, 53 | y: 500.0, 54 | max_width: Some(500.0), 55 | max_height: Some(500.0), 56 | line_height: 1.0, 57 | vertical_align: VerticalAlign::Middle, 58 | ..Default::default() 59 | }; 60 | 61 | // Append some text with our layout settings. 62 | let message = String::from("Fontdue on web"); 63 | text_layer.append( 64 | &fonts, 65 | &layout_settings, 66 | &[Text { 67 | text: &message, 68 | font_index: 0, 69 | px: SIZE, 70 | color: COLOR, 71 | depth: 0.0, 72 | }], 73 | ); 74 | 75 | TextApp { 76 | is_dragging, 77 | transform, 78 | text_shader, 79 | text_layer, 80 | fonts, 81 | layout_settings, 82 | message, 83 | } 84 | } 85 | 86 | fn on_update(&mut self, ctx: &mut Context, _delta: f32) { 87 | ctx.clear(ClearMode::new().with_color(RGBA8::BLACK).with_depth(1.0, DepthTest::Less)); 88 | self.text_layer.draw(&self.text_shader); 89 | } 90 | 91 | fn on_close_requested(&mut self, ctx: &mut Context) { 92 | ctx.request_stop(); 93 | } 94 | 95 | fn on_received_character(&mut self, _ctx: &mut Context, character: char) { 96 | // Backspace 97 | if character == '\u{08}' { 98 | return; 99 | } 100 | self.message.push(character); 101 | self.text_layer.clear_text(); 102 | self.text_layer.append( 103 | &self.fonts, 104 | &self.layout_settings, 105 | &[Text { 106 | text: &self.message, 107 | font_index: 0, 108 | px: SIZE, 109 | color: COLOR, 110 | depth: 0.0, 111 | }], 112 | ); 113 | } 114 | 115 | fn on_key_pressed(&mut self, ctx: &mut Context, key: event::KeyboardButton, _is_repeat: bool) { 116 | match key { 117 | KeyboardButton::Escape => ctx.request_stop(), 118 | KeyboardButton::Tab => { 119 | self.transform.set().scale = 1.0; 120 | self.text_layer.set_ortho(self.transform.matrix()); 121 | } 122 | KeyboardButton::Back => { 123 | self.message.pop(); 124 | self.text_layer.clear_text(); 125 | self.text_layer.append( 126 | &self.fonts, 127 | &self.layout_settings, 128 | &[Text { 129 | text: &self.message, 130 | font_index: 0, 131 | px: SIZE, 132 | color: COLOR, 133 | depth: 0.0, 134 | }], 135 | ); 136 | } 137 | _ => {} 138 | } 139 | } 140 | 141 | fn on_cursor_pressed( 142 | &mut self, 143 | _ctx: &mut Context, 144 | button: event::CursorButton, 145 | _physical_pos: cgmath::Vector2, 146 | _normalized_pos: cgmath::Vector2, 147 | ) { 148 | match button { 149 | CursorButton::Left => self.is_dragging = true, 150 | _ => {} 151 | } 152 | } 153 | 154 | fn on_cursor_released( 155 | &mut self, 156 | _ctx: &mut Context, 157 | button: event::CursorButton, 158 | _physical_pos: cgmath::Vector2, 159 | _normalized_pos: cgmath::Vector2, 160 | ) { 161 | match button { 162 | CursorButton::Left => self.is_dragging = false, 163 | _ => {} 164 | } 165 | } 166 | 167 | fn on_cursor_delta(&mut self, _ctx: &mut Context, delta: cgmath::Vector2, _focused: bool) { 168 | if self.is_dragging { 169 | let scale = self.transform.get().scale; 170 | self.transform.set().translation += delta.extend(0.0) / scale; 171 | self.text_layer.set_ortho(self.transform.matrix()); 172 | } 173 | } 174 | 175 | fn on_cursor_scroll(&mut self, _ctx: &mut Context, direction: event::ScrollDirection) { 176 | match direction { 177 | ScrollDirection::Up => self.transform.set().scale *= 1.1, 178 | ScrollDirection::Down => self.transform.set().scale /= 1.1, 179 | _ => {} 180 | } 181 | self.text_layer.set_ortho(self.transform.matrix()); 182 | } 183 | 184 | fn on_window_resized( 185 | &mut self, 186 | _ctx: &mut Context, 187 | _physical_size: cgmath::Vector2, 188 | logical_size: cgmath::Vector2, 189 | _scale_factor: f32, 190 | ) { 191 | self.transform.set_size(logical_size); 192 | self.text_layer.set_ortho(self.transform.matrix()); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /examples/texture.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{Vector2, Vector3}; 2 | use core::time::Duration; 3 | use storm::audio::*; 4 | use storm::color::RGBA8; 5 | use storm::event::*; 6 | use storm::graphics::Buffer; 7 | use storm::graphics::{ 8 | shaders::sprite::*, ClearMode, DepthTest, DisplayMode, Shader, Texture, TextureFiltering, Uniform, Vsync, 9 | WindowSettings, 10 | }; 11 | use storm::math::OrthographicCamera; 12 | use storm::*; 13 | 14 | static TEXTURE_A: &[u8] = include_bytes!("resources/3.png"); 15 | static SOUND: &[u8] = include_bytes!("resources/boop.flac"); 16 | 17 | /// Run with: cargo run --example texture --release 18 | fn main() { 19 | start::(WindowSettings { 20 | title: String::from("Storm: Texture"), 21 | display_mode: DisplayMode::Windowed { 22 | width: 1280, 23 | height: 1024, 24 | resizable: true, 25 | }, 26 | vsync: Vsync::Disabled, 27 | }); 28 | } 29 | 30 | struct TextureApp { 31 | sprite_shader: Shader, 32 | texture_atlas: Texture, 33 | sprite_buffer: Buffer, 34 | transform: OrthographicCamera, 35 | transform_uniform: Uniform, 36 | sound: SoundControl, 37 | sprites: [Sprite; 3], 38 | clicking: bool, 39 | } 40 | 41 | impl App for TextureApp { 42 | fn new(ctx: &mut Context) -> Self { 43 | ctx.wait_periodic(Some(Duration::from_secs_f32(1.0 / 144.0))); 44 | 45 | let sprite_shader = Shader::new(ctx, SPRITE_SHADER); 46 | let texture_atlas = Texture::from_png(ctx, TEXTURE_A, TextureFiltering::none()); 47 | let mut sprite_buffer = Buffer::new(ctx); 48 | 49 | let mut transform = OrthographicCamera::new(ctx.window_logical_size()); 50 | transform.set().rotation = 0.12; 51 | 52 | let transform_uniform = Uniform::new(ctx, transform.matrix()); 53 | 54 | let source = Sound::from_bytes(SOUND).unwrap(); 55 | let sound = source.play(ctx, 0.3, 0.1); 56 | 57 | let sprites = [ 58 | Sprite::default(), 59 | Sprite { 60 | pos: Vector3::new(-200.0, -62.0, 0.0), 61 | size: Vector2::new(25, 25), 62 | color: RGBA8::WHITE, 63 | ..Sprite::default() 64 | }, 65 | Sprite { 66 | pos: Vector3::new(-200.0, -50.0, 0.0), 67 | size: Vector2::new(400, 3), 68 | color: RGBA8::BLACK, 69 | ..Sprite::default() 70 | }, 71 | ]; 72 | sprite_buffer.set_data(&sprites); 73 | 74 | let clicking = false; 75 | 76 | TextureApp { 77 | sprite_shader, 78 | texture_atlas, 79 | sprite_buffer, 80 | transform, 81 | transform_uniform, 82 | sound, 83 | sprites, 84 | clicking, 85 | } 86 | } 87 | 88 | fn on_update(&mut self, ctx: &mut Context, _delta: f32) { 89 | ctx.clear(ClearMode::new().with_color(RGBA8::BLUE).with_depth(1.0, DepthTest::Less)); 90 | self.sprite_shader.bind(&[&self.transform_uniform], &[&self.texture_atlas]); 91 | self.sprite_buffer.draw(); 92 | } 93 | 94 | fn on_close_requested(&mut self, ctx: &mut Context) { 95 | ctx.request_stop(); 96 | } 97 | 98 | fn on_key_pressed(&mut self, ctx: &mut Context, key: event::KeyboardButton, _is_repeat: bool) { 99 | match key { 100 | KeyboardButton::Escape => ctx.request_stop(), 101 | KeyboardButton::P => self.sound.pause(), 102 | KeyboardButton::R => self.sound.resume(), 103 | KeyboardButton::Q => { 104 | log::info!("Q Read"); 105 | ctx.read(&["./docs/load.png", "./docs/start.png"], |_ctx, _app, assets| { 106 | for asset in assets { 107 | match &asset.result { 108 | Ok(contents) => { 109 | log::info!("Loaded {}: {}", asset.relative_path, contents[1]); 110 | } 111 | Err(error) => log::warn!("Error {}: {:?}", asset.relative_path, error), 112 | } 113 | } 114 | }) 115 | } 116 | KeyboardButton::A => { 117 | log::info!("A Read"); 118 | ctx.read(&["./load.png"], |_ctx, _app, assets| { 119 | for asset in assets { 120 | match &asset.result { 121 | Ok(contents) => { 122 | log::info!("Loaded {}: {}", asset.relative_path, contents[1]); 123 | } 124 | Err(error) => log::warn!("Error {}: {:?}", asset.relative_path, error), 125 | } 126 | } 127 | }) 128 | } 129 | _ => {} 130 | } 131 | } 132 | 133 | fn on_cursor_pressed( 134 | &mut self, 135 | _ctx: &mut Context, 136 | _button: event::CursorButton, 137 | _physical_pos: cgmath::Vector2, 138 | normalized_pos: cgmath::Vector2, 139 | ) { 140 | let pos = self.transform.screen_to_world(normalized_pos); 141 | if pos.x >= self.sprites[1].pos.x 142 | && pos.x <= self.sprites[1].pos.x + self.sprites[1].size.x as f32 143 | && pos.y >= self.sprites[1].pos.y 144 | && pos.y <= self.sprites[1].pos.y + self.sprites[1].size.y as f32 145 | { 146 | self.clicking = true; 147 | } 148 | } 149 | 150 | fn on_cursor_released( 151 | &mut self, 152 | _ctx: &mut Context, 153 | _button: event::CursorButton, 154 | _physical_pos: cgmath::Vector2, 155 | _normalized_pos: cgmath::Vector2, 156 | ) { 157 | self.clicking = false; 158 | } 159 | 160 | fn on_cursor_moved( 161 | &mut self, 162 | _ctx: &mut Context, 163 | _physical_pos: cgmath::Vector2, 164 | normalized_pos: cgmath::Vector2, 165 | ) { 166 | let pos = self.transform.screen_to_world(normalized_pos); 167 | let mut x = pos.x - 12.0; 168 | if self.clicking { 169 | if x < -200.0 { 170 | x = -200.0; 171 | } else if x > 175.0 { 172 | x = 175.0 173 | } 174 | let volume = (x + 200.0) / 375.0; 175 | self.sound.set_volume(volume, 0.01); 176 | self.sprites[1].pos.x = x; 177 | self.sprite_buffer.set_data(&self.sprites); 178 | } 179 | } 180 | 181 | fn on_window_resized( 182 | &mut self, 183 | _ctx: &mut Context, 184 | _physical_size: cgmath::Vector2, 185 | logical_size: cgmath::Vector2, 186 | _scale_factor: f32, 187 | ) { 188 | self.transform.set_size(logical_size); 189 | self.transform_uniform.set(self.transform.matrix()); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /examples/triangle_shader/fragment.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | in vec4 v_color; 4 | out vec4 a_color; 5 | 6 | void main() { 7 | a_color = v_color; 8 | } -------------------------------------------------------------------------------- /examples/triangle_shader/mod.rs: -------------------------------------------------------------------------------- 1 | use storm::cgmath::*; 2 | use storm::graphics::{ 3 | DrawMode, ShaderDescription, VertexAttribute, VertexDescriptor, VertexInputType, VertexInstancing, 4 | VertexOutputType, 5 | }; 6 | 7 | pub const TRIANGLE_SHADER: ShaderDescription = ShaderDescription { 8 | vertex_shader: include_str!("vertex.glsl"), 9 | fragment_shader: include_str!("fragment.glsl"), 10 | texture_names: &[], 11 | uniform_names: &["vertex"], 12 | }; 13 | 14 | #[repr(C)] 15 | #[derive(Debug, Copy, Clone, PartialEq)] 16 | pub struct TrianglePoint { 17 | pub pos: Vector3, 18 | pub col: Vector3, 19 | } 20 | 21 | impl VertexDescriptor for TrianglePoint { 22 | const INSTANCING: VertexInstancing = VertexInstancing::none(); 23 | const ATTRIBUTES: &'static [VertexAttribute] = &[ 24 | VertexAttribute::new(3, VertexInputType::F32, VertexOutputType::F32), 25 | VertexAttribute::new(3, VertexInputType::F32, VertexOutputType::F32), 26 | ]; 27 | const DRAW_MODE: DrawMode = DrawMode::Triangles; 28 | } 29 | 30 | impl Default for TrianglePoint { 31 | fn default() -> TrianglePoint { 32 | TrianglePoint { 33 | pos: Vector3::zero(), 34 | col: Vector3::zero(), 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/triangle_shader/vertex.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision highp int; 3 | 4 | layout(location = 0) in vec3 a_pos; 5 | layout(location = 1) in vec3 a_color; 6 | 7 | out vec4 v_color; 8 | 9 | layout(std140) uniform vertex { 10 | mat4 ortho; 11 | }; 12 | 13 | void main() { 14 | gl_Position = ortho * vec4(a_pos, 1.0); 15 | v_color = vec4(a_color, 1.0); 16 | } -------------------------------------------------------------------------------- /licenses/RSSchermer/std140/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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. -------------------------------------------------------------------------------- /licenses/Ralith/clatter/LICENSE-ZLIB: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Benjamin Saunders 2 | 3 | This software is provided 'as-is', without any express or implied warranty. In 4 | no event will the authors be held liable for any damages arising from the use of 5 | this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, including 8 | commercial applications, and to alter it and redistribute it freely, subject to 9 | the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not claim 12 | that you wrote the original software. If you use this software in a product, an 13 | acknowledgment in the product documentation would be appreciated but is not 14 | required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not be 17 | misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /licenses/Ralith/clatter/README.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | Licensed under either of 4 | 5 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 6 | http://www.apache.org/licenses/LICENSE-2.0) 7 | * Zlib license ([LICENSE-ZLIB](LICENSE-ZLIB) or 8 | https://opensource.org/licenses/Zlib) 9 | 10 | at your option. -------------------------------------------------------------------------------- /particles.bat: -------------------------------------------------------------------------------- 1 | cargo build --example particles --release --target=wasm32-unknown-unknown 2 | @REM wasm-bindgen: cargo install -f wasm-bindgen-cli 3 | wasm-bindgen --target web --no-typescript --out-dir ./docs/ --out-name particles ./target/wasm32-unknown-unknown/release/examples/particles.wasm -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # match_block_trailing_comma = true 2 | # comment_width = 100 3 | # wrap_comments = true 4 | max_width = 110 5 | use_small_heuristics = "Off" -------------------------------------------------------------------------------- /src/asset/asset.rs: -------------------------------------------------------------------------------- 1 | use super::LoaderError; 2 | use crate::{App, Context}; 3 | use alloc::{ 4 | boxed::Box, 5 | string::{String, ToString}, 6 | vec::Vec, 7 | }; 8 | 9 | /// Represents a binary blob loaded from an external source. 10 | #[derive(Clone, Debug)] 11 | pub struct Asset { 12 | /// The path used to query for this asset. 13 | pub relative_path: String, 14 | /// Either the contents of the asset as bytes, or an error. 15 | pub result: Result, LoaderError>, 16 | } 17 | 18 | impl Asset { 19 | /// Creates a new asset that successfully loaded. 20 | pub fn new_ok(relative_path: String, contents: Vec) -> Asset { 21 | Asset { 22 | relative_path, 23 | result: Ok(contents), 24 | } 25 | } 26 | 27 | /// Creates a new asset that failed to load. 28 | pub fn new_err(relative_path: String, error: LoaderError) -> Asset { 29 | Asset { 30 | relative_path, 31 | result: Err(error), 32 | } 33 | } 34 | } 35 | 36 | pub(crate) struct AssetRequest { 37 | pub assets: Vec, 38 | pub callback: Box, &mut A, Vec) + 'static>, 39 | } 40 | 41 | unsafe impl Send for AssetRequest {} 42 | 43 | impl AssetRequest { 44 | pub(crate) fn new, &mut A, Vec) + 'static>( 45 | relative_paths: &[impl AsRef], 46 | callback: C, 47 | ) -> AssetRequest { 48 | AssetRequest { 49 | assets: relative_paths 50 | .into_iter() 51 | .map(|path| Asset::new_err(path.as_ref().to_string(), LoaderError::Pending)) 52 | .collect::>(), 53 | callback: Box::new(callback), 54 | } 55 | } 56 | 57 | pub(crate) fn call(mut self, ctx: &mut Context, app: &mut A) { 58 | (self.callback)(ctx, app, self.assets) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/asset/error.rs: -------------------------------------------------------------------------------- 1 | /// A list specifying general categories of I/O error. 2 | /// 3 | /// This list is intended to grow over time and it is not recommended to 4 | /// exhaustively match against it. 5 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 6 | #[non_exhaustive] 7 | pub enum LoaderError { 8 | /// This file is queued to be read. 9 | Pending, 10 | /// An entity was not found, often a file. 11 | NotFound, 12 | /// The operation lacked the necessary privileges to complete. 13 | PermissionDenied, 14 | /// The connection was refused by the remote server. 15 | ConnectionRefused, 16 | /// The connection was reset by the remote server. 17 | ConnectionReset, 18 | /// The connection was aborted (terminated) by the remote server. 19 | ConnectionAborted, 20 | /// The network operation failed because it was not connected yet. 21 | NotConnected, 22 | /// A socket address could not be bound because the address is already in 23 | /// use elsewhere. 24 | AddrInUse, 25 | /// A nonexistent interface was requested or the requested address was not 26 | /// local. 27 | AddrNotAvailable, 28 | /// The operation failed because a pipe was closed. 29 | BrokenPipe, 30 | /// An entity already exists, often a file. 31 | AlreadyExists, 32 | /// The operation needs to block to complete, but the blocking operation was 33 | /// requested to not occur. 34 | WouldBlock, 35 | /// A parameter was incorrect. 36 | InvalidInput, 37 | /// Data not valid for the operation were encountered. 38 | /// 39 | /// Unlike [`InvalidInput`], this typically means that the operation 40 | /// parameters were valid, however the error was caused by malformed 41 | /// input data. 42 | /// 43 | /// For example, a function that reads a file into a string will error with 44 | /// `InvalidData` if the file's contents are not valid UTF-8. 45 | /// 46 | /// [`InvalidInput`]: LoaderError::InvalidInput 47 | InvalidData, 48 | /// The I/O operation's timeout expired, causing it to be canceled. 49 | TimedOut, 50 | /// An error returned when an operation could not be completed because a 51 | /// call to `write` returned [`Ok(0)`]. 52 | /// 53 | /// This typically means that an operation could only succeed if it wrote a 54 | /// particular number of bytes but only a smaller number of bytes could be 55 | /// written. 56 | WriteZero, 57 | /// This operation was interrupted. 58 | /// 59 | /// Interrupted operations can typically be retried. 60 | Interrupted, 61 | /// This operation is unsupported on this platform. 62 | /// 63 | /// This means that the operation can never succeed. 64 | Unsupported, 65 | // ErrorKinds which are primarily categorisations for OS error 66 | // codes should be added above. 67 | // 68 | /// An error returned when an operation could not be completed because an 69 | /// "end of file" was reached prematurely. 70 | /// 71 | /// This typically means that an operation could only succeed if it read a 72 | /// particular number of bytes but only a smaller number of bytes could be 73 | /// read. 74 | UnexpectedEof, 75 | /// An operation could not be completed, because it failed 76 | /// to allocate enough memory. 77 | OutOfMemory, 78 | // "Unusual" error kinds which do not correspond simply to (sets 79 | // of) OS error codes, should be added just above this comment. 80 | // `Other` and `Uncategorised` should remain at the end: 81 | // 82 | /// A custom error that does not fall under any other I/O error kind. 83 | /// 84 | /// This can be used to construct your own `Error`s that do not match any 85 | /// `ErrorKind`. 86 | /// 87 | /// This `ErrorKind` is not used by the standard library. 88 | /// 89 | /// Errors from the standard library that do not fall under any of the I/O 90 | /// error kinds cannot be `match`ed on, and will only match a wildcard (`_`) pattern. 91 | /// New `ErrorKind`s might be added in the future for some of those. 92 | Other, 93 | } 94 | -------------------------------------------------------------------------------- /src/asset/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | #[path = "platform/native.rs"] 3 | mod native; 4 | #[cfg(not(target_arch = "wasm32"))] 5 | pub(crate) use native::AssetState; 6 | 7 | #[cfg(target_arch = "wasm32")] 8 | #[path = "platform/wasm.rs"] 9 | mod wasm; 10 | #[cfg(target_arch = "wasm32")] 11 | pub(crate) use wasm::AssetState; 12 | 13 | mod asset; 14 | mod error; 15 | 16 | pub use self::asset::Asset; 17 | pub use self::error::LoaderError; 18 | 19 | pub(crate) use self::asset::AssetRequest; 20 | 21 | use crate::{App, Context}; 22 | 23 | pub(crate) trait AssetStateContract { 24 | /// Creates a new asset state. 25 | fn init() -> Self; 26 | 27 | /// Pushes a read request to the queue. Relative to the current working directory. 28 | fn read(&mut self, request: AssetRequest); 29 | 30 | /// Processes all available completed read requests. 31 | fn next(&mut self) -> Option>; 32 | } 33 | 34 | /// Asset related functions. 35 | impl Context { 36 | /// Requests a read of a set of assets. This produces an AssetRead event with the result of the 37 | /// read once all assets requested have completed. 38 | /// 39 | /// ## Platform-specific 40 | /// 41 | /// - **Non-web:** The path is relative to the current working directory. 42 | /// - **Web:** The path is relative to the current url's root. 43 | pub fn read, &mut A, alloc::vec::Vec) + 'static>( 44 | &mut self, 45 | relative_paths: &[impl AsRef], 46 | callback: C, 47 | ) { 48 | let request = AssetRequest::new(relative_paths, callback); 49 | self.assets().read(request); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/asset/platform/native.rs: -------------------------------------------------------------------------------- 1 | use crate::asset::{Asset, AssetRequest, AssetStateContract, LoaderError}; 2 | use crate::sync::{make as spsc_make, Consumer, Producer, Signal}; 3 | use crate::App; 4 | use alloc::{sync::Arc, vec::Vec}; 5 | use std::fs::File; 6 | use std::{io, io::Read}; 7 | use std::{thread, thread::JoinHandle}; 8 | 9 | pub(crate) struct AssetState { 10 | handle: JoinHandle<()>, 11 | signal: Arc, 12 | pending: Vec>, 13 | read_request_sender: Producer>, 14 | read_result_receiver: Consumer>, 15 | } 16 | 17 | impl AssetStateContract for AssetState { 18 | fn init() -> Self { 19 | let (read_request_sender, read_request_receiver): ( 20 | Producer>, 21 | Consumer>, 22 | ) = spsc_make(128); 23 | let (read_result_sender, read_result_receiver): ( 24 | Producer>, 25 | Consumer>, 26 | ) = spsc_make(128); 27 | 28 | let signal = Arc::new(Signal::new()); 29 | let handle_signal = signal.clone(); 30 | 31 | let handle = thread::spawn(move || loop { 32 | while let Some(mut request) = read_request_receiver.try_pop() { 33 | for asset in &mut request.assets { 34 | read(asset); 35 | } 36 | read_result_sender.push(request); 37 | } 38 | handle_signal.wait(); 39 | }); 40 | 41 | AssetState { 42 | handle, 43 | signal, 44 | pending: Vec::new(), 45 | read_request_sender, 46 | read_result_receiver, 47 | } 48 | } 49 | 50 | fn read(&mut self, request: AssetRequest) { 51 | if let Some(path) = self.read_request_sender.try_push(request) { 52 | self.pending.push(path); 53 | } else { 54 | self.signal.notify(); 55 | } 56 | } 57 | 58 | fn next(&mut self) -> Option> { 59 | // Send the backlog to be read. 60 | while let Some(path) = self.pending.pop() { 61 | if let Some(path) = self.read_request_sender.try_push(path) { 62 | self.pending.push(path); 63 | break; 64 | } else { 65 | self.signal.notify(); 66 | } 67 | } 68 | // Process the finished requests. 69 | self.read_result_receiver.try_pop() 70 | } 71 | } 72 | 73 | /// Relative to the current working directory. 74 | fn read(asset: &mut Asset) { 75 | let mut file = match File::open(&asset.relative_path) { 76 | Ok(file) => file, 77 | Err(error) => { 78 | asset.result = Err(error.kind().into()); 79 | return; 80 | } 81 | }; 82 | let mut contents = Vec::new(); 83 | if let Err(error) = file.read_to_end(&mut contents) { 84 | asset.result = Err(error.kind().into()); 85 | return; 86 | } 87 | asset.result = Ok(contents); 88 | } 89 | 90 | impl From for LoaderError { 91 | fn from(error: io::ErrorKind) -> Self { 92 | match error { 93 | io::ErrorKind::NotFound => LoaderError::NotFound, 94 | io::ErrorKind::PermissionDenied => LoaderError::PermissionDenied, 95 | io::ErrorKind::ConnectionRefused => LoaderError::ConnectionRefused, 96 | io::ErrorKind::ConnectionReset => LoaderError::ConnectionReset, 97 | io::ErrorKind::ConnectionAborted => LoaderError::ConnectionAborted, 98 | io::ErrorKind::NotConnected => LoaderError::NotConnected, 99 | io::ErrorKind::AddrInUse => LoaderError::AddrInUse, 100 | io::ErrorKind::AddrNotAvailable => LoaderError::AddrNotAvailable, 101 | io::ErrorKind::BrokenPipe => LoaderError::BrokenPipe, 102 | io::ErrorKind::AlreadyExists => LoaderError::AlreadyExists, 103 | io::ErrorKind::WouldBlock => LoaderError::WouldBlock, 104 | io::ErrorKind::InvalidInput => LoaderError::InvalidInput, 105 | io::ErrorKind::InvalidData => LoaderError::InvalidData, 106 | io::ErrorKind::TimedOut => LoaderError::TimedOut, 107 | io::ErrorKind::WriteZero => LoaderError::WriteZero, 108 | io::ErrorKind::Interrupted => LoaderError::Interrupted, 109 | io::ErrorKind::Unsupported => LoaderError::Unsupported, 110 | io::ErrorKind::UnexpectedEof => LoaderError::UnexpectedEof, 111 | io::ErrorKind::OutOfMemory => LoaderError::OutOfMemory, 112 | _ => LoaderError::Other, 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/asset/platform/wasm.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use crate::asset::{AssetRequest, AssetStateContract, LoaderError}; 4 | use crate::App; 5 | use alloc::vec::Vec; 6 | use hashbrown::HashMap; 7 | use js_sys::{Array, Uint8Array}; 8 | use wasm_bindgen::prelude::wasm_bindgen; 9 | use wasm_bindgen::{closure::Closure, JsCast, JsValue}; 10 | 11 | #[wasm_bindgen(raw_module = "./storm.js")] 12 | extern "C" { 13 | fn fs_load_files(index: usize, paths: Array); 14 | fn fs_init_callback(callback: &Closure); 15 | } 16 | 17 | thread_local! { 18 | static STORM_ASSET_FINISHED: RefCell> = RefCell::new(Vec::new()); 19 | } 20 | 21 | pub(crate) struct AssetState { 22 | count: usize, 23 | pending: HashMap>, 24 | } 25 | 26 | impl AssetStateContract for AssetState { 27 | fn init() -> Self { 28 | let closure = Closure::new(|slot_key: usize, responses: Array| { 29 | STORM_ASSET_FINISHED.with_borrow_mut(|finished| { 30 | finished.push((slot_key, responses)); 31 | }); 32 | }); 33 | fs_init_callback(&closure); 34 | closure.forget(); 35 | 36 | AssetState { 37 | count: 0, 38 | pending: HashMap::with_capacity(16), 39 | } 40 | } 41 | 42 | fn read(&mut self, request: AssetRequest) { 43 | let paths = Array::new(); 44 | for asset in &request.assets { 45 | paths.push(&JsValue::from_str(&asset.relative_path)); 46 | } 47 | self.pending.insert(self.count, request); 48 | fs_load_files(self.count, paths); 49 | self.count += 1; 50 | } 51 | 52 | fn next(&mut self) -> Option> { 53 | STORM_ASSET_FINISHED.with_borrow_mut(|pending| match pending.pop() { 54 | Some((slot_key, responses)) => { 55 | let slot_request = self.pending.remove(&slot_key); 56 | match slot_request { 57 | Some(mut request) => { 58 | for (index, response) in responses.iter().enumerate() { 59 | if response.is_object() { 60 | let contents: Uint8Array = response.dyn_into().unwrap(); 61 | let contents = contents.to_vec(); 62 | request.assets[index].result = Ok(contents); 63 | } else { 64 | let status = response.as_f64().unwrap() as u32; 65 | let status = match status { 66 | 400 => LoaderError::InvalidInput, 67 | 401 | 402 | 403 => LoaderError::PermissionDenied, 68 | 404 => LoaderError::NotFound, 69 | _ => LoaderError::Other, 70 | }; 71 | request.assets[index].result = Err(status); 72 | } 73 | } 74 | return Some(request); 75 | } 76 | None => panic!( 77 | "Got asset results from Javascript, but no pending Rust request. Key [{}]", 78 | slot_key 79 | ), 80 | } 81 | } 82 | _ => None, 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/audio/control.rs: -------------------------------------------------------------------------------- 1 | use alloc::sync::Arc; 2 | use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 3 | 4 | struct Inner { 5 | volume: AtomicU64, 6 | paused: AtomicBool, 7 | stop: AtomicBool, 8 | } 9 | 10 | /// Various controls for managing an active sound. 11 | #[repr(transparent)] 12 | #[derive(Clone)] 13 | pub struct SoundControl(Arc); 14 | 15 | impl SoundControl { 16 | pub(crate) fn new(volume: f32, smooth: f32, paused: bool) -> SoundControl { 17 | let volume = pack_volume(volume, smooth); 18 | SoundControl(Arc::new(Inner { 19 | volume: AtomicU64::new(volume), 20 | paused: AtomicBool::new(paused), 21 | stop: AtomicBool::new(false), 22 | })) 23 | } 24 | 25 | /// Sets the sound's volume. 26 | /// # Arguments 27 | /// 28 | /// * `volume` - A value between `[0, 1]`, where 0 is muted, and 1 is the sound's original volume. 29 | /// * `smooth` - The duration in seconds to fade the change in volume from the current value to 30 | /// the given value. 31 | pub fn set_volume(&self, volume: f32, smooth: f32) { 32 | let volume = pack_volume(volume, smooth); 33 | self.0.volume.store(volume, Ordering::Relaxed) 34 | } 35 | 36 | /// Pauses the sound. The sound can later be resumed. 37 | pub fn pause(&self) { 38 | self.0.paused.store(true, Ordering::Relaxed); 39 | } 40 | 41 | /// Resumes the sound. Only a paused sound can be resumed. 42 | pub fn resume(&self) { 43 | self.0.paused.store(false, Ordering::Relaxed); 44 | } 45 | 46 | /// Stops the sound. This action is irreversible. 47 | pub fn stop(&self) { 48 | self.0.stop.store(true, Ordering::Relaxed); 49 | } 50 | 51 | pub(crate) fn load_volume(&self) -> (f32, f32) { 52 | unpack_volume(self.0.volume.load(Ordering::Relaxed)) 53 | } 54 | 55 | pub(crate) fn load_paused(&self) -> bool { 56 | self.0.paused.load(Ordering::Relaxed) 57 | } 58 | 59 | /// Returns if the sound is stopped. If the sound was manually stopped or finished playing, it 60 | /// will be marked as stopped. 61 | pub fn is_stopped(&self) -> bool { 62 | self.0.stop.load(Ordering::Relaxed) 63 | } 64 | } 65 | 66 | fn pack_volume(volume: f32, smooth: f32) -> u64 { 67 | let volume = if volume < 0.0 { 68 | 0.0 69 | } else if volume > 1.0 { 70 | 1.0 71 | } else { 72 | volume 73 | }; 74 | let smooth = if smooth < 0.01 { 75 | 0.01 76 | } else { 77 | smooth 78 | }; 79 | ((volume.to_bits() as u64) << 32) | smooth.to_bits() as u64 80 | } 81 | 82 | fn unpack_volume(packed: u64) -> (f32, f32) { 83 | let volume = f32::from_bits((packed >> 32) as u32); 84 | let smooth = f32::from_bits(packed as u32); 85 | (volume, smooth) 86 | } 87 | -------------------------------------------------------------------------------- /src/audio/instance.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::{Sound, SoundControl}; 2 | use crate::math::Interpolation; 3 | 4 | pub struct SoundInstance { 5 | control: SoundControl, 6 | source: Sound, 7 | volume: Interpolation, 8 | paused: bool, 9 | smooth: f32, 10 | time: f64, 11 | } 12 | 13 | impl SoundInstance { 14 | pub fn new(sound: &Sound, control: &SoundControl) -> SoundInstance { 15 | let (volume, smooth) = control.load_volume(); 16 | let paused = control.load_paused(); 17 | SoundInstance { 18 | control: control.clone(), 19 | source: sound.clone(), 20 | volume: Interpolation::new(0.0, volume), 21 | paused, 22 | smooth: smooth, 23 | time: 0.0, 24 | } 25 | } 26 | 27 | pub fn control(&mut self) -> &mut SoundControl { 28 | &mut self.control 29 | } 30 | 31 | pub fn mix(&mut self, interval: f32, out: &mut [[f32; 2]]) -> bool { 32 | // Stopping the sound. 33 | if self.control.is_stopped() { 34 | return true; 35 | } 36 | 37 | // Sync volume. 38 | let (volume, smooth) = self.control.load_volume(); 39 | if volume != self.volume.end() || smooth != self.smooth { 40 | self.volume.update(volume); 41 | self.smooth = 1.0 / smooth; 42 | } 43 | 44 | // Current and next state are paused. 45 | let paused = self.control.load_paused(); 46 | if self.paused && paused { 47 | return false; 48 | } 49 | 50 | let mut sample = self.time * self.source.sample_rate(); 51 | let rate = (interval as f64) * self.source.sample_rate(); 52 | 53 | if self.paused != paused { 54 | let (start, step) = if paused { 55 | let start = self.volume.get(); 56 | let step = -start / (out.len() as f32); 57 | (start, step) 58 | } else { 59 | let start = 0.0; 60 | let step = self.volume.get() / (out.len() as f32); 61 | (start, step) 62 | }; 63 | for (index, target) in out.iter_mut().enumerate() { 64 | let index = index as f32; 65 | let amplitude = (start + step * index).perceptual(); 66 | self.source.mix(sample, amplitude, target); 67 | sample += rate; 68 | } 69 | self.paused = paused; 70 | } else if self.volume.progress() == 1.0 { 71 | let amplitude = self.volume.get().perceptual(); 72 | for target in out.iter_mut() { 73 | self.source.mix(sample, amplitude, target); 74 | sample += rate; 75 | } 76 | } else { 77 | let progress = interval / self.smooth; 78 | for target in out.iter_mut() { 79 | let amplitude = self.volume.get().perceptual(); 80 | self.source.mix(sample, amplitude, target); 81 | self.volume.advance(progress); 82 | sample += rate; 83 | } 84 | } 85 | 86 | self.time += (interval as f64) * (out.len() as f64); 87 | self.time >= self.source.duration() 88 | } 89 | } 90 | 91 | trait Perceptual { 92 | fn perceptual(&self) -> Self; 93 | } 94 | 95 | impl Perceptual for f32 { 96 | fn perceptual(&self) -> Self { 97 | self * self 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/audio/mixer.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::SoundInstance; 2 | use crate::sync::Consumer; 3 | use alloc::vec::Vec; 4 | 5 | pub struct Mixer { 6 | receiver: Consumer, 7 | active: Vec, 8 | sample_interval: f32, 9 | } 10 | 11 | impl Mixer { 12 | pub fn new(sample_rate: u32, receiver: Consumer) -> Mixer { 13 | Mixer { 14 | receiver, 15 | active: Vec::with_capacity(32), 16 | sample_interval: 1.0 / sample_rate as f32, 17 | } 18 | } 19 | 20 | pub fn sample(&mut self, out: &mut [[f32; 2]]) { 21 | while let Some(instance) = self.receiver.try_pop() { 22 | self.active.push(instance); 23 | } 24 | 25 | for target in out.iter_mut() { 26 | *target = [0.0, 0.0]; 27 | } 28 | 29 | let mut index = 0; 30 | while index < self.active.len() { 31 | let instance = &mut self.active[index]; 32 | if instance.mix(self.sample_interval, out) { 33 | let mut instance = self.active.swap_remove(index); 34 | instance.control().stop(); 35 | } else { 36 | index += 1; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/audio/mod.rs: -------------------------------------------------------------------------------- 1 | mod control; 2 | mod instance; 3 | mod mixer; 4 | mod sound; 5 | mod state; 6 | 7 | pub use self::control::SoundControl; 8 | pub use self::sound::{Sound, SoundError}; 9 | 10 | pub(crate) use self::instance::SoundInstance; 11 | pub(crate) use self::mixer::Mixer; 12 | pub(crate) use self::state::{audio, AudioState}; 13 | -------------------------------------------------------------------------------- /src/audio/sound.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::{audio, SoundControl, SoundInstance}; 2 | use crate::math::lerp; 3 | use crate::{App, Context}; 4 | use alloc::{sync::Arc, vec::Vec}; 5 | 6 | #[derive(Copy, Clone, Debug)] 7 | /// An error that prevents successful decoding of an audio stream. 8 | pub enum SoundError { 9 | /// The channel count is unsupported. Only mono and stero sounds are supported. 10 | UnsupportedChannelCount, 11 | /// A feature in the audio file isn't supported by the parser. 12 | UnsupportedFeature, 13 | /// The audio file not formatted correctly for the encoding. 14 | InvalidFormat, 15 | } 16 | 17 | /// Basic audio container. 18 | #[derive(Clone)] 19 | pub struct Sound { 20 | sample_rate: f64, 21 | duration: f64, 22 | samples: Arc<[[f32; 2]]>, 23 | } 24 | 25 | impl Sound { 26 | /// Attempts to decode FLAC, Ogg Vorbis, WAV, or ALAC into a sound. 27 | pub fn from_bytes(bytes: &[u8]) -> Result { 28 | let mut audio_stream = { 29 | let file = std::io::Cursor::new(bytes); 30 | audrey::Reader::new(file).unwrap() 31 | }; 32 | let description = audio_stream.description(); 33 | let reader = audio_stream.samples::().map(std::result::Result::unwrap); 34 | let mut buffer = Vec::with_capacity(description.sample_rate() as usize); 35 | match description.channel_count() { 36 | 1 => { 37 | for x in reader { 38 | buffer.push([x, x]); 39 | } 40 | } 41 | 2 => { 42 | let mut iter = reader; 43 | while let Some(x) = iter.next() { 44 | let y = iter.next().unwrap() as f32; 45 | buffer.push([x, y]); 46 | } 47 | } 48 | _ => return Err(SoundError::UnsupportedChannelCount), 49 | } 50 | Sound::new(description.sample_rate(), buffer) 51 | } 52 | 53 | /// Creates a new sound from a slice of stereo samples. 54 | pub fn new(sample_rate: u32, samples: Vec<[f32; 2]>) -> Result { 55 | let sample_rate = sample_rate as f64; 56 | Ok(Sound { 57 | sample_rate, 58 | duration: samples.len() as f64 / sample_rate, 59 | samples: samples.into(), 60 | }) 61 | } 62 | 63 | /// The duration of the sound in seconds. 64 | pub fn duration(&self) -> f64 { 65 | self.duration 66 | } 67 | 68 | /// The sample rate of the sound. 69 | pub fn sample_rate(&self) -> f64 { 70 | self.sample_rate 71 | } 72 | 73 | /// Plays a sound with a given volume. 74 | /// # Arguments 75 | /// 76 | /// * `volume` - A value between `[0, 1]`, where 0 is muted, and 1 is the sound's original volume. 77 | /// * `smooth` - The duration in seconds to fade the change in volume from the current value to 78 | /// the given value. Sounds start at a volume of 0.0 when first played to prevent popping. 79 | /// # Returns 80 | /// 81 | /// * `SoundControl` - A handle to control sound properties during play. 82 | pub fn play(&self, _ctx: &Context, volume: f32, smooth: f32) -> SoundControl { 83 | let control = SoundControl::new(volume, smooth, false); 84 | let instance = SoundInstance::new(self, &control); 85 | audio().push_sound(instance); 86 | control 87 | } 88 | 89 | pub(crate) fn mix(&self, sample: f64, amplitude: f32, out: &mut [f32; 2]) { 90 | if sample < 0.0 { 91 | return; 92 | } 93 | let trunc = sample.trunc(); 94 | let whole = trunc as usize; 95 | if whole + 1 >= self.samples.len() { 96 | return; 97 | } 98 | let t = (sample - trunc) as f32; 99 | let a = unsafe { self.samples.get_unchecked(whole) }; 100 | let b = unsafe { self.samples.get_unchecked(whole + 1) }; 101 | out[0] += lerp(a[0], b[0], t) * amplitude; 102 | out[1] += lerp(a[1], b[1], t) * amplitude; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/audio/state.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::{Mixer, SoundInstance}; 2 | use crate::sync::{make as spsc_make, Producer}; 3 | use core::mem::MaybeUninit; 4 | use core::sync::atomic::{AtomicBool, Ordering}; 5 | use cpal::{ 6 | traits::{DeviceTrait, HostTrait, StreamTrait}, 7 | Stream, 8 | }; 9 | 10 | #[no_mangle] 11 | static mut _STORM_AUDIO_INITIALIZED: AtomicBool = AtomicBool::new(false); 12 | #[no_mangle] 13 | static mut _STORM_AUDIO: MaybeUninit = MaybeUninit::::uninit(); 14 | 15 | pub(crate) fn audio() -> &'static mut AudioState { 16 | unsafe { _STORM_AUDIO.assume_init_mut() } 17 | } 18 | 19 | pub(crate) struct AudioState { 20 | sender: Producer, 21 | _stream: Stream, 22 | } 23 | 24 | impl AudioState { 25 | pub(crate) fn init() { 26 | if unsafe { _STORM_AUDIO_INITIALIZED.swap(true, Ordering::Relaxed) } { 27 | panic!("Audio has already initialized."); 28 | } 29 | 30 | let host = cpal::default_host(); 31 | let device = host.default_output_device().expect("no output device available"); 32 | let sample_rate = device.default_output_config().unwrap().sample_rate(); 33 | let config = cpal::StreamConfig { 34 | channels: 2, 35 | sample_rate, 36 | buffer_size: cpal::BufferSize::Default, 37 | }; 38 | let (sender, receiver) = spsc_make(256); 39 | let mut mixer = Mixer::new(sample_rate.0, receiver); 40 | 41 | let stream = device 42 | .build_output_stream( 43 | &config, 44 | move |out_flat: &mut [f32], _: &cpal::OutputCallbackInfo| { 45 | mixer.sample(as_stereo(out_flat)); 46 | }, 47 | move |err| { 48 | log::error!("{}", err); 49 | }, 50 | None, 51 | ) 52 | .unwrap(); 53 | stream.play().unwrap(); 54 | 55 | unsafe { 56 | _STORM_AUDIO.write(AudioState { 57 | sender, 58 | _stream: stream, 59 | }) 60 | }; 61 | } 62 | 63 | pub(crate) fn push_sound(&mut self, instance: SoundInstance) { 64 | self.sender.push(instance); 65 | } 66 | } 67 | 68 | fn as_stereo(xs: &mut [f32]) -> &mut [[f32; 2]] { 69 | unsafe { core::slice::from_raw_parts_mut(xs.as_mut_ptr() as _, xs.len() / 2) } 70 | } 71 | -------------------------------------------------------------------------------- /src/color/mod.rs: -------------------------------------------------------------------------------- 1 | mod r8; 2 | mod rg8; 3 | mod rgb8; 4 | mod rgba8; 5 | 6 | pub use r8::R8; 7 | pub use rg8::RG8; 8 | pub use rgb8::RGB8; 9 | pub use rgba8::RGBA8; 10 | 11 | use crate::graphics::{PixelFormat, PixelInternalFormat, PixelType}; 12 | 13 | /// A trait to describe size and layout of color components. 14 | /// 15 | /// # Example 16 | /// ``` 17 | /// // This is an example for how to implement ColorDescriptor for a simple type. 18 | /// use storm::color::*; 19 | /// 20 | /// #[repr(C)] 21 | /// #[derive(Copy, Clone)] 22 | /// pub struct BGRA8 { 23 | /// pub b: u8, 24 | /// pub g: u8, 25 | /// pub r: u8, 26 | /// pub a: u8, 27 | /// } 28 | /// 29 | /// // This allows for bytes to represent single channel resources. 30 | /// impl ColorDescriptor for BGRA8 { 31 | /// fn component_type() -> ColorComponentType { 32 | /// ColorComponentType::U8 33 | /// } 34 | /// fn layout() -> ColorLayoutFormat { 35 | /// ColorLayoutFormat::BGRA 36 | /// } 37 | /// } 38 | /// ``` 39 | pub trait ColorDescriptor: Sized + Copy + Default { 40 | /// Gets the component type of the color. 41 | fn component_type() -> ColorComponentType; 42 | 43 | /// Gets the layout of the color. 44 | fn layout() -> ColorLayoutFormat; 45 | } 46 | 47 | /// Represents the type of each color component. 48 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 49 | #[repr(u32)] 50 | pub enum ColorComponentType { 51 | U8 = PixelType::UnsignedByte as u32, 52 | F32 = PixelType::Float as u32, 53 | } 54 | 55 | impl ColorComponentType { 56 | pub(crate) fn pixel_type(&self) -> PixelType { 57 | unsafe { core::mem::transmute(*self) } 58 | } 59 | } 60 | 61 | /// Represents the layout of the color components. 62 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 63 | #[repr(u32)] 64 | pub enum ColorLayoutFormat { 65 | R = PixelFormat::RED as u32, 66 | RG = PixelFormat::RG as u32, 67 | RGB = PixelFormat::RGB as u32, 68 | RGBA = PixelFormat::RGBA as u32, 69 | BGRA = PixelFormat::BGRA as u32, 70 | } 71 | 72 | impl ColorLayoutFormat { 73 | pub(crate) fn gpu_format(&self) -> PixelInternalFormat { 74 | match self { 75 | ColorLayoutFormat::R => PixelInternalFormat::R8, 76 | ColorLayoutFormat::RG => PixelInternalFormat::RG8, 77 | ColorLayoutFormat::RGB => PixelInternalFormat::RGB8, 78 | ColorLayoutFormat::RGBA => PixelInternalFormat::RGBA8, 79 | ColorLayoutFormat::BGRA => PixelInternalFormat::RGBA8, 80 | } 81 | } 82 | 83 | pub(crate) fn cpu_format(&self) -> PixelFormat { 84 | unsafe { core::mem::transmute(*self) } 85 | } 86 | } 87 | 88 | impl ColorDescriptor for u8 { 89 | fn component_type() -> ColorComponentType { 90 | ColorComponentType::U8 91 | } 92 | fn layout() -> ColorLayoutFormat { 93 | ColorLayoutFormat::R 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/color/r8.rs: -------------------------------------------------------------------------------- 1 | use super::{ColorComponentType, ColorDescriptor, ColorLayoutFormat}; 2 | 3 | /// Simple R8 color type to represent colors. 4 | #[repr(C)] 5 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 6 | pub struct R8 { 7 | /// Represents the red color channel. 8 | pub r: u8, 9 | } 10 | 11 | impl R8 { 12 | pub const fn new(red: u8) -> R8 { 13 | R8 { 14 | r: red, 15 | } 16 | } 17 | 18 | /// Helper function to create this color from an f32. 19 | pub fn from_f32(red: f32) -> R8 { 20 | R8 { 21 | r: (red * 255.0) as u8, 22 | } 23 | } 24 | } 25 | 26 | impl From for f32 { 27 | fn from(x: R8) -> Self { 28 | (x.r as f32) / 255.0 29 | } 30 | } 31 | 32 | impl From for R8 { 33 | fn from(r: f32) -> Self { 34 | Self::from_f32(r) 35 | } 36 | } 37 | 38 | impl Default for R8 { 39 | fn default() -> Self { 40 | Self { 41 | r: 255, 42 | } 43 | } 44 | } 45 | 46 | impl ColorDescriptor for R8 { 47 | fn component_type() -> ColorComponentType { 48 | ColorComponentType::U8 49 | } 50 | fn layout() -> ColorLayoutFormat { 51 | ColorLayoutFormat::R 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/color/rg8.rs: -------------------------------------------------------------------------------- 1 | use super::{ColorComponentType, ColorDescriptor, ColorLayoutFormat}; 2 | 3 | /// Simple RG8 color type to represent colors. 4 | #[repr(C)] 5 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 6 | pub struct RG8 { 7 | /// Represents the red color channel. 8 | pub r: u8, 9 | /// Represents the green color channel. 10 | pub g: u8, 11 | } 12 | 13 | impl RG8 { 14 | pub const fn new(red: u8, green: u8) -> RG8 { 15 | RG8 { 16 | r: red, 17 | g: green, 18 | } 19 | } 20 | 21 | /// Helper function to create this color from f32s. 22 | pub fn from_f32(red: f32, green: f32) -> RG8 { 23 | RG8 { 24 | r: (red * 255.0) as u8, 25 | g: (green * 255.0) as u8, 26 | } 27 | } 28 | } 29 | 30 | impl From for (f32, f32) { 31 | fn from(x: RG8) -> Self { 32 | let r = (x.r as f32) / 255.0; 33 | let g = (x.g as f32) / 255.0; 34 | (r, g) 35 | } 36 | } 37 | 38 | impl From for [f32; 2] { 39 | fn from(x: RG8) -> Self { 40 | let r = (x.r as f32) / 255.0; 41 | let g = (x.g as f32) / 255.0; 42 | [r, g] 43 | } 44 | } 45 | 46 | impl From<(f32, f32)> for RG8 { 47 | fn from(x: (f32, f32)) -> Self { 48 | let (r, g) = x; 49 | Self::from_f32(r, g) 50 | } 51 | } 52 | 53 | impl From<[f32; 2]> for RG8 { 54 | fn from(x: [f32; 2]) -> Self { 55 | let [r, g] = x; 56 | Self::from_f32(r, g) 57 | } 58 | } 59 | 60 | impl Default for RG8 { 61 | fn default() -> Self { 62 | Self { 63 | r: 255, 64 | g: 255, 65 | } 66 | } 67 | } 68 | 69 | impl ColorDescriptor for RG8 { 70 | fn component_type() -> ColorComponentType { 71 | ColorComponentType::U8 72 | } 73 | fn layout() -> ColorLayoutFormat { 74 | ColorLayoutFormat::RG 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/color/rgb8.rs: -------------------------------------------------------------------------------- 1 | use super::{ColorComponentType, ColorDescriptor, ColorLayoutFormat}; 2 | 3 | /// Simple RGB8 color type to represent colors. 4 | #[repr(C)] 5 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 6 | pub struct RGB8 { 7 | /// Represents the red color channel. 8 | pub r: u8, 9 | /// Represents the green color channel. 10 | pub g: u8, 11 | /// Represents the blue color channel. 12 | pub b: u8, 13 | } 14 | 15 | impl RGB8 { 16 | pub const RED: RGB8 = RGB8::new(255, 0, 0); 17 | pub const PURPLE: RGB8 = RGB8::new(128, 0, 128); 18 | pub const BLUE: RGB8 = RGB8::new(0, 0, 255); 19 | pub const GREEN: RGB8 = RGB8::new(0, 255, 0); 20 | pub const YELLOW: RGB8 = RGB8::new(255, 255, 0); 21 | pub const ORANGE: RGB8 = RGB8::new(255, 164, 0); 22 | pub const MAGENTA: RGB8 = RGB8::new(255, 0, 255); 23 | pub const WHITE: RGB8 = RGB8::new(255, 255, 255); 24 | pub const BLACK: RGB8 = RGB8::new(0, 0, 0); 25 | 26 | pub const fn new(red: u8, green: u8, blue: u8) -> RGB8 { 27 | RGB8 { 28 | r: red, 29 | g: green, 30 | b: blue, 31 | } 32 | } 33 | 34 | /// Helper function to create this color from f32s. 35 | pub fn from_f32(red: f32, green: f32, blue: f32) -> RGB8 { 36 | RGB8 { 37 | r: (red * 255.0) as u8, 38 | g: (green * 255.0) as u8, 39 | b: (blue * 255.0) as u8, 40 | } 41 | } 42 | } 43 | 44 | impl From for (f32, f32, f32) { 45 | fn from(x: RGB8) -> Self { 46 | let r = (x.r as f32) / 255.0; 47 | let g = (x.g as f32) / 255.0; 48 | let b = (x.b as f32) / 255.0; 49 | (r, g, b) 50 | } 51 | } 52 | 53 | impl From for [f32; 3] { 54 | fn from(x: RGB8) -> Self { 55 | let r = (x.r as f32) / 255.0; 56 | let g = (x.g as f32) / 255.0; 57 | let b = (x.b as f32) / 255.0; 58 | [r, g, b] 59 | } 60 | } 61 | 62 | impl From<(f32, f32, f32)> for RGB8 { 63 | fn from(x: (f32, f32, f32)) -> Self { 64 | let (r, g, b) = x; 65 | Self::from_f32(r, g, b) 66 | } 67 | } 68 | 69 | impl From<[f32; 3]> for RGB8 { 70 | fn from(x: [f32; 3]) -> Self { 71 | let [r, g, b] = x; 72 | Self::from_f32(r, g, b) 73 | } 74 | } 75 | 76 | impl Default for RGB8 { 77 | fn default() -> Self { 78 | Self { 79 | r: 255, 80 | g: 255, 81 | b: 255, 82 | } 83 | } 84 | } 85 | 86 | impl ColorDescriptor for RGB8 { 87 | fn component_type() -> ColorComponentType { 88 | ColorComponentType::U8 89 | } 90 | fn layout() -> ColorLayoutFormat { 91 | ColorLayoutFormat::RGB 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/color/rgba8.rs: -------------------------------------------------------------------------------- 1 | use super::{ColorComponentType, ColorDescriptor, ColorLayoutFormat}; 2 | 3 | /// Simple RGBA8 color type to represent colors. 4 | #[repr(C)] 5 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 6 | pub struct RGBA8 { 7 | /// Represents the red color channel. 8 | pub r: u8, 9 | /// Represents the green color channel. 10 | pub g: u8, 11 | /// Represents the blue color channel. 12 | pub b: u8, 13 | /// Represents the alpha color channel. 14 | pub a: u8, 15 | } 16 | 17 | impl RGBA8 { 18 | pub const RED: RGBA8 = RGBA8::new(255, 0, 0, 255); 19 | pub const PURPLE: RGBA8 = RGBA8::new(128, 0, 128, 255); 20 | pub const BLUE: RGBA8 = RGBA8::new(0, 0, 255, 255); 21 | pub const GREEN: RGBA8 = RGBA8::new(0, 255, 0, 255); 22 | pub const YELLOW: RGBA8 = RGBA8::new(255, 255, 0, 255); 23 | pub const ORANGE: RGBA8 = RGBA8::new(255, 164, 0, 255); 24 | pub const MAGENTA: RGBA8 = RGBA8::new(255, 0, 255, 255); 25 | pub const WHITE: RGBA8 = RGBA8::new(255, 255, 255, 255); 26 | pub const BLACK: RGBA8 = RGBA8::new(0, 0, 0, 255); 27 | pub const TRANSPARENT: RGBA8 = RGBA8::new(0, 0, 0, 0); 28 | 29 | pub const fn new(red: u8, green: u8, blue: u8, alpha: u8) -> RGBA8 { 30 | RGBA8 { 31 | r: red, 32 | g: green, 33 | b: blue, 34 | a: alpha, 35 | } 36 | } 37 | 38 | /// Helper function to create this color from f32s. 39 | pub fn from_f32(red: f32, green: f32, blue: f32, alpha: f32) -> RGBA8 { 40 | RGBA8 { 41 | r: (red * 255.0) as u8, 42 | g: (green * 255.0) as u8, 43 | b: (blue * 255.0) as u8, 44 | a: (alpha * 255.0) as u8, 45 | } 46 | } 47 | } 48 | 49 | impl From for (f32, f32, f32, f32) { 50 | fn from(x: RGBA8) -> Self { 51 | let r = (x.r as f32) / 255.0; 52 | let g = (x.g as f32) / 255.0; 53 | let b = (x.b as f32) / 255.0; 54 | let a = (x.a as f32) / 255.0; 55 | (r, g, b, a) 56 | } 57 | } 58 | 59 | impl From for [f32; 4] { 60 | fn from(x: RGBA8) -> Self { 61 | let r = (x.r as f32) / 255.0; 62 | let g = (x.g as f32) / 255.0; 63 | let b = (x.b as f32) / 255.0; 64 | let a = (x.a as f32) / 255.0; 65 | [r, g, b, a] 66 | } 67 | } 68 | 69 | impl From<(f32, f32, f32, f32)> for RGBA8 { 70 | fn from(x: (f32, f32, f32, f32)) -> Self { 71 | let (r, g, b, a) = x; 72 | Self::from_f32(r, g, b, a) 73 | } 74 | } 75 | 76 | impl From<[f32; 4]> for RGBA8 { 77 | fn from(x: [f32; 4]) -> Self { 78 | let [r, g, b, a] = x; 79 | Self::from_f32(r, g, b, a) 80 | } 81 | } 82 | 83 | impl Default for RGBA8 { 84 | fn default() -> Self { 85 | Self { 86 | r: 255, 87 | g: 255, 88 | b: 255, 89 | a: 255, 90 | } 91 | } 92 | } 93 | 94 | impl ColorDescriptor for RGBA8 { 95 | fn component_type() -> ColorComponentType { 96 | ColorComponentType::U8 97 | } 98 | fn layout() -> ColorLayoutFormat { 99 | ColorLayoutFormat::RGBA 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/engine.rs: -------------------------------------------------------------------------------- 1 | use crate::asset::{AssetState, AssetStateContract}; 2 | use crate::audio::AudioState; 3 | use crate::event::EventConverter; 4 | use crate::graphics::{graphics, OpenGLState, OpenGLWindowContract, WindowSettings}; 5 | use crate::time::Instant; 6 | use crate::App; 7 | use core::sync::atomic::{AtomicBool, Ordering}; 8 | use core::time::Duration; 9 | use log::info; 10 | use winit::event::Event as WinitEvent; 11 | use winit::event_loop::ControlFlow; 12 | 13 | #[no_mangle] 14 | static mut INITIALIZED: AtomicBool = AtomicBool::new(false); 15 | 16 | /// The main entry point into the engine context. All state is initialized by this type. 17 | pub struct Context { 18 | // Global states 19 | assets: AssetState, 20 | // Context state 21 | stop: bool, 22 | control_flow: Option, 23 | last_update: Instant, 24 | wait_next: Instant, 25 | wait_periodic: Option, 26 | } 27 | 28 | /// Initializes the context. Graphics, audio, assets, and you app, are initialized by this function. 29 | pub fn start(desc: WindowSettings) -> ! { 30 | if unsafe { INITIALIZED.swap(true, Ordering::Relaxed) } { 31 | panic!("Start has already been called."); 32 | } 33 | 34 | init_logger(A::LOG_LEVEL); 35 | 36 | let event_loop = winit::event_loop::EventLoop::new(); 37 | OpenGLState::init(&desc, &event_loop); 38 | AudioState::init(); 39 | let assets = AssetState::init(); 40 | let mut ctx = Context { 41 | assets, 42 | stop: false, 43 | control_flow: Some(ControlFlow::Poll), 44 | last_update: Instant::now(), 45 | wait_next: Instant::now(), 46 | wait_periodic: None, 47 | }; 48 | let mut input = EventConverter::new(); 49 | let mut app = A::new(&mut ctx); 50 | event_loop.run(move |event, _, control_flow| { 51 | match event { 52 | WinitEvent::DeviceEvent { 53 | .. 54 | } => { 55 | input.push(event, &mut ctx, &mut app); 56 | } 57 | WinitEvent::WindowEvent { 58 | .. 59 | } => { 60 | input.push(event, &mut ctx, &mut app); 61 | } 62 | WinitEvent::MainEventsCleared => { 63 | while let Some(response) = ctx.assets.next() { 64 | response.call(&mut ctx, &mut app); 65 | } 66 | let now = Instant::now(); 67 | if now >= ctx.wait_next { 68 | { 69 | profiling::scope!("storm_update"); 70 | if let Some(duration) = ctx.wait_periodic { 71 | ctx.wait_next += duration; 72 | if ctx.wait_next < now { 73 | ctx.wait_next = now; 74 | } 75 | ctx.control_flow = Some(ControlFlow::WaitUntil(ctx.wait_next)); 76 | } 77 | let delta = now - ctx.last_update; 78 | ctx.last_update = now; 79 | app.on_update(&mut ctx, delta.as_secs_f32()); 80 | } 81 | graphics().window().swap_buffers(); 82 | } 83 | } 84 | WinitEvent::LoopDestroyed => { 85 | ctx.stop = true; 86 | } 87 | _ => {} 88 | } 89 | if ctx.stop { 90 | *control_flow = ControlFlow::Exit; 91 | } else if let Some(next_control_flow) = ctx.control_flow { 92 | *control_flow = next_control_flow; 93 | ctx.control_flow = None; 94 | } 95 | }); 96 | } 97 | 98 | #[cfg(not(target_arch = "wasm32"))] 99 | fn init_logger(level: log::Level) { 100 | use simplelog::*; 101 | 102 | match TermLogger::init( 103 | level.to_level_filter(), 104 | Config::default(), 105 | TerminalMode::Stdout, 106 | ColorChoice::Auto, 107 | ) { 108 | Ok(_) => info!("Using the default logger: simplelog::loggers::termlog."), 109 | Err(_) => info!("Using the provided logger."), 110 | } 111 | } 112 | 113 | #[cfg(target_arch = "wasm32")] 114 | fn init_logger(level: log::Level) { 115 | console_error_panic_hook::set_once(); 116 | match console_log::init_with_level(level) { 117 | Ok(_) => info!("Using the default logger: console_log."), 118 | Err(_) => info!("Using the provided logger."), 119 | } 120 | } 121 | 122 | /// Event loop related functions. 123 | impl Context { 124 | pub(crate) fn assets(&mut self) -> &mut AssetState { 125 | &mut self.assets 126 | } 127 | 128 | /// Stops the context after the next update. 129 | pub fn request_stop(&mut self) { 130 | self.stop = true; 131 | } 132 | 133 | /// Prevents the update event from being sent for at least the duration. If a periodic wait is 134 | /// active, this wait will temporarily override only if it causes the next update event to 135 | /// happen later than the periodic wait would have. 136 | pub fn wait_for(&mut self, duration: Duration) { 137 | self.wait_until(Instant::now() + duration); 138 | } 139 | 140 | /// Prevents the update event from being sent until at least the given instant. If a periodic 141 | /// wait is active, this wait will temporarily override only if it causes the next update event 142 | /// to happen later than the periodic wait would have. 143 | pub fn wait_until(&mut self, instant: Instant) { 144 | if instant > self.wait_next { 145 | self.wait_next = instant; 146 | self.control_flow = Some(ControlFlow::WaitUntil(self.wait_next)); 147 | } 148 | } 149 | 150 | /// Prevents the update event from being sent more frequently than the given duration. Set this 151 | /// to None to disable the periodic wait. 152 | pub fn wait_periodic(&mut self, duration: Option) { 153 | self.wait_periodic = duration; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/event/event.rs: -------------------------------------------------------------------------------- 1 | // Re-exports. 2 | pub use winit::event::MouseButton as CursorButton; 3 | pub use winit::event::VirtualKeyCode as KeyboardButton; 4 | 5 | /// A cursor wheel movement. Some mice have left and right scroll options. 6 | #[derive(Copy, Clone, Debug, PartialEq)] 7 | pub enum ScrollDirection { 8 | /// Cursor wheel scrolled up. 9 | Up, 10 | /// Cursor wheel scrolled down. 11 | Down, 12 | /// Cursor wheel scrolled left. 13 | Left, 14 | /// Cursor wheel scrolled right. 15 | Right, 16 | } 17 | -------------------------------------------------------------------------------- /src/event/mod.rs: -------------------------------------------------------------------------------- 1 | mod converter; 2 | mod event; 3 | 4 | pub use self::event::{CursorButton, KeyboardButton, ScrollDirection}; 5 | 6 | pub(crate) use self::converter::EventConverter; 7 | -------------------------------------------------------------------------------- /src/graphics/blend.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{graphics, BlendEquation, BlendFactor}; 2 | 3 | #[derive(Debug, Copy, Clone, PartialEq)] 4 | pub struct BlendState { 5 | equation: BlendEquation, 6 | sfactor: BlendFactor, 7 | dfactor: BlendFactor, 8 | } 9 | 10 | /// Pixel arithmetic description for blending operations. 11 | /// 12 | /// Specifies how incoming RGBA values 13 | /// (source) and the RGBA in framebuffer (destination) are combined. source_color is the new pixel 14 | /// color and destination color is color from the destination buffer. 15 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 16 | pub enum BlendMode { 17 | /// Adds source and destination. Source and destination are multiplied 18 | /// by blending parameters before addition. 19 | /// 20 | /// Example: `SourceColor * BlendFactor1 + DestinationColor * BlendFactor2` 21 | Add(BlendFactor, BlendFactor), 22 | /// Subtracts destination from source. Source and destination are 23 | /// multiplied by blending parameters before subtraction. 24 | /// 25 | /// Example: `SourceColor * BlendFactor1 - DestinationColor * BlendFactor2` 26 | Subtract(BlendFactor, BlendFactor), 27 | /// Subtracts source from destination. Source and destination are 28 | /// multiplied by blending parameters before subtraction. 29 | /// 30 | /// Example: `DestinationColor * BlendFactor1 - SourceColor * BlendFactor2` 31 | ReverseSubtract(BlendFactor, BlendFactor), 32 | /// Selects the minimum between the source and destination. Min does not use the source or 33 | /// destination factors, only the source and destination colors. 34 | Min, 35 | /// Selects the maximum between the source and destination. Max does not use the source or 36 | /// destination factors, only the source and destination colors. 37 | Max, 38 | } 39 | 40 | impl BlendMode { 41 | pub(crate) fn apply(&self) { 42 | let gl = graphics().gl(); 43 | match self { 44 | BlendMode::Add(src, dst) => { 45 | gl.blend_equation(BlendEquation::Add); 46 | gl.blend_func(*src, *dst); 47 | } 48 | BlendMode::Subtract(src, dst) => { 49 | gl.blend_equation(BlendEquation::Subtract); 50 | gl.blend_func(*src, *dst); 51 | } 52 | BlendMode::ReverseSubtract(src, dst) => { 53 | gl.blend_equation(BlendEquation::ReverseSubtract); 54 | gl.blend_func(*src, *dst); 55 | } 56 | BlendMode::Min => { 57 | gl.blend_equation(BlendEquation::Min); 58 | } 59 | BlendMode::Max => { 60 | gl.blend_equation(BlendEquation::Max); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/graphics/buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{ 2 | configure_vertex, graphics, resource, BufferBindingTarget, BufferUsage, VertexDescriptor, 3 | }; 4 | use crate::{App, Context}; 5 | use core::marker::PhantomData; 6 | 7 | /// Buffers a set of elements on the device. 8 | pub struct Buffer { 9 | // This type is !Send + !Sync. 10 | _unsend: core::marker::PhantomData<*const ()>, 11 | vbo: resource::Buffer, 12 | vao: resource::VertexArray, 13 | vertices: usize, 14 | phantom: PhantomData, 15 | } 16 | 17 | impl Buffer { 18 | /// Creates a new array buffer. 19 | pub fn new(_ctx: &Context) -> Buffer { 20 | let gl = graphics().gl(); 21 | 22 | let vao = gl.create_vertex_array(); 23 | gl.bind_vertex_array(Some(vao)); 24 | let vbo = gl.create_buffer(); 25 | gl.bind_buffer(BufferBindingTarget::ArrayBuffer, Some(vbo)); 26 | configure_vertex::(&T::ATTRIBUTES, gl); 27 | gl.bind_vertex_array(None); 28 | 29 | Buffer { 30 | _unsend: core::marker::PhantomData, 31 | vbo, 32 | vao, 33 | vertices: 0, 34 | phantom: PhantomData, 35 | } 36 | } 37 | 38 | /// Sets the data in the buffer. 39 | pub fn set_data(&mut self, items: &[T]) { 40 | self.vertices = items.len(); 41 | let gl = graphics().gl(); 42 | gl.bind_buffer(BufferBindingTarget::ArrayBuffer, Some(self.vbo)); 43 | gl.buffer_data(BufferBindingTarget::ArrayBuffer, items, BufferUsage::StaticDraw); 44 | } 45 | 46 | /// Perfroms a draw call for the buffer. The most recently bound shader will dictate how this 47 | /// data is processed. 48 | pub fn draw(&self) { 49 | let gl = graphics().gl(); 50 | gl.bind_vertex_array(Some(self.vao)); 51 | if T::INSTANCING.is_instanced() { 52 | gl.draw_arrays_instanced(T::DRAW_MODE, 0, T::INSTANCING.count, self.vertices as i32); 53 | } else { 54 | gl.draw_arrays(T::DRAW_MODE, 0, self.vertices as i32); 55 | } 56 | } 57 | } 58 | 59 | impl Drop for Buffer { 60 | fn drop(&mut self) { 61 | let gl = graphics().gl(); 62 | gl.delete_buffer(self.vbo); 63 | gl.delete_vertex_array(self.vao); 64 | } 65 | } 66 | 67 | impl AsRef> for Buffer { 68 | fn as_ref(&self) -> &Buffer { 69 | self 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/graphics/frame_buffer.rs: -------------------------------------------------------------------------------- 1 | pub struct FrameBuffer {} 2 | 3 | impl FrameBuffer { 4 | /// A manually sized framebuffer. 5 | fn from_size() -> FrameBuffer { 6 | FrameBuffer {} 7 | } 8 | 9 | /// An automatically resizing framebuffer that always matches the screen size. 10 | fn from_screen() -> FrameBuffer { 11 | FrameBuffer {} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/graphics/index_buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{ 2 | configure_vertex, graphics, resource, BufferBindingTarget, BufferUsage, VertexDescriptor, 3 | }; 4 | use crate::{math::UnsignedInteger, App, Context}; 5 | use core::marker::PhantomData; 6 | 7 | /// Buffers a set of elements on the device. 8 | pub struct IndexBuffer { 9 | // This type is !Send + !Sync. 10 | _unsend: core::marker::PhantomData<*const ()>, 11 | vbo: resource::Buffer, 12 | ebo: resource::Buffer, 13 | vao: resource::VertexArray, 14 | vertices: usize, 15 | indices: usize, 16 | phantom: PhantomData<(T, U)>, 17 | } 18 | 19 | impl IndexBuffer { 20 | /// Creates a new array buffer. 21 | pub fn new(_ctx: &Context) -> IndexBuffer { 22 | let gl = graphics().gl(); 23 | 24 | let vao = gl.create_vertex_array(); 25 | gl.bind_vertex_array(Some(vao)); 26 | 27 | let vbo = gl.create_buffer(); 28 | gl.bind_buffer(BufferBindingTarget::ArrayBuffer, Some(vbo)); 29 | configure_vertex::(&T::ATTRIBUTES, gl); 30 | 31 | let ebo = gl.create_buffer(); 32 | gl.bind_buffer(BufferBindingTarget::ElementArrayBuffer, Some(ebo)); 33 | 34 | gl.bind_vertex_array(None); 35 | 36 | IndexBuffer { 37 | _unsend: core::marker::PhantomData, 38 | vbo, 39 | ebo, 40 | vao, 41 | vertices: 0, 42 | indices: 0, 43 | phantom: PhantomData, 44 | } 45 | } 46 | 47 | /// Sets the data in the buffer. 48 | pub fn set_data(&mut self, items: &[T]) { 49 | self.vertices = items.len(); 50 | let gl = graphics().gl(); 51 | gl.bind_buffer(BufferBindingTarget::ArrayBuffer, Some(self.vbo)); 52 | gl.buffer_data(BufferBindingTarget::ArrayBuffer, items, BufferUsage::StaticDraw); 53 | } 54 | 55 | /// Attaches indices to the buffer. 56 | pub fn set_indices(&mut self, indices: &[U]) { 57 | self.indices = indices.len(); 58 | let gl = graphics().gl(); 59 | gl.bind_vertex_array(Some(self.vao)); 60 | gl.buffer_data(BufferBindingTarget::ElementArrayBuffer, indices, BufferUsage::StaticDraw); 61 | } 62 | 63 | /// Perfroms a draw call for the buffer. The most recently bound shader will dictate how this 64 | /// data is processed. 65 | pub fn draw(&self) { 66 | let gl = graphics().gl(); 67 | gl.bind_vertex_array(Some(self.vao)); 68 | if T::INSTANCING.is_instanced() { 69 | gl.draw_elements_instanced(T::DRAW_MODE, U::INDICE_TYPE, self.indices as i32, T::INSTANCING.count) 70 | } else { 71 | gl.draw_elements(T::DRAW_MODE, U::INDICE_TYPE, self.indices as i32) 72 | } 73 | } 74 | } 75 | 76 | impl Drop for IndexBuffer { 77 | fn drop(&mut self) { 78 | let gl = graphics().gl(); 79 | gl.delete_buffer(self.vbo); 80 | gl.delete_buffer(self.ebo); 81 | gl.delete_vertex_array(self.vao); 82 | } 83 | } 84 | 85 | impl AsRef> for IndexBuffer { 86 | fn as_ref(&self) -> &IndexBuffer { 87 | self 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/graphics/mod.rs: -------------------------------------------------------------------------------- 1 | /// Bundled sample shaders for basic sprite and text rendering. 2 | pub mod shaders; 3 | /// Rust struct types that are compatible with the GLSL std140 memory layout. 4 | pub mod std140; 5 | 6 | mod blend; 7 | mod buffer; 8 | mod frame_buffer; 9 | mod index_buffer; 10 | mod opengl; 11 | mod shader; 12 | mod state; 13 | mod texture; 14 | mod texture_atlas; 15 | mod texture_section; 16 | mod uniform; 17 | mod vertex_descriptor; 18 | mod window; 19 | 20 | pub use self::blend::BlendMode; 21 | pub use self::buffer::Buffer; 22 | pub use self::index_buffer::IndexBuffer; 23 | pub use self::opengl::{BlendFactor, ClearMode, DepthTest, DrawMode, IndiceType}; 24 | pub use self::shader::{Shader, ShaderDescription}; 25 | pub use self::texture::{Texture, TextureFiltering}; 26 | pub use self::texture_atlas::TextureAtlas; 27 | pub use self::texture_section::TextureSection; 28 | pub use self::uniform::Uniform; 29 | pub use self::vertex_descriptor::{ 30 | VertexAttribute, VertexDescriptor, VertexInputType, VertexInstancing, VertexOutputType, 31 | }; 32 | pub use self::window::{DisplayMode, Vsync, WindowSettings}; 33 | 34 | pub(crate) use self::opengl::*; 35 | pub(crate) use self::state::{graphics, OpenGLState}; 36 | pub(crate) use self::vertex_descriptor::configure_vertex; 37 | pub(crate) use self::window::{OpenGLWindow, OpenGLWindowContract}; 38 | -------------------------------------------------------------------------------- /src/graphics/shader.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{graphics, resource, Texture, Uniform}; 2 | use crate::{App, Context}; 3 | use alloc::{format, vec::Vec}; 4 | 5 | /// A struct to describe a shader's inputs and outputs so they can be represented without using the 6 | /// heap. 7 | pub struct ShaderDescription<'a> { 8 | pub vertex_shader: &'a str, 9 | pub fragment_shader: &'a str, 10 | pub texture_names: &'a [&'a str], 11 | pub uniform_names: &'a [&'a str], 12 | } 13 | 14 | /// Represents the runtime metadata required to configure and draw with a shader. 15 | pub struct Shader { 16 | // This type is !Send + !Sync. 17 | _unsend: core::marker::PhantomData<*const ()>, 18 | program: resource::Program, 19 | uniform_len: usize, 20 | texture_locations: Vec, 21 | } 22 | 23 | impl Shader { 24 | /// Creates a new shader. Shaders hold no mutable state and should be reused as often as 25 | /// possible. 26 | pub fn new(_ctx: &Context, description: ShaderDescription) -> Shader { 27 | let gl = graphics().gl(); 28 | 29 | let program = gl.shader_program(description.vertex_shader, description.fragment_shader); 30 | 31 | for (i, name) in description.uniform_names.iter().enumerate() { 32 | let idx = gl 33 | .get_uniform_block_index(program, name) 34 | .expect(&format!("Failed to find uniform block named '{}' in your shader.", name)); 35 | gl.uniform_block_binding(program, idx, i as u32); 36 | } 37 | 38 | let texture_locations = description 39 | .texture_names 40 | .iter() 41 | .map(|name| { 42 | gl.get_uniform_location(program, name) 43 | .expect(&format!("Failed to find texture named '{}' in your shader.", name)) 44 | }) 45 | .collect(); 46 | 47 | Shader { 48 | _unsend: core::marker::PhantomData, 49 | program, 50 | uniform_len: description.uniform_names.len(), 51 | texture_locations, 52 | } 53 | } 54 | 55 | /// Binds this shader for future draw calls. The order of uniforms and textures are as they're 56 | /// defined in the `ShaderDescription` used to create this `Shader`. 57 | /// # Arguments 58 | /// 59 | /// * `uniform` - The uniform to use for the shader invocation. 60 | /// * `textures` - The set of textures to use in the fragment shader. 61 | pub fn bind(&self, uniforms: &[&Uniform], textures: &[&Texture]) { 62 | if textures.len() != self.texture_locations.len() { 63 | panic!( 64 | "Textures length ({}) must equal ShaderDescriptor::TEXTURE_NAMES length ({})", 65 | textures.len(), 66 | self.texture_locations.len() 67 | ); 68 | } 69 | if uniforms.len() != self.uniform_len { 70 | panic!( 71 | "Uniforms length ({}) must equal ShaderDescriptor::UNIFORM_NAMES length ({})", 72 | textures.len(), 73 | self.texture_locations.len() 74 | ); 75 | } 76 | 77 | let gl = graphics().gl(); 78 | gl.use_program(Some(self.program)); 79 | 80 | for i in 0..self.uniform_len { 81 | uniforms[i].bind(i as u32); 82 | } 83 | 84 | for i in 0..self.texture_locations.len() { 85 | textures[i].bind(i as u32); 86 | gl.uniform_1_i32(Some(&self.texture_locations[i]), i as i32); 87 | } 88 | } 89 | } 90 | 91 | impl Drop for Shader { 92 | fn drop(&mut self) { 93 | let gl = graphics().gl(); 94 | gl.delete_program(self.program); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/graphics/shaders/mod.rs: -------------------------------------------------------------------------------- 1 | /// Bundled sample shaders for basic sprite rendering. 2 | pub mod sprite; 3 | /// Bundled sample shaders for basic text rendering. 4 | pub mod text; 5 | -------------------------------------------------------------------------------- /src/graphics/shaders/sprite/fragment.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | in vec2 v_uv; 4 | in vec4 v_color; 5 | out vec4 a_color; 6 | 7 | uniform sampler2D tex; 8 | 9 | void main() { 10 | a_color = texture(tex, v_uv) * v_color; 11 | if (a_color.a <= 0.0) { 12 | discard; 13 | } 14 | } -------------------------------------------------------------------------------- /src/graphics/shaders/sprite/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::color::RGBA8; 2 | use crate::graphics::{ 3 | DrawMode, ShaderDescription, TextureSection, VertexAttribute, VertexDescriptor, VertexInputType, 4 | VertexInstancing, VertexOutputType, 5 | }; 6 | use crate::math::AABB2D; 7 | use cgmath::*; 8 | 9 | /// Configuraiton for the sprite shader. 10 | pub const SPRITE_SHADER: ShaderDescription = ShaderDescription { 11 | vertex_shader: include_str!("vertex.glsl"), 12 | fragment_shader: include_str!("fragment.glsl"), 13 | texture_names: &["tex"], 14 | uniform_names: &["vertex"], 15 | }; 16 | 17 | /// Configuration settings for a sprite. 18 | #[repr(C)] 19 | #[derive(Debug, Copy, Clone, PartialEq)] 20 | pub struct Sprite { 21 | /// Position of the sprite. The X and Y coordinates represent the bottom left corner of the 22 | /// sprite. The Z coordinate represents sprite depth. Units are measured in pixels. 23 | pub pos: Vector3, 24 | /// Units are measured in pixels. 25 | pub size: Vector2, 26 | /// Texture to apply to the sprite. The default is a plain white texture. 27 | pub texture: TextureSection, 28 | /// Color multiplier to apply to the sprite. The default is white. 29 | pub color: RGBA8, 30 | /// Rotation of the sprite. Units are 1/65536th of a turn. 31 | pub rotation: u16, 32 | } 33 | 34 | impl VertexDescriptor for Sprite { 35 | const INSTANCING: VertexInstancing = VertexInstancing::instanced(4); 36 | const ATTRIBUTES: &'static [VertexAttribute] = &[ 37 | // Pos, Size, Texture, Color::RGBA8, Rotation 38 | VertexAttribute::new(3, VertexInputType::F32, VertexOutputType::F32), 39 | VertexAttribute::new(2, VertexInputType::U16, VertexOutputType::F32), 40 | VertexAttribute::new(4, VertexInputType::U16, VertexOutputType::NormalizedF32), 41 | VertexAttribute::new(4, VertexInputType::U8, VertexOutputType::NormalizedF32), 42 | VertexAttribute::new(1, VertexInputType::U16, VertexOutputType::NormalizedF32), 43 | ]; 44 | const DRAW_MODE: DrawMode = DrawMode::TriangleStrip; 45 | } 46 | 47 | impl Default for Sprite { 48 | fn default() -> Sprite { 49 | Sprite { 50 | pos: Vector3::new(0.0, 0.0, 0.0), 51 | size: Vector2::new(100, 100), 52 | texture: TextureSection::default(), 53 | color: RGBA8::WHITE, 54 | rotation: 0, 55 | } 56 | } 57 | } 58 | 59 | impl Sprite { 60 | /// Creates aa new sprite. This converts the rotation and size from floats automatically. Size 61 | /// is measured in pixels, and is limited to 65535. Rotation is measured in turns from [0, 1). 62 | /// Values outside of the range are wrapped into the range. For example, 1.75 is wrapped into 63 | /// 0.75, -0.4 is wrapped into 0.6. 64 | pub fn new( 65 | pos: Vector3, 66 | size: Vector2, 67 | texture: TextureSection, 68 | color: RGBA8, 69 | rotation: f32, 70 | ) -> Sprite { 71 | Sprite { 72 | pos, 73 | size: { 74 | let x = (size.x as u32) & 0xFFFF; 75 | let y = (size.y as u32) & 0xFFFF; 76 | Vector2::new(x as u16, y as u16) 77 | }, 78 | texture, 79 | color, 80 | rotation: (rotation.fract() * 65536.0) as u16, 81 | } 82 | } 83 | 84 | /// Creates a new sprite. This does not perform conversions and represents exactly the members 85 | /// of the sprite type. 86 | pub fn new_raw( 87 | pos: Vector3, 88 | size: Vector2, 89 | texture: TextureSection, 90 | color: RGBA8, 91 | rotation: u16, 92 | ) -> Sprite { 93 | Sprite { 94 | pos, 95 | size, 96 | texture, 97 | color, 98 | rotation, 99 | } 100 | } 101 | } 102 | 103 | impl From for AABB2D { 104 | fn from(sprite: Sprite) -> Self { 105 | AABB2D::from_pos_size(sprite.pos.truncate(), sprite.size.cast().unwrap()) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/graphics/shaders/sprite/vertex.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | const float TWO_PI = 6.283185307179586476925286766559; 4 | 5 | layout(location = 0) in vec3 a_pos; 6 | layout(location = 1) in vec2 a_size; 7 | layout(location = 2) in vec4 a_uv; 8 | layout(location = 3) in vec4 a_color; 9 | layout(location = 4) in float a_rotation; 10 | 11 | out vec2 v_uv; 12 | out vec4 v_color; 13 | 14 | layout(std140) uniform vertex { 15 | mat4 ortho; 16 | }; 17 | 18 | // UV Layout: xmin xmax ymin ymax 19 | // ymin and ymax are swapped below because OpenGL reads images from bottom row to top row, but 20 | // they're stored top to bottom on upload, so this corrects that. 21 | vec4 uv_lut[4] = vec4[4]( 22 | vec4(1.0, 0.0, 1.0, 0.0), // left bottom 23 | vec4(1.0, 0.0, 0.0, 1.0), // left top 24 | vec4(0.0, 1.0, 1.0, 0.0), // right bottom 25 | vec4(0.0, 1.0, 0.0, 1.0)); // right top 26 | 27 | vec2 size_lut[4] = vec2[4]( 28 | vec2(0.0, 1.0), // left top 29 | vec2(0.0, 0.0), // right top 30 | vec2(1.0, 1.0), // left bottom 31 | vec2(1.0, 0.0)); // right bottom 32 | 33 | vec4 rotateZ(vec3 pos) { 34 | float psi = TWO_PI * a_rotation; 35 | float sina = sin(psi); 36 | float cosa = cos(psi); 37 | vec2 origin = vec2( 38 | a_pos.x + (a_size.x * 0.5), 39 | a_pos.y + (a_size.y * 0.5)); 40 | return vec4( 41 | (cosa * (pos.x - origin.x)) - (sina * (pos.y - origin.y)) + origin.x, 42 | (sina * (pos.x - origin.x)) + (cosa * (pos.y - origin.y)) + origin.y, 43 | pos.z, 44 | 1.0); 45 | } 46 | 47 | void main() { 48 | vec4 temp = a_uv * uv_lut[gl_VertexID]; 49 | v_uv = vec2(temp.x + temp.y, temp.z + temp.w); 50 | v_color = a_color; 51 | 52 | vec3 size = vec3(a_size * size_lut[gl_VertexID], 0.0); 53 | vec3 pos = a_pos + size; 54 | gl_Position = ortho * rotateZ(pos); 55 | } -------------------------------------------------------------------------------- /src/graphics/shaders/text/data.rs: -------------------------------------------------------------------------------- 1 | use crate::color::RGBA8; 2 | use crate::graphics::{ 3 | DrawMode, TextureSection, VertexAttribute, VertexDescriptor, VertexInputType, VertexInstancing, 4 | VertexOutputType, 5 | }; 6 | use cgmath::{Vector2, Vector3}; 7 | use fontdue::layout::TextStyle; 8 | 9 | /// Configuration settings for text. 10 | pub struct Text<'a> { 11 | /// The text to layout. 12 | pub text: &'a str, 13 | /// The scale of the text in pixel units. The units of the scale are pixels per Em unit. 14 | pub px: f32, 15 | /// The font to layout the text in. 16 | pub font_index: usize, 17 | /// The text color, 18 | pub color: RGBA8, 19 | /// The depth value used for rendering the text. 20 | pub depth: f32, 21 | } 22 | 23 | impl<'a> Into> for &Text<'a> { 24 | fn into(self) -> TextStyle<'a, TextUserData> { 25 | TextStyle { 26 | text: self.text, 27 | px: self.px, 28 | font_index: self.font_index, 29 | user_data: TextUserData { 30 | depth: self.depth, 31 | color: self.color, 32 | }, 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Copy, Clone, PartialEq)] 38 | pub(crate) struct TextUserData { 39 | pub color: RGBA8, 40 | pub depth: f32, 41 | } 42 | /// Holds configuration settings for a glyph of text. 43 | #[repr(C)] 44 | #[derive(Debug, Copy, Clone, PartialEq)] 45 | pub struct TextSprite { 46 | /// Position of the sprite. The X and Y coordinates represent the bottom left corner of the 47 | /// sprite. The Z coordinate represents sprite depth. Units are measured in pixels. 48 | pub pos: Vector3, 49 | /// Units are measured in pixels. 50 | pub size: Vector2, 51 | /// Texture to apply to the sprite. The default is a plain white texture. 52 | pub texture: TextureSection, 53 | /// Color multiplier to apply to the sprite. The default is white. 54 | pub color: RGBA8, 55 | } 56 | 57 | impl VertexDescriptor for TextSprite { 58 | const INSTANCING: VertexInstancing = VertexInstancing::instanced(4); 59 | const ATTRIBUTES: &'static [VertexAttribute] = &[ 60 | // Position, Size, UV, Color::RGBA8 61 | VertexAttribute::new(3, VertexInputType::F32, VertexOutputType::F32), 62 | VertexAttribute::new(2, VertexInputType::U16, VertexOutputType::F32), 63 | VertexAttribute::new(4, VertexInputType::U16, VertexOutputType::NormalizedF32), 64 | VertexAttribute::new(4, VertexInputType::U8, VertexOutputType::NormalizedF32), 65 | ]; 66 | const DRAW_MODE: DrawMode = DrawMode::TriangleStrip; 67 | } 68 | 69 | impl TextSprite { 70 | /// Helper function to create a new text sprite. 71 | pub fn new(pos: Vector3, size: Vector2, texture: TextureSection, color: RGBA8) -> TextSprite { 72 | TextSprite { 73 | pos, 74 | size: { 75 | let x = (size.x as u32) & 0xFFFF; 76 | let y = (size.y as u32) & 0xFFFF; 77 | Vector2::new(x as u16, y as u16) 78 | }, 79 | texture, 80 | color, 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/graphics/shaders/text/fragment.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | in vec2 v_uv; 4 | in vec4 v_color; 5 | out vec4 a_color; 6 | 7 | uniform sampler2D tex; 8 | 9 | void main() { 10 | a_color = vec4(v_color.rgb, texture(tex, v_uv).r); 11 | if (a_color.a <= 0.0) { 12 | discard; 13 | } 14 | } -------------------------------------------------------------------------------- /src/graphics/shaders/text/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | mod shader; 3 | 4 | pub(crate) use self::data::{TextSprite, TextUserData}; 5 | 6 | pub use self::data::Text; 7 | pub use self::shader::{TextShaderPass, TEXT_SHADER}; 8 | -------------------------------------------------------------------------------- /src/graphics/shaders/text/shader.rs: -------------------------------------------------------------------------------- 1 | use crate::color::R8; 2 | use crate::graphics::{ 3 | shaders::text::{Text, TextSprite, TextUserData}, 4 | texture_atlas::TextureAtlas, 5 | Buffer, Shader, ShaderDescription, TextureFiltering, TextureSection, Uniform, 6 | }; 7 | use crate::image::Image; 8 | use crate::{App, Context}; 9 | use alloc::vec::Vec; 10 | use cgmath::*; 11 | use fontdue::{ 12 | layout::{CoordinateSystem, GlyphRasterConfig, Layout, LayoutSettings}, 13 | Font, 14 | }; 15 | use hashbrown::HashMap; 16 | 17 | /// Configuration for the text shader. 18 | pub const TEXT_SHADER: ShaderDescription = ShaderDescription { 19 | vertex_shader: include_str!("vertex.glsl"), 20 | fragment_shader: include_str!("fragment.glsl"), 21 | texture_names: &["tex"], 22 | uniform_names: &["vertex"], 23 | }; 24 | 25 | #[derive(Debug, Copy, Clone)] 26 | struct CharCacheValue { 27 | uv: TextureSection, 28 | size: Vector2, 29 | } 30 | 31 | /// Holds the state required to cache and draw text to the screen. 32 | pub struct TextShaderPass { 33 | uniform: Uniform, 34 | atlas: TextureAtlas, 35 | buffer: Buffer, 36 | 37 | sprites: Vec, 38 | layout: Layout, 39 | cache: HashMap, 40 | dirty: bool, 41 | } 42 | 43 | impl TextShaderPass { 44 | pub fn new(ctx: &Context, ortho: Matrix4) -> TextShaderPass { 45 | let max = ctx.max_texture_size().min(4096) as u32; 46 | TextShaderPass { 47 | uniform: Uniform::new(ctx, ortho), 48 | atlas: TextureAtlas::new::(ctx, max, TextureFiltering::none()), 49 | buffer: Buffer::new(ctx), 50 | 51 | sprites: Vec::new(), 52 | layout: Layout::new(CoordinateSystem::PositiveYUp), 53 | cache: HashMap::new(), 54 | dirty: false, 55 | } 56 | } 57 | 58 | /// Sets the orthographic projection used to draw this pass. If none is passed, this function 59 | /// does nothing. 60 | pub fn set_ortho(&mut self, ortho: Matrix4) { 61 | self.uniform.set(ortho); 62 | } 63 | 64 | /// Draws the pass to the screen. 65 | pub fn draw(&mut self, shader: &Shader) { 66 | if self.sprites.len() > 0 { 67 | if self.dirty { 68 | self.dirty = false; 69 | self.buffer.set_data(&self.sprites); 70 | } 71 | shader.bind(&[&self.uniform], &[self.atlas.get()]); 72 | self.buffer.draw(); 73 | } 74 | } 75 | 76 | /// Appends text to the instance. 77 | pub fn append(&mut self, fonts: &[Font], layout: &LayoutSettings, styles: &[Text]) { 78 | self.layout.reset(layout); 79 | for style in styles { 80 | self.layout.append(fonts, &style.into()); 81 | } 82 | // log::info!("{:?}", self.layout.glyphs()); 83 | // log::info!("{:?}", self.layout.lines()); 84 | for glyph in self.layout.glyphs() { 85 | if glyph.width == 0 { 86 | continue; 87 | } 88 | let value = match self.cache.get(&glyph.key).copied() { 89 | Some(value) => value, 90 | None => { 91 | let font = &fonts[glyph.font_index]; 92 | let (metrics, bitmap) = font.rasterize_config(glyph.key); 93 | // info!("{:?}", metrics); // Debug 94 | let image = Image::from_vec(bitmap, metrics.width as u32, metrics.height as u32); 95 | let uv = self.atlas.pack(&image).expect("Text packer is full."); 96 | let value = CharCacheValue { 97 | uv, 98 | size: Vector2::new(metrics.width as f32, metrics.height as f32), 99 | }; 100 | self.cache.insert(glyph.key, value); 101 | value 102 | } 103 | }; 104 | self.sprites.push(TextSprite::new( 105 | Vector3::new(glyph.x, glyph.y, glyph.user_data.depth), 106 | value.size, 107 | value.uv, 108 | glyph.user_data.color, 109 | )); 110 | self.dirty = true; 111 | } 112 | } 113 | 114 | /// Clears all the text, drawing nothing. 115 | pub fn clear_text(&mut self) { 116 | self.sprites.clear(); 117 | self.dirty = true; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/graphics/shaders/text/vertex.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | const float TWO_PI = 6.283185307179586476925286766559; 4 | 5 | layout(location = 0) in vec3 a_pos; 6 | layout(location = 1) in vec2 a_size; 7 | layout(location = 2) in vec4 a_uv; 8 | layout(location = 3) in vec4 a_color; 9 | 10 | out vec2 v_uv; 11 | out vec4 v_color; 12 | 13 | layout(std140) uniform vertex { 14 | mat4 ortho; 15 | }; 16 | 17 | // UV Layout: xmin xmax ymin ymax 18 | // ymin and ymax are swapped below because OpenGL reads images from bottom row to top row, but 19 | // they're stored top to bottom on upload, so this corrects that. 20 | vec4 uv_lut[4] = vec4[4]( 21 | vec4(1.0, 0.0, 1.0, 0.0), // left bottom 22 | vec4(1.0, 0.0, 0.0, 1.0), // left top 23 | vec4(0.0, 1.0, 1.0, 0.0), // right bottom 24 | vec4(0.0, 1.0, 0.0, 1.0)); // right top 25 | 26 | vec2 size_lut[4] = vec2[4]( 27 | vec2(0.0, 1.0), // left top 28 | vec2(0.0, 0.0), // right top 29 | vec2(1.0, 1.0), // left bottom 30 | vec2(1.0, 0.0)); // right bottom 31 | 32 | void main() { 33 | vec4 temp = a_uv * uv_lut[gl_VertexID]; 34 | v_uv = vec2(temp.x + temp.y, temp.z + temp.w); 35 | v_color = a_color; 36 | 37 | vec3 size = vec3(a_size * size_lut[gl_VertexID], 0.0); 38 | vec3 pos = a_pos + size; 39 | gl_Position = ortho * vec4(pos, 1.0); 40 | } -------------------------------------------------------------------------------- /src/graphics/std140/core.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::std140::*; 2 | 3 | // float ======================================== 4 | 5 | impl IntoStd140 for [f32; 3] { 6 | type Output = vec3; 7 | fn std140(self) -> Self::Output { 8 | Self::Output { 9 | x: self[0], 10 | y: self[1], 11 | z: self[2], 12 | } 13 | } 14 | } 15 | 16 | impl IntoStd140 for [f32; 4] { 17 | type Output = vec4; 18 | fn std140(self) -> Self::Output { 19 | Self::Output { 20 | x: self[0], 21 | y: self[1], 22 | z: self[2], 23 | w: self[3], 24 | } 25 | } 26 | } 27 | 28 | impl From for float { 29 | fn from(v: f32) -> Self { 30 | Self::fill(v) 31 | } 32 | } 33 | 34 | impl From for vec2 { 35 | fn from(v: f32) -> Self { 36 | Self::fill(v) 37 | } 38 | } 39 | 40 | impl From for vec3 { 41 | fn from(v: f32) -> Self { 42 | Self::fill(v) 43 | } 44 | } 45 | 46 | impl From for vec4 { 47 | fn from(v: f32) -> Self { 48 | Self::fill(v) 49 | } 50 | } 51 | 52 | // int ========================================== 53 | 54 | impl IntoStd140 for [i32; 3] { 55 | type Output = ivec3; 56 | fn std140(self) -> Self::Output { 57 | Self::Output { 58 | x: self[0], 59 | y: self[1], 60 | z: self[2], 61 | } 62 | } 63 | } 64 | 65 | impl IntoStd140 for [i32; 4] { 66 | type Output = ivec4; 67 | fn std140(self) -> Self::Output { 68 | Self::Output { 69 | x: self[0], 70 | y: self[1], 71 | z: self[2], 72 | w: self[3], 73 | } 74 | } 75 | } 76 | 77 | impl From for int { 78 | fn from(v: i32) -> Self { 79 | Self::fill(v) 80 | } 81 | } 82 | 83 | impl From for ivec2 { 84 | fn from(v: i32) -> Self { 85 | Self::fill(v) 86 | } 87 | } 88 | 89 | impl From for ivec3 { 90 | fn from(v: i32) -> Self { 91 | Self::fill(v) 92 | } 93 | } 94 | 95 | impl From for ivec4 { 96 | fn from(v: i32) -> Self { 97 | Self::fill(v) 98 | } 99 | } 100 | 101 | // uint ========================================= 102 | 103 | impl IntoStd140 for [u32; 3] { 104 | type Output = uvec3; 105 | fn std140(self) -> Self::Output { 106 | Self::Output { 107 | x: self[0], 108 | y: self[1], 109 | z: self[2], 110 | } 111 | } 112 | } 113 | 114 | impl IntoStd140 for [u32; 4] { 115 | type Output = uvec4; 116 | fn std140(self) -> Self::Output { 117 | Self::Output { 118 | x: self[0], 119 | y: self[1], 120 | z: self[2], 121 | w: self[3], 122 | } 123 | } 124 | } 125 | 126 | impl From for uint { 127 | fn from(v: u32) -> Self { 128 | Self::fill(v) 129 | } 130 | } 131 | 132 | impl From for uvec2 { 133 | fn from(v: u32) -> Self { 134 | Self::fill(v) 135 | } 136 | } 137 | 138 | impl From for uvec3 { 139 | fn from(v: u32) -> Self { 140 | Self::fill(v) 141 | } 142 | } 143 | 144 | impl From for uvec4 { 145 | fn from(v: u32) -> Self { 146 | Self::fill(v) 147 | } 148 | } 149 | 150 | // boolean ====================================== 151 | 152 | impl IntoStd140 for [bool; 3] { 153 | type Output = bvec3; 154 | fn std140(self) -> Self::Output { 155 | Self::Output { 156 | x: self[0].into(), 157 | y: self[1].into(), 158 | z: self[2].into(), 159 | } 160 | } 161 | } 162 | 163 | impl IntoStd140 for [bool; 4] { 164 | type Output = bvec4; 165 | fn std140(self) -> Self::Output { 166 | Self::Output { 167 | x: self[0].into(), 168 | y: self[1].into(), 169 | z: self[2].into(), 170 | w: self[3].into(), 171 | } 172 | } 173 | } 174 | 175 | impl From for boolean { 176 | fn from(x: bool) -> Self { 177 | match x { 178 | true => boolean::True, 179 | false => boolean::False, 180 | } 181 | } 182 | } 183 | 184 | impl From for bvec2 { 185 | fn from(v: bool) -> Self { 186 | Self::fill(v.into()) 187 | } 188 | } 189 | 190 | impl From for bvec3 { 191 | fn from(v: bool) -> Self { 192 | Self::fill(v.into()) 193 | } 194 | } 195 | 196 | impl From for bvec4 { 197 | fn from(v: bool) -> Self { 198 | Self::fill(v.into()) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/graphics/texture_atlas.rs: -------------------------------------------------------------------------------- 1 | use crate::color::ColorDescriptor; 2 | use crate::graphics::{Texture, TextureFiltering, TextureSection}; 3 | use crate::image::{Image, Packer}; 4 | use crate::{App, Context}; 5 | 6 | /// Simple image atlas that adds padding to reduce mip map artifacts. Extra padding is added to 7 | /// packed images based on the number of mip levels. More mip levels means more space dedicated to 8 | /// padding. 9 | pub struct TextureAtlas { 10 | atlas: Texture, 11 | packer: Packer, 12 | padding: Option, 13 | } 14 | 15 | impl TextureAtlas { 16 | /// Creates a new atlas. 17 | /// # Arguments 18 | /// 19 | /// * `size` - The width and height of the atlas. This must be a power of two value. 20 | /// * `filtering` - The filtering to apply. If any filtering is used, this atlas will add 21 | /// padding to each texture packed to offset mip map artifacts. 22 | pub fn new( 23 | ctx: &Context, 24 | size: u32, 25 | filtering: TextureFiltering, 26 | ) -> TextureAtlas { 27 | assert!(size.is_power_of_two(), "size is not a power of two."); 28 | 29 | let atlas = Texture::from_image(ctx, &Image::from_color(T::default(), size, size), filtering); 30 | let packer = Packer::new(size, size); 31 | let padding = if let Some(mip_levels) = filtering.mip_levels() { 32 | Some(2u32.pow(mip_levels as u32)) 33 | } else { 34 | None 35 | }; 36 | 37 | TextureAtlas { 38 | atlas, 39 | packer, 40 | padding, 41 | } 42 | } 43 | 44 | /// Packs an image into the texture atlas, returning a texture section for where the image was 45 | /// added. Returns None if the image could not be fit in the atlas. 46 | pub fn pack(&mut self, image: &Image) -> Option { 47 | if let Some(padding) = self.padding { 48 | let image = image.pad_uniform(padding); 49 | let rect = self.packer.pack(image.width(), image.height()); 50 | if let Some(rect) = rect { 51 | self.atlas.set(rect.x, rect.y, &image); 52 | return Some(self.atlas.subsection( 53 | rect.x + padding, 54 | rect.x + rect.w - padding, 55 | rect.y + padding, 56 | rect.y + rect.h - padding, 57 | )); 58 | } 59 | } else { 60 | let rect = self.packer.pack(image.width(), image.height()); 61 | if let Some(rect) = rect { 62 | self.atlas.set(rect.x, rect.y, image); 63 | return Some(self.atlas.subsection(rect.x, rect.x + rect.w, rect.y, rect.y + rect.h)); 64 | } 65 | } 66 | None 67 | } 68 | 69 | /// Gets a reference to the underlying texture. 70 | pub fn get(&self) -> &Texture { 71 | &self.atlas 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/graphics/texture_section.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::Texture; 2 | use cgmath::*; 3 | 4 | const MAX_INTEGER: u16 = u16::MAX; 5 | const MAX_FLOAT: f32 = u16::MAX as f32 + 1.0; 6 | 7 | /// Token to reference a texture with. Has basic configuration settings. 8 | #[derive(Copy, Clone, Debug, PartialEq)] 9 | #[repr(transparent)] 10 | pub struct TextureSection(pub Vector4); 11 | 12 | impl Default for TextureSection { 13 | fn default() -> TextureSection { 14 | TextureSection::full() 15 | } 16 | } 17 | 18 | impl TextureSection { 19 | /// Coordinates relative to the top left corner of the texture. (0, 0) is the top left of the 20 | /// texture, and (width, height) is the bottom right of the texture. 21 | pub fn from_texture(texture: &Texture, left: u32, right: u32, top: u32, bottom: u32) -> TextureSection { 22 | let inv_width = MAX_FLOAT / (texture.width() as f32); 23 | let inv_height = MAX_FLOAT / (texture.height() as f32); 24 | let left = (left as f32) * inv_width; 25 | let right = (right as f32) * inv_width; 26 | let top = (top as f32) * inv_height; 27 | let bottom = (bottom as f32) * inv_height; 28 | TextureSection(Vector4::new( 29 | left as u16 + 1, // Left 30 | right as u16 - 1, // Right 31 | top as u16 + 1, // Top 32 | bottom as u16 - 1, // Bottom 33 | )) 34 | } 35 | 36 | /// Creates a texture section that encompases the whole texture. 37 | pub fn full() -> TextureSection { 38 | TextureSection(Vector4::new(0, MAX_INTEGER, 0, MAX_INTEGER)) 39 | } 40 | 41 | /// Mirrors the texture along the Y axis. Creates a new texture. 42 | pub fn mirror_y(&self) -> TextureSection { 43 | TextureSection(Vector4::new(self.0.y, self.0.x, self.0.z, self.0.w)) 44 | } 45 | 46 | /// Mirrors the texture along the X axis. Creates a new texture. 47 | pub fn mirror_x(&self) -> TextureSection { 48 | TextureSection(Vector4::new(self.0.x, self.0.y, self.0.w, self.0.z)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/graphics/uniform.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{ 2 | graphics, resource, std140::IntoStd140, BufferBindingTarget, BufferBlockBindingTarget, BufferUsage, 3 | }; 4 | use crate::{App, Context}; 5 | 6 | /// Stores a uniform on the device. 7 | pub struct Uniform { 8 | _unsend: core::marker::PhantomData<*const ()>, 9 | vbo: resource::Buffer, 10 | } 11 | 12 | impl Uniform { 13 | /// Creates a new uniform. 14 | pub fn new(_ctx: &Context, uniform: T) -> Uniform { 15 | let gl = graphics().gl(); 16 | 17 | let vbo = gl.create_buffer(); 18 | gl.bind_buffer(BufferBindingTarget::UniformBuffer, Some(vbo)); 19 | gl.buffer_data(BufferBindingTarget::UniformBuffer, &[uniform.std140()], BufferUsage::StaticDraw); 20 | 21 | Uniform { 22 | _unsend: core::marker::PhantomData, 23 | vbo, 24 | } 25 | } 26 | 27 | /// Sets the value of the uniform. 28 | pub fn set(&mut self, uniform: T) { 29 | let gl = graphics().gl(); 30 | gl.bind_buffer(BufferBindingTarget::UniformBuffer, Some(self.vbo)); 31 | gl.buffer_data(BufferBindingTarget::UniformBuffer, &[uniform.std140()], BufferUsage::StaticDraw); 32 | } 33 | 34 | pub(crate) fn bind(&self, block: u32) { 35 | let gl = graphics().gl(); 36 | gl.bind_buffer_base(BufferBlockBindingTarget::UniformBuffer, block, Some(self.vbo)); 37 | } 38 | } 39 | 40 | impl Drop for Uniform { 41 | fn drop(&mut self) { 42 | let gl = graphics().gl(); 43 | gl.delete_buffer(self.vbo); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/graphics/window/display_mode.rs: -------------------------------------------------------------------------------- 1 | /// Enumeration for window display options. 2 | #[derive(Copy, Clone, Debug, PartialEq)] 3 | pub enum DisplayMode { 4 | /// Normal windowed mode. 5 | Windowed { 6 | /// The height of the window. 7 | width: i32, 8 | /// The height of the window. 9 | height: i32, 10 | /// If the window is resizable. 11 | resizable: bool, 12 | }, 13 | /// For "fake" fullscreen that takes the size of the desktop. 14 | WindowedFullscreen, 15 | /// For "real" fullscreen with a videomode change. 16 | Fullscreen, 17 | } 18 | 19 | /// Enumeration for all possible vsync settings. 20 | #[derive(Copy, Clone, Debug, PartialEq)] 21 | pub enum Vsync { 22 | /// Vsync will be disabled. 23 | Disabled, 24 | /// Vsync will be enabled. 25 | Enabled, 26 | } 27 | -------------------------------------------------------------------------------- /src/graphics/window/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | #[path = "platform/native.rs"] 3 | mod native; 4 | #[cfg(not(target_arch = "wasm32"))] 5 | pub use self::native::OpenGLWindow; 6 | 7 | #[cfg(target_arch = "wasm32")] 8 | #[path = "platform/wasm.rs"] 9 | mod wasm; 10 | #[cfg(target_arch = "wasm32")] 11 | pub use self::wasm::OpenGLWindow; 12 | 13 | mod display_mode; 14 | mod window_settings; 15 | 16 | pub use display_mode::{DisplayMode, Vsync}; 17 | pub use window_settings::WindowSettings; 18 | 19 | use cgmath::Vector2; 20 | use winit::event_loop::EventLoop; 21 | 22 | pub(crate) trait OpenGLWindowContract: Sized { 23 | fn new(desc: &WindowSettings, event_loop: &EventLoop<()>) -> (Self, glow::Context); 24 | 25 | /// Gets the scale factor of the window. This is related to DPI scaling. 26 | fn scale_factor(&self) -> f32; 27 | 28 | /// Gets the logical size of the window. This may differ from the viewport's logical size. 29 | fn logical_size(&self) -> Vector2; 30 | 31 | /// Gets the physical size of the window. This may differ from the viewport's physical size. 32 | fn physical_size(&self) -> Vector2; 33 | 34 | /// Grabs the cursor, preventing it from leaving the window. 35 | /// 36 | /// ## Platform-specific 37 | /// 38 | /// - **macOS:** This locks the cursor in a fixed location, which looks visually awkward. 39 | fn set_cursor_grab(&self, grab: bool); 40 | 41 | /// Sets the visibility of the cursor. 42 | /// 43 | /// ## Platform-specific 44 | /// 45 | /// - **Windows:** The cursor is only hidden within the confines of the window. 46 | /// - **X11:** The cursor is only hidden within the confines of the window. 47 | /// - **Wayland:** The cursor is only hidden within the confines of the window. 48 | /// - **macOS:** The cursor is hidden as long as the window has input focus, even if the cursor is 49 | /// outside of the window. 50 | fn set_cursor_visible(&self, grab: bool); 51 | 52 | /// Sets the title of the window. 53 | /// 54 | /// ## Platform-specific 55 | /// 56 | /// - **Web:** This sets the page title. 57 | fn set_title(&self, title: &str); 58 | 59 | /// Sets the display mode of the window. 60 | fn set_display_mode(&self, display_mode: DisplayMode); 61 | 62 | /// Swaps the buffers in case of double or triple buffering. You should call this function every 63 | /// time you have finished rendering, or the image may not be displayed on the screen. 64 | /// 65 | /// ## Platform-specific 66 | /// 67 | /// - **Web:** This is a no-op. 68 | fn swap_buffers(&self); 69 | } 70 | -------------------------------------------------------------------------------- /src/graphics/window/platform/native.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{DisplayMode, OpenGLWindowContract, Vsync, WindowSettings}; 2 | use cgmath::*; 3 | use glutin::ContextBuilder; 4 | use log::info; 5 | use winit::dpi::LogicalSize; 6 | use winit::event_loop::EventLoop; 7 | use winit::window::{CursorGrabMode, Fullscreen, Window, WindowBuilder}; 8 | 9 | pub struct OpenGLWindow { 10 | inner: glutin::ContextWrapper, 11 | } 12 | 13 | impl OpenGLWindowContract for OpenGLWindow { 14 | fn new(desc: &WindowSettings, event_loop: &EventLoop<()>) -> (OpenGLWindow, glow::Context) { 15 | let mut window_builder = WindowBuilder::new().with_title(&desc.title); 16 | match desc.display_mode { 17 | DisplayMode::Windowed { 18 | width, 19 | height, 20 | resizable, 21 | } => { 22 | window_builder = 23 | window_builder.with_resizable(resizable).with_inner_size(LogicalSize::new(width, height)) 24 | } 25 | DisplayMode::WindowedFullscreen | DisplayMode::Fullscreen => { 26 | let fullscreen = Fullscreen::Borderless(event_loop.primary_monitor()); 27 | window_builder = window_builder.with_fullscreen(Some(fullscreen)); 28 | } 29 | } 30 | let mut context_builder = ContextBuilder::new(); 31 | match desc.vsync { 32 | Vsync::Disabled => { 33 | context_builder = context_builder.with_vsync(false); 34 | } 35 | Vsync::Enabled => { 36 | context_builder = context_builder.with_vsync(true); 37 | } 38 | } 39 | let window_context = context_builder.build_windowed(window_builder, &event_loop).unwrap(); 40 | let window_context = unsafe { window_context.make_current() }.unwrap(); 41 | let gl = unsafe { 42 | glow::Context::from_loader_function(|s| window_context.get_proc_address(s) as *const _) 43 | }; 44 | info!("Created window."); 45 | ( 46 | OpenGLWindow { 47 | inner: window_context, 48 | }, 49 | gl, 50 | ) 51 | } 52 | 53 | fn scale_factor(&self) -> f32 { 54 | self.inner.window().scale_factor() as f32 55 | } 56 | 57 | fn logical_size(&self) -> Vector2 { 58 | let size = self.inner.window().inner_size(); 59 | let scale_factor = self.inner.window().scale_factor() as f32; 60 | let size = Vector2::new(size.width as f32, size.height as f32); 61 | size / scale_factor 62 | } 63 | 64 | fn physical_size(&self) -> Vector2 { 65 | let size = self.inner.window().inner_size(); 66 | Vector2::new(size.width as f32, size.height as f32) 67 | } 68 | 69 | fn set_cursor_grab(&self, grab: bool) { 70 | let mode = if grab { 71 | #[cfg(target_os = "macos")] 72 | { 73 | CursorGrabMode::Locked 74 | } 75 | #[cfg(not(target_os = "macos"))] 76 | { 77 | CursorGrabMode::Confined 78 | } 79 | } else { 80 | CursorGrabMode::None 81 | }; 82 | let _ = self.inner.window().set_cursor_grab(mode); 83 | } 84 | 85 | fn set_cursor_visible(&self, visible: bool) { 86 | let _ = self.inner.window().set_cursor_visible(visible); 87 | } 88 | 89 | fn swap_buffers(&self) { 90 | self.inner.swap_buffers().unwrap(); 91 | } 92 | 93 | fn set_title(&self, title: &str) { 94 | self.inner.window().set_title(title); 95 | } 96 | 97 | fn set_display_mode(&self, display_mode: DisplayMode) { 98 | match display_mode { 99 | DisplayMode::Windowed { 100 | width, 101 | height, 102 | resizable, 103 | } => { 104 | self.inner.window().set_inner_size(LogicalSize::new(width, height)); 105 | self.inner.window().set_resizable(resizable); 106 | self.inner.window().set_fullscreen(None); 107 | } 108 | DisplayMode::WindowedFullscreen | DisplayMode::Fullscreen => { 109 | let fullscreen = Fullscreen::Borderless(self.inner.window().current_monitor()); 110 | self.inner.window().set_fullscreen(Some(fullscreen)); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/graphics/window/platform/wasm.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{DisplayMode, OpenGLWindowContract, WindowSettings}; 2 | use cgmath::*; 3 | use log::info; 4 | use wasm_bindgen::JsCast; 5 | use winit::dpi::LogicalSize; 6 | use winit::event_loop::EventLoop; 7 | use winit::platform::web::WindowExtWebSys; 8 | use winit::window::{CursorGrabMode, Fullscreen, Window, WindowBuilder}; 9 | 10 | pub struct OpenGLWindow { 11 | inner: Window, 12 | } 13 | 14 | impl OpenGLWindowContract for OpenGLWindow { 15 | fn new(desc: &WindowSettings, event_loop: &EventLoop<()>) -> (OpenGLWindow, glow::Context) { 16 | let mut builder = WindowBuilder::new().with_title(&desc.title); 17 | builder = match desc.display_mode { 18 | DisplayMode::Windowed { 19 | width, 20 | height, 21 | .. 22 | } => builder.with_inner_size(LogicalSize::new(width, height)), 23 | DisplayMode::WindowedFullscreen | DisplayMode::Fullscreen => { 24 | builder.with_fullscreen(Some(Fullscreen::Borderless(None))) 25 | } 26 | }; 27 | let winit_window = builder.build(event_loop).expect("Window build"); 28 | 29 | let canvas = winit_window.canvas(); 30 | let webgl2_context = canvas 31 | .get_context("webgl2") // Result, JsValue> 32 | .expect("Get webgl2 context A") // Option 33 | .expect("Get webgl2 context B") // Object 34 | .dyn_into::() // Result 35 | .expect("Get webgl2 context C"); // WebGl2RenderingContext 36 | let gl = glow::Context::from_webgl2_context(webgl2_context); 37 | 38 | let window = web_sys::window().unwrap(); 39 | let document = window.document().unwrap(); 40 | let body = document.body().unwrap(); 41 | body.append_child(&canvas).expect("Append canvas to HTML body"); 42 | 43 | let window = OpenGLWindow { 44 | inner: winit_window, 45 | }; 46 | window.set_title(&desc.title); 47 | window.set_display_mode(desc.display_mode); 48 | info!("Created canvas."); 49 | 50 | (window, gl) 51 | } 52 | 53 | fn scale_factor(&self) -> f32 { 54 | self.inner.scale_factor() as f32 55 | } 56 | 57 | fn logical_size(&self) -> Vector2 { 58 | let size = self.inner.inner_size(); 59 | let scale_factor = self.inner.scale_factor() as f32; 60 | let size = Vector2::new(size.width as f32, size.height as f32); 61 | size / scale_factor 62 | } 63 | 64 | fn physical_size(&self) -> Vector2 { 65 | let size = self.inner.inner_size(); 66 | Vector2::new(size.width as f32, size.height as f32) 67 | } 68 | 69 | fn set_cursor_grab(&self, grab: bool) { 70 | let mode = if grab { 71 | CursorGrabMode::Locked 72 | } else { 73 | CursorGrabMode::None 74 | }; 75 | let _ = self.inner.set_cursor_grab(mode); 76 | } 77 | 78 | fn set_cursor_visible(&self, visible: bool) { 79 | let _ = self.inner.set_cursor_visible(visible); 80 | } 81 | 82 | fn swap_buffers(&self) { 83 | // This is implicit on web. 84 | } 85 | 86 | fn set_title(&self, title: &str) { 87 | web_sys::window() // Option 88 | .unwrap() // Window 89 | .document() // Option 90 | .unwrap() // Document 91 | .set_title(title); 92 | } 93 | 94 | fn set_display_mode(&self, display_mode: DisplayMode) { 95 | match display_mode { 96 | DisplayMode::Windowed { 97 | width, 98 | height, 99 | .. 100 | } => { 101 | if let Some(_) = self.inner.fullscreen() { 102 | self.inner.set_fullscreen(None); 103 | } 104 | self.inner.set_inner_size(LogicalSize::new(width, height)); 105 | } 106 | DisplayMode::WindowedFullscreen | DisplayMode::Fullscreen => { 107 | self.inner.set_fullscreen(Some(Fullscreen::Borderless(None))); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/graphics/window/window_settings.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{DisplayMode, Vsync}; 2 | use alloc::string::String; 3 | 4 | /// Configuration settings for the window. 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub struct WindowSettings { 7 | /// The title of the window. 8 | pub title: String, 9 | /// The display mode of the window. 10 | pub display_mode: DisplayMode, 11 | /// Vsync mode for the window. 12 | pub vsync: Vsync, 13 | } 14 | 15 | impl Default for WindowSettings { 16 | fn default() -> WindowSettings { 17 | WindowSettings { 18 | title: String::from("Storm Engine"), 19 | display_mode: DisplayMode::Windowed { 20 | width: 500, 21 | height: 500, 22 | resizable: true, 23 | }, 24 | vsync: Vsync::Disabled, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/image/image.rs: -------------------------------------------------------------------------------- 1 | use crate::color::{ColorDescriptor, RGBA8}; 2 | use alloc::{vec, vec::Vec}; 3 | 4 | /// Basic image type. 5 | #[derive(Clone)] 6 | pub struct Image { 7 | pixels: Vec, 8 | width: u32, 9 | height: u32, 10 | } 11 | 12 | impl Image { 13 | /// Interpret a slice of bytes as a PNG and decodes it into an RGBA image. 14 | pub fn from_png(bytes: &[u8]) -> Image { 15 | crate::image::png::read_png(bytes) 16 | } 17 | } 18 | 19 | impl Image { 20 | /// Creates an image with the given color and size. 21 | pub fn from_color(color: T, width: u32, height: u32) -> Image { 22 | assert!(width > 0 && height > 0, "Neither width or height can be 0."); 23 | let pixels = vec![color; (width * height) as usize]; 24 | Image { 25 | pixels, 26 | width, 27 | height, 28 | } 29 | } 30 | 31 | /// Creates an image with the given buffer and size. The buffer length must match the image 32 | /// dimensions. 33 | pub fn from_vec(buf: Vec, width: u32, height: u32) -> Image { 34 | assert!(width > 0 && height > 0, "Neither width or height can be 0."); 35 | assert!(buf.len() == (width * height) as usize, "Buffer length must match image dimensions."); 36 | Image { 37 | pixels: buf, 38 | width, 39 | height, 40 | } 41 | } 42 | 43 | #[inline(always)] 44 | pub fn index_for(&self, x: u32, y: u32) -> usize { 45 | (y * self.width + x) as usize 46 | } 47 | 48 | /// The width of the image. 49 | pub fn width(&self) -> u32 { 50 | self.width 51 | } 52 | 53 | /// The height of the image. 54 | pub fn height(&self) -> u32 { 55 | self.height 56 | } 57 | 58 | pub fn as_slice(&self) -> &[T] { 59 | self.pixels.as_slice() 60 | } 61 | 62 | pub fn as_mut_slice(&mut self) -> &mut [T] { 63 | self.pixels.as_mut_slice() 64 | } 65 | 66 | /// Returns the underlying Vec that backs the image data. 67 | pub fn into_vec(self) -> Vec { 68 | self.pixels 69 | } 70 | 71 | /// Gets the value of an individual pixel. 0 is the first pixel, and width * height is the last 72 | /// pixel. 73 | pub fn get_indexed(&self, index: usize) -> T { 74 | self.pixels[index] 75 | } 76 | 77 | /// Gets the value of an individual pixel. (0, 0) is the top left of the image, and 78 | /// (width, height) is the bottom right of the image. 79 | pub fn get(&self, x: u32, y: u32) -> T { 80 | self.get_indexed(self.index_for(x, y)) 81 | } 82 | 83 | /// Sets an individual pixel to the given value. 0 is the first pixel, and width * height is the 84 | /// last pixel. 85 | pub fn set_indexed(&mut self, index: usize, val: T) { 86 | self.pixels[index] = val; 87 | } 88 | 89 | /// Sets an individual pixel to the given value. (0, 0) is the top left of the image, and 90 | /// (width, height) is the bottom right of the image. 91 | pub fn set(&mut self, x: u32, y: u32, val: T) { 92 | self.set_indexed(self.index_for(x, y), val); 93 | } 94 | 95 | /// Sets a subsection of the image to the given image. (0, 0) is the top left of the image, and 96 | /// (width, height) is the bottom right of the image. 97 | /// # Arguments 98 | /// 99 | /// * `offset_x` - The top left texel x coordinate to offset the image by. 100 | /// * `offset_y` - The top left texel y coordinate to offset the image by. 101 | /// * `tex` - The image to overwrite the current image with. 102 | pub fn set_subsection(&mut self, offset_x: u32, offset_y: u32, tex: &Image) { 103 | assert!(tex.width + offset_x <= self.width && tex.height + offset_y <= self.height); 104 | for x in 0..tex.width { 105 | for y in 0..tex.height { 106 | let index_self = self.index_for(x + offset_x, y + offset_y); 107 | let index_tex = tex.index_for(x, y); 108 | self.pixels[index_self] = tex.pixels[index_tex]; 109 | } 110 | } 111 | } 112 | 113 | /// Extends all edges of this image by `thickness`. The pixel color of the padded region is 114 | /// clamped to the edge of the original image. 115 | pub fn pad_uniform(&self, thickness: u32) -> Image { 116 | self.pad(thickness, thickness, thickness, thickness) 117 | } 118 | 119 | /// Extends each edge individually. The pixel color of the padded region is clamped to the edge 120 | /// of the original image. 121 | pub fn pad(&self, left: u32, right: u32, top: u32, bot: u32) -> Image { 122 | let mut result = Image::from_color( 123 | T::default(), // 124 | self.width + left + right, 125 | self.height + top + bot, 126 | ); 127 | result.set_subsection(left, top, &self); 128 | 129 | // Top Left Corner 130 | let reference = self.get(0, 0); 131 | for x in 0..left { 132 | for y in 0..top { 133 | result.set(x, y, reference); 134 | } 135 | } 136 | // Top Right Corner 137 | let reference = self.get(self.width - 1, 0); 138 | let offset_x = self.width + left; 139 | for x in 0..right { 140 | for y in 0..top { 141 | result.set(x + offset_x, y, reference); 142 | } 143 | } 144 | // Bottom Left Corner 145 | let reference = self.get(0, self.height - 1); 146 | let offset_y = self.height + top; 147 | for x in 0..left { 148 | for y in 0..bot { 149 | result.set(x, y + offset_y, reference); 150 | } 151 | } 152 | // Bottom Right Corner 153 | let reference = self.get(self.width - 1, self.height - 1); 154 | let offset_x = self.width + left; 155 | let offset_y = self.height + top; 156 | for x in 0..right { 157 | for y in 0..bot { 158 | result.set(x + offset_x, y + offset_y, reference); 159 | } 160 | } 161 | 162 | // Top Side 163 | for x in 0..self.width { 164 | let reference = self.get(x, 0); 165 | for y in 0..top { 166 | result.set(x + left, y, reference); 167 | } 168 | } 169 | // Bottom Side 170 | let offset_y = self.height + top; 171 | for x in 0..self.width { 172 | let reference = self.get(x, self.height - 1); 173 | for y in 0..bot { 174 | result.set(x + left, y + offset_y, reference); 175 | } 176 | } 177 | // Left Side 178 | for y in 0..self.height { 179 | let reference = self.get(0, y); 180 | for x in 0..left { 181 | result.set(x, y + top, reference); 182 | } 183 | } 184 | // Right Side 185 | let offset_x = self.width + left; 186 | for y in 0..self.height { 187 | let reference = self.get(self.width - 1, y); 188 | for x in 0..right { 189 | result.set(x + offset_x, y + top, reference); 190 | } 191 | } 192 | 193 | result 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/image/mod.rs: -------------------------------------------------------------------------------- 1 | mod image; 2 | mod packer; 3 | mod png; 4 | mod resize; 5 | 6 | pub use image::Image; 7 | pub use packer::{Packer, Rect}; 8 | -------------------------------------------------------------------------------- /src/image/packer.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | /// A basic rectangle type used in the packer. 4 | #[derive(Copy, Clone, Debug)] 5 | pub struct Rect { 6 | pub x: u32, 7 | pub y: u32, 8 | pub w: u32, 9 | pub h: u32, 10 | } 11 | 12 | impl Rect { 13 | pub fn new(x: u32, y: u32, w: u32, h: u32) -> Rect { 14 | Rect { 15 | x, 16 | y, 17 | w, 18 | h, 19 | } 20 | } 21 | 22 | #[inline(always)] 23 | fn top(&self) -> u32 { 24 | self.y 25 | } 26 | 27 | #[inline(always)] 28 | fn bottom(&self) -> u32 { 29 | self.y + self.h - 1 30 | } 31 | 32 | #[inline(always)] 33 | fn left(&self) -> u32 { 34 | self.x 35 | } 36 | 37 | #[inline(always)] 38 | fn right(&self) -> u32 { 39 | self.x + self.w - 1 40 | } 41 | 42 | #[inline(always)] 43 | fn contains(&self, other: &Rect) -> bool { 44 | self.left() <= other.left() 45 | && self.right() >= other.right() 46 | && self.top() <= other.top() 47 | && self.bottom() >= other.bottom() 48 | } 49 | } 50 | 51 | struct Skyline { 52 | pub x: u32, 53 | pub y: u32, 54 | pub w: u32, 55 | } 56 | 57 | impl Skyline { 58 | #[inline(always)] 59 | fn left(&self) -> u32 { 60 | self.x 61 | } 62 | 63 | #[inline(always)] 64 | fn right(&self) -> u32 { 65 | self.x + self.w - 1 66 | } 67 | } 68 | 69 | /// Rectangle packer using the skyline algorithm. 70 | pub struct Packer { 71 | border: Rect, 72 | // The skylines are sorted by their `x` position. 73 | skylines: Vec, 74 | } 75 | 76 | impl Packer { 77 | /// Creates a new packer with the given width and height. 78 | pub fn new(w: u32, h: u32) -> Packer { 79 | let mut skylines = Vec::new(); 80 | skylines.push(Skyline { 81 | x: 0, 82 | y: 0, 83 | w, 84 | }); 85 | 86 | Packer { 87 | border: Rect::new(0, 0, w, h), 88 | skylines, 89 | } 90 | } 91 | 92 | /// Clears all packed rectangles and starts with a fresh canvas. 93 | pub fn clear(&mut self) { 94 | self.skylines.clear(); 95 | self.skylines.push(Skyline { 96 | x: 0, 97 | y: 0, 98 | w: self.border.w, 99 | }); 100 | } 101 | 102 | /// Packs a rectangle with a given width and height, returning the position of the rectangle in 103 | /// the canvas if it could be packed. 104 | pub fn pack(&mut self, width: u32, height: u32) -> Option { 105 | if let Some((i, rect)) = self.find_skyline(width, height) { 106 | self.split(i, &rect); 107 | self.merge(); 108 | return Some(rect); 109 | } 110 | None 111 | } 112 | 113 | // Return `rect` if rectangle (w, h) can fit the skyline started at `i`. 114 | fn can_put(&self, mut i: usize, w: u32, h: u32) -> Option { 115 | let mut rect = Rect::new(self.skylines[i].x, 0, w, h); 116 | let mut width_left = rect.w; 117 | loop { 118 | rect.y = rect.y.max(self.skylines[i].y); 119 | // The source rect is too large. 120 | if !self.border.contains(&rect) { 121 | return None; 122 | } 123 | if self.skylines[i].w >= width_left { 124 | return Some(rect); 125 | } 126 | width_left -= self.skylines[i].w; 127 | i += 1; 128 | assert!(i < self.skylines.len()); 129 | } 130 | } 131 | 132 | fn find_skyline(&self, w: u32, h: u32) -> Option<(usize, Rect)> { 133 | let mut bottom = core::u32::MAX; 134 | let mut width = core::u32::MAX; 135 | let mut index = None; 136 | let mut rect = Rect::new(0, 0, 0, 0); 137 | 138 | // Keep the `bottom` and `width` as small as possible. 139 | for i in 0..self.skylines.len() { 140 | if let Some(r) = self.can_put(i, w, h) { 141 | if r.bottom() < bottom || (r.bottom() == bottom && self.skylines[i].w < width) { 142 | bottom = r.bottom(); 143 | width = self.skylines[i].w; 144 | index = Some(i); 145 | rect = r; 146 | } 147 | } 148 | } 149 | 150 | if let Some(index) = index { 151 | Some((index, rect)) 152 | } else { 153 | None 154 | } 155 | } 156 | 157 | fn split(&mut self, index: usize, rect: &Rect) { 158 | let skyline = Skyline { 159 | x: rect.left(), 160 | y: rect.bottom() + 1, 161 | w: rect.w, 162 | }; 163 | 164 | assert!(skyline.right() <= self.border.right()); 165 | assert!(skyline.y <= self.border.bottom()); 166 | 167 | self.skylines.insert(index, skyline); 168 | 169 | let i = index + 1; 170 | while i < self.skylines.len() { 171 | assert!(self.skylines[i - 1].left() <= self.skylines[i].left()); 172 | 173 | if self.skylines[i].left() <= self.skylines[i - 1].right() { 174 | let shrink = self.skylines[i - 1].right() - self.skylines[i].left() + 1; 175 | if self.skylines[i].w <= shrink { 176 | self.skylines.remove(i); 177 | } else { 178 | self.skylines[i].x += shrink; 179 | self.skylines[i].w -= shrink; 180 | break; 181 | } 182 | } else { 183 | break; 184 | } 185 | } 186 | } 187 | 188 | fn merge(&mut self) { 189 | let mut i = 1; 190 | while i < self.skylines.len() { 191 | if self.skylines[i - 1].y == self.skylines[i].y { 192 | self.skylines[i - 1].w += self.skylines[i].w; 193 | self.skylines.remove(i); 194 | i -= 1; 195 | } 196 | i += 1; 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/image/png.rs: -------------------------------------------------------------------------------- 1 | use super::Image; 2 | use crate::color::RGBA8; 3 | use alloc::{vec, vec::Vec}; 4 | use png::{ColorType, Decoder}; 5 | 6 | /// Interpret a slice of bytes as a PNG and decodes it into an RGBA image. 7 | pub fn read_png(bytes: &[u8]) -> Image { 8 | let decoder = Decoder::new(bytes); 9 | let (info, mut reader) = decoder.read_info().expect("Unable to read PNG info."); 10 | let mut input = vec![0; info.buffer_size()]; 11 | reader.next_frame(&mut input).expect("Unable to read PNG payload."); 12 | 13 | match info.color_type { 14 | ColorType::RGB => { 15 | let mut output = Vec::with_capacity((input.len() / 3) * 4); 16 | for rgb in input.chunks_exact(3) { 17 | output.push(RGBA8::new(rgb[0], rgb[1], rgb[2], 255)); 18 | } 19 | Image::from_vec(output, info.width, info.height) 20 | } 21 | ColorType::RGBA => { 22 | let mut output = Vec::with_capacity(input.len()); 23 | for rgba in input.chunks_exact(4) { 24 | output.push(RGBA8::new(rgba[0], rgba[1], rgba[2], rgba[3])); 25 | } 26 | Image::from_vec(output, info.width, info.height) 27 | } 28 | ColorType::Grayscale => { 29 | let mut output = Vec::with_capacity(input.len() * 4); 30 | for g in input { 31 | output.push(RGBA8::new(g, g, g, 255)); 32 | } 33 | Image::from_vec(output, info.width, info.height) 34 | } 35 | ColorType::GrayscaleAlpha => { 36 | let mut output = Vec::with_capacity(input.len() * 2); 37 | for ga in input.chunks_exact(2) { 38 | output.push(RGBA8::new(ga[0], ga[0], ga[0], ga[1])); 39 | } 40 | Image::from_vec(output, info.width, info.height) 41 | } 42 | ColorType::Indexed => panic!("PNG Indexed color type is unsupported."), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/image/resize.rs: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2014 PistonDevelopers 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 | // 23 | // Below is from https://github.com/image-rs/image/blob/master/src/imageops/sample.rs 24 | 25 | use crate::image::Image; 26 | use crate::{color::RGBA8, math::Float}; 27 | use alloc::vec::Vec; 28 | 29 | impl Image { 30 | /// Performs a lanczos resampling of the image. 31 | pub fn resize(&self, width: u32, height: u32) -> Image { 32 | let image = vertical_sample(self, height); 33 | horizontal_sample(&image, width) 34 | } 35 | } 36 | 37 | #[inline] 38 | fn clamp(a: N, min: N, max: N) -> N { 39 | if a < min { 40 | min 41 | } else if a > max { 42 | max 43 | } else { 44 | a 45 | } 46 | } 47 | 48 | fn sinc(t: f32) -> f32 { 49 | let a = t * core::f32::consts::PI; 50 | 51 | if t == 0.0 { 52 | 1.0 53 | } else { 54 | a.sin_rad_fast() / a 55 | } 56 | } 57 | 58 | fn lanczos3(x: f32) -> f32 { 59 | const WINDOW: f32 = 3.0; 60 | if x.abs() < WINDOW { 61 | sinc(x) * sinc(x / WINDOW) 62 | } else { 63 | 0.0 64 | } 65 | } 66 | 67 | fn vertical_sample(image: &Image, new_height: u32) -> Image { 68 | const SUPPORT: f32 = 3.0; 69 | let (width, height) = (image.width(), image.height()); 70 | let mut out = Image::from_color(RGBA8::MAGENTA, width, new_height); 71 | let mut ws = Vec::new(); 72 | 73 | let ratio = height as f32 / new_height as f32; 74 | let sratio = if ratio < 1.0 { 75 | 1.0 76 | } else { 77 | ratio 78 | }; 79 | let src_support = SUPPORT * sratio; 80 | 81 | for outy in 0..new_height { 82 | let inputy = (outy as f32 + 0.5) * ratio; 83 | 84 | let left = (inputy - src_support).floor() as i64; 85 | let left = clamp(left, 0, >::from(height) - 1) as u32; 86 | 87 | let right = (inputy + src_support).ceil() as i64; 88 | let right = clamp(right, >::from(left) + 1, >::from(height)) as u32; 89 | 90 | let inputy = inputy - 0.5; 91 | 92 | ws.clear(); 93 | let mut sum = 0.0; 94 | for i in left..right { 95 | let w = lanczos3((i as f32 - inputy) / sratio); 96 | ws.push(w); 97 | sum += w; 98 | } 99 | ws.iter_mut().for_each(|w| *w /= sum); 100 | 101 | for x in 0..width { 102 | let mut t = (0.0, 0.0, 0.0, 0.0); 103 | 104 | for (i, w) in ws.iter().enumerate() { 105 | let p = image.get(x, left + i as u32); 106 | let vec: (f32, f32, f32, f32) = p.into(); 107 | t.0 += vec.0 * w; 108 | t.1 += vec.1 * w; 109 | t.2 += vec.2 * w; 110 | t.3 += vec.3 * w; 111 | } 112 | 113 | let t = RGBA8::from_f32(t.0, t.1, t.2, t.3); 114 | 115 | out.set(x, outy, t); 116 | } 117 | } 118 | 119 | out 120 | } 121 | 122 | fn horizontal_sample(image: &Image, new_width: u32) -> Image { 123 | const SUPPORT: f32 = 3.0; 124 | let (width, height) = (image.width(), image.height()); 125 | let mut out = Image::from_color(RGBA8::MAGENTA, new_width, height); 126 | let mut ws = Vec::new(); 127 | 128 | let ratio = width as f32 / new_width as f32; 129 | let sratio = if ratio < 1.0 { 130 | 1.0 131 | } else { 132 | ratio 133 | }; 134 | let src_support = SUPPORT * sratio; 135 | 136 | for outx in 0..new_width { 137 | // Find the point in the input image corresponding to the centre 138 | // of the current pixel in the output image. 139 | let inputx = (outx as f32 + 0.5) * ratio; 140 | 141 | // Left and right are slice bounds for the input pixels relevant 142 | // to the output pixel we are calculating. Pixel x is relevant 143 | // if and only if (x >= left) && (x < right). 144 | 145 | // Invariant: 0 <= left < right <= width 146 | 147 | let left = (inputx - src_support).floor() as i64; 148 | let left = clamp(left, 0, >::from(width) - 1) as u32; 149 | 150 | let right = (inputx + src_support).ceil() as i64; 151 | let right = clamp(right, >::from(left) + 1, >::from(width)) as u32; 152 | 153 | // Go back to left boundary of pixel, to properly compare with i 154 | // below, as the kernel treats the centre of a pixel as 0. 155 | let inputx = inputx - 0.5; 156 | 157 | ws.clear(); 158 | let mut sum = 0.0; 159 | for i in left..right { 160 | let w = lanczos3((i as f32 - inputx) / sratio); 161 | ws.push(w); 162 | sum += w; 163 | } 164 | ws.iter_mut().for_each(|w| *w /= sum); 165 | 166 | for y in 0..height { 167 | let mut t = (0.0, 0.0, 0.0, 0.0); 168 | 169 | for (i, w) in ws.iter().enumerate() { 170 | let p = image.get(left + i as u32, y); 171 | let vec: (f32, f32, f32, f32) = p.into(); 172 | t.0 += vec.0 * w; 173 | t.1 += vec.1 * w; 174 | t.2 += vec.2 * w; 175 | t.3 += vec.3 * w; 176 | } 177 | 178 | let t = RGBA8::from_f32( 179 | clamp(t.0, 0.0, 1.0), 180 | clamp(t.1, 0.0, 1.0), 181 | clamp(t.2, 0.0, 1.0), 182 | clamp(t.3, 0.0, 1.0), 183 | ); 184 | 185 | out.set(outx, y, t); 186 | } 187 | } 188 | 189 | out 190 | } 191 | -------------------------------------------------------------------------------- /src/math/aabb.rs: -------------------------------------------------------------------------------- 1 | use cgmath::*; 2 | 3 | /// Provides tools for interacting with and creating axis aligned bounding boxes in 2D. 4 | #[derive(Debug, Clone, Copy, PartialEq)] 5 | pub struct AABB2D { 6 | pub min: Vector2, 7 | pub max: Vector2, 8 | } 9 | 10 | impl AABB2D { 11 | pub const fn new(minx: f32, miny: f32, maxx: f32, maxy: f32) -> AABB2D { 12 | AABB2D { 13 | min: Vector2 { 14 | x: minx, 15 | y: miny, 16 | }, 17 | max: Vector2 { 18 | x: maxx, 19 | y: maxy, 20 | }, 21 | } 22 | } 23 | 24 | pub fn from_min_max(min: Vector2, max: Vector2) -> AABB2D { 25 | AABB2D { 26 | min, 27 | max, 28 | } 29 | } 30 | 31 | pub fn from_pos_size(pos: Vector2, size: Vector2) -> AABB2D { 32 | AABB2D { 33 | min: pos, 34 | max: pos + size, 35 | } 36 | } 37 | 38 | #[inline(always)] 39 | pub fn intersects(&self, other: &AABB2D) -> bool { 40 | self.min.x <= other.max.x 41 | && self.max.x >= other.min.x 42 | && self.min.y <= other.max.y 43 | && self.max.y >= other.min.y 44 | } 45 | 46 | #[inline(always)] 47 | pub fn contains(&self, other: &AABB2D) -> bool { 48 | self.min.x <= other.min.x 49 | && self.max.x >= other.max.x 50 | && self.min.y <= other.min.y 51 | && self.max.y >= other.max.y 52 | } 53 | 54 | #[inline(always)] 55 | pub fn contains_point(&self, point: &Vector2) -> bool { 56 | self.min.x <= point.x && self.max.x >= point.x && self.min.y <= point.y && self.max.y >= point.y 57 | } 58 | 59 | pub fn slide(&mut self, mov: &Vector2, others: &[AABB2D]) -> bool { 60 | if mov.x == 0f32 && mov.y == 0f32 { 61 | return false; 62 | } 63 | let mut result = false; 64 | let mut res = *mov; 65 | let mut aabb = *self; 66 | 67 | // Y movement 68 | 69 | if mov.y < 0f32 { 70 | for other in others { 71 | if aabb.max.x > other.min.x && aabb.min.x < other.max.x && other.max.y <= aabb.min.y { 72 | let min = other.max.y - aabb.min.y; 73 | if min > res.y { 74 | result = true; 75 | res.y = min; 76 | } 77 | } 78 | } 79 | } else if mov.y > 0f32 { 80 | for other in others { 81 | if aabb.max.x > other.min.x && aabb.min.x < other.max.x && other.min.y >= aabb.max.y { 82 | let max = other.min.y - aabb.max.y; 83 | if max < res.y { 84 | result = true; 85 | res.y = max; 86 | } 87 | } 88 | } 89 | } 90 | 91 | aabb.min.y += res.y; 92 | aabb.max.y += res.y; 93 | 94 | // X movement 95 | 96 | if mov.x < 0f32 { 97 | for other in others { 98 | if aabb.max.y > other.min.y && aabb.min.y < other.max.y && other.max.x <= aabb.min.x { 99 | let min = other.max.x - aabb.min.x; 100 | if min > res.x { 101 | result = true; 102 | res.x = min; 103 | } 104 | } 105 | } 106 | } else if mov.x > 0f32 { 107 | for other in others { 108 | if aabb.max.y > other.min.y && aabb.min.y < other.max.y && other.min.x >= aabb.max.x { 109 | let max = other.min.x - aabb.max.x; 110 | if max < res.x { 111 | result = true; 112 | res.x = max; 113 | } 114 | } 115 | } 116 | } 117 | 118 | aabb.min.x += res.x; 119 | aabb.max.x += res.x; 120 | *self = aabb; 121 | 122 | result 123 | } 124 | } 125 | 126 | // //////////////////////////////////////////////////////////////////////////// 127 | // Tests 128 | // //////////////////////////////////////////////////////////////////////////// 129 | 130 | #[cfg(test)] 131 | mod tests { 132 | #![allow(unused_imports)] 133 | use super::*; 134 | use alloc::vec; 135 | 136 | #[test] 137 | fn slide() { 138 | let v = vec![AABB2D::new(2f32, 0f32, 3f32, 1f32), AABB2D::new(0f32, 1f32, 1f32, 2f32)]; 139 | let mut aabb = AABB2D::new(0f32, 0f32, 1f32, 1f32); 140 | 141 | { 142 | let mot = Vector2::new(2f32, 0f32); 143 | aabb.slide(&mot, &v); 144 | assert_eq!(aabb, AABB2D::new(1f32, 0f32, 2f32, 1f32)); 145 | } 146 | 147 | { 148 | let mot = Vector2::new(-4f32, 1f32); 149 | aabb.slide(&mot, &v); 150 | assert_eq!(aabb, AABB2D::new(1f32, 1f32, 2f32, 2f32)); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/math/interpolation.rs: -------------------------------------------------------------------------------- 1 | use crate::math::lerp; 2 | 3 | /// Provides functions for interpolating between two f32s. 4 | pub struct Interpolation { 5 | /// Initial value. 6 | start: f32, 7 | /// Target value. 8 | end: f32, 9 | /// Value progress from 0 to 1 between the `start` and `end`. 10 | progress: f32, 11 | } 12 | 13 | impl Interpolation { 14 | /// Creates a new interpolation between start and end, starting with a progress of 0.0. 15 | pub fn new(start: f32, end: f32) -> Interpolation { 16 | Interpolation { 17 | start, 18 | end, 19 | progress: 0.0, 20 | } 21 | } 22 | 23 | /// Gets the set start value. 24 | pub fn start(&self) -> f32 { 25 | self.start 26 | } 27 | 28 | /// Gets the set end value. 29 | pub fn end(&self) -> f32 { 30 | self.end 31 | } 32 | 33 | /// Gets the current progress. 34 | pub fn progress(&self) -> f32 { 35 | self.progress 36 | } 37 | 38 | /// Computes the current value of the interpolation. 39 | pub fn get(&self) -> f32 { 40 | lerp(self.start, self.end, self.progress) 41 | } 42 | 43 | /// Sets the start and end of the interpolation, restarting progress. 44 | pub fn set(&mut self, start: f32, end: f32) { 45 | self.start = start; 46 | self.end = end; 47 | self.progress = 0.0; 48 | } 49 | 50 | /// Updates the end of the interpolation, continuing from the current value, and restarting 51 | /// progress. 52 | pub fn update(&mut self, end: f32) { 53 | self.start = self.get(); 54 | self.end = end; 55 | self.progress = 0.0; 56 | } 57 | 58 | /// Adds the given delta to the progress of the interpolation. Progress is tracked as a value 59 | /// between [0, 1] 60 | pub fn advance(&mut self, progress: f32) { 61 | self.progress += progress; 62 | if self.progress > 1.0 { 63 | self.progress = 1.0; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/math/mod.rs: -------------------------------------------------------------------------------- 1 | mod aabb; 2 | mod interpolation; 3 | mod num; 4 | mod orthographic; 5 | mod perspective; 6 | 7 | use cgmath::Vector2; 8 | 9 | pub use self::aabb::AABB2D; 10 | pub use self::interpolation::Interpolation; 11 | pub use self::num::{Float, UnsignedInteger}; 12 | pub use self::orthographic::{ortho_from_bounds, OrthographicCamera}; 13 | pub use self::perspective::PerspectiveCamera; 14 | 15 | /// Represents 2 * pi. 16 | pub const TAO: f32 = 6.283_185_307_179_586_476f32; 17 | /// Represents pi. 18 | pub const PI: f32 = 3.141_592_653_589_793_238f32; 19 | /// Represents pi / 2. 20 | pub const PI_2: f32 = 1.570_796_326_794_896_619f32; 21 | /// Represents -pi / 2. 22 | pub const PI_NEG_2: f32 = -1.570_796_326_794_896_619f32; 23 | /// Simple const identity matrix. 24 | pub const IDENTITY_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( 25 | 1.0, 0.0, 0.0, 0.0, // 26 | 0.0, 1.0, 0.0, 0.0, // 27 | 0.0, 0.0, 1.0, 0.0, // 28 | 0.0, 0.0, 0.0, 1.0, // 29 | ); 30 | 31 | /// Fast `Vector2` normalization. 32 | pub fn fast_normalize2(vector: Vector2) -> Vector2 { 33 | vector * (vector.x * vector.x + vector.y * vector.y).inv_sqrt() 34 | } 35 | 36 | /// Linearly interpolates between a and b by t. 37 | pub fn lerp(a: f32, b: f32, t: f32) -> f32 { 38 | a + t * (b - a) 39 | } 40 | -------------------------------------------------------------------------------- /src/math/num/mod.rs: -------------------------------------------------------------------------------- 1 | mod nostd; 2 | mod trigonometry; 3 | 4 | use self::trigonometry::{atan2, cos_deg, cos_rad, sin_deg, sin_rad}; 5 | use crate::graphics::IndiceType; 6 | 7 | /// Extra functions on floating point values. 8 | pub trait Float { 9 | /// Computes the sine of a number. Input in degrees, output in radians. 10 | /// 11 | /// * Average error of 0.00060 radians. 12 | /// * Largest error of 0.00229 radians. 13 | /// * Speedup of 30x over f32.sin(); 14 | fn sin_deg_fast(self) -> Self; 15 | 16 | /// Computes the sine of a number. Input in radians, output in radians. 17 | /// 18 | /// * Average error of 0.00060 radians. 19 | /// * Largest error of 0.00229 radians. 20 | /// * Speedup of 30x over f32.sin(); 21 | fn sin_rad_fast(self) -> Self; 22 | 23 | /// Computes the cosine of a number. Input in degrees, output in radians. 24 | /// 25 | /// * Average error of 0.00060 radians. 26 | /// * Largest error of 0.00229 radians. 27 | /// * Speedup of 30x over f32.cos(); 28 | fn cos_deg_fast(self) -> Self; 29 | 30 | /// Computes the cosine of a number. Input in radians, output in radians. 31 | /// 32 | /// * Average error of 0.00060 radians. 33 | /// * Largest error of 0.00229 radians. 34 | /// * Speedup of 30x over f32.cos(); 35 | fn cos_rad_fast(self) -> Self; 36 | 37 | /// Computes the four quadrant arctangent of self (y) and other (x) in radians. 38 | /// 39 | /// * Average error of 0.00231 radians. 40 | /// * Largest error of 0.00488 radians. 41 | /// * Speedup of 20.67x over f32.atan2(y); 42 | fn atan2_fast(self, x: f32) -> Self; 43 | 44 | /// Quake fast inverse square root. 45 | fn inv_sqrt(self) -> Self; 46 | } 47 | 48 | /// Extra functions on unsigned integers. 49 | pub trait UnsignedInteger: Copy + Clone { 50 | const INDICE_TYPE: IndiceType; 51 | } 52 | 53 | impl UnsignedInteger for u8 { 54 | const INDICE_TYPE: IndiceType = IndiceType::UnsignedByte; 55 | } 56 | 57 | impl UnsignedInteger for u16 { 58 | const INDICE_TYPE: IndiceType = IndiceType::UnsignedShort; 59 | } 60 | 61 | impl UnsignedInteger for u32 { 62 | const INDICE_TYPE: IndiceType = IndiceType::UnsignedInt; 63 | } 64 | -------------------------------------------------------------------------------- /src/math/num/nostd.rs: -------------------------------------------------------------------------------- 1 | use crate::math::Float; 2 | 3 | impl Float for f32 { 4 | fn sin_deg_fast(self) -> Self { 5 | super::sin_deg(self) 6 | } 7 | 8 | fn sin_rad_fast(self) -> Self { 9 | super::sin_rad(self) 10 | } 11 | 12 | fn cos_deg_fast(self) -> Self { 13 | super::cos_deg(self) 14 | } 15 | 16 | fn cos_rad_fast(self) -> Self { 17 | super::cos_rad(self) 18 | } 19 | 20 | fn atan2_fast(self, x: f32) -> Self { 21 | super::atan2(self, x) 22 | } 23 | 24 | fn inv_sqrt(self) -> Self { 25 | let i = self.to_bits(); 26 | let i = 0x5f3759df - (i >> 1); 27 | let y = f32::from_bits(i); 28 | y * (1.5 - 0.5 * self * y * y) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/math/orthographic.rs: -------------------------------------------------------------------------------- 1 | use crate::math::IDENTITY_MATRIX; 2 | use cgmath::*; 3 | 4 | /// Creates an orthographic matrix from screen bounds with a fixed aspect ratio and with 0,0 in the 5 | /// center. 6 | pub fn ortho_from_bounds(bounds: &Vector2) -> Matrix4 { 7 | let w = bounds.x / 2.0; 8 | let h = bounds.y / 2.0; 9 | ortho(-w.floor(), w.ceil(), -h.floor(), h.ceil(), -1.0, 1.0) 10 | } 11 | 12 | pub struct OrthographicParams { 13 | /// The translation of the layer. 14 | pub translation: Vector3, 15 | /// The zoom level of the layer. This is 1.0 by default, meaning 1 pixel takes up 1x1 pixels on 16 | /// screen. 17 | pub scale: f32, 18 | /// Rotation is measured in turns from [0, 1). Values outside of the range are wrapped into the 19 | /// range. For example, 1.75 is wrapped into 0.75, -0.4 is wrapped into 0.6. 20 | pub rotation: f32, 21 | } 22 | 23 | /// Simple camera for orthographic projections. 24 | pub struct OrthographicCamera { 25 | params: OrthographicParams, 26 | pixel_perfect: bool, 27 | logical_size: Vector2, 28 | 29 | transform: Matrix4, 30 | transform_dirty: bool, 31 | ortho: Matrix4, 32 | ortho_dirty: bool, 33 | ortho_transform: Matrix4, 34 | ortho_transform_dirty: bool, 35 | } 36 | 37 | impl OrthographicCamera { 38 | pub fn new(logical_size: Vector2) -> OrthographicCamera { 39 | OrthographicCamera { 40 | params: OrthographicParams { 41 | translation: Vector3::zero(), 42 | scale: 1.0, 43 | rotation: 0.0, 44 | }, 45 | pixel_perfect: true, 46 | logical_size, 47 | 48 | transform: IDENTITY_MATRIX, 49 | transform_dirty: false, 50 | ortho: IDENTITY_MATRIX, 51 | ortho_dirty: true, 52 | ortho_transform: IDENTITY_MATRIX, 53 | ortho_transform_dirty: true, 54 | } 55 | } 56 | 57 | /// Gets an immutable reference to the transform parameters. 58 | pub fn get(&self) -> &OrthographicParams { 59 | &self.params 60 | } 61 | 62 | /// Gets an mutable reference to the transform parameters. 63 | pub fn set(&mut self) -> &mut OrthographicParams { 64 | self.transform_dirty = true; 65 | self.ortho_transform_dirty = true; 66 | &mut self.params 67 | } 68 | 69 | /// Flags pixel perfect alignment. 70 | pub fn set_pixel_perfect(&mut self, pixel_perfect: bool) { 71 | if self.pixel_perfect != pixel_perfect { 72 | self.pixel_perfect = pixel_perfect; 73 | self.transform_dirty = true; 74 | self.ortho_transform_dirty = true; 75 | } 76 | } 77 | 78 | /// Logical size of the viewport. 79 | pub fn set_size(&mut self, logical_size: Vector2) { 80 | self.ortho_dirty = true; 81 | self.ortho_transform_dirty = true; 82 | self.logical_size = logical_size; 83 | } 84 | 85 | /// Creates a new transform matix based on the parameters of the LayerTransform. The transform 86 | /// matrix is built in this order: Scale * Translation * Rotation. 87 | pub fn matrix(&mut self) -> Matrix4 { 88 | if self.transform_dirty { 89 | let mut translation = self.params.translation; 90 | if self.pixel_perfect { 91 | translation.x = (translation.x * self.params.scale).floor() / self.params.scale; 92 | translation.y = (translation.y * self.params.scale).floor() / self.params.scale; 93 | } 94 | self.transform = Matrix4::from_scale(self.params.scale) 95 | * Matrix4::from_translation(translation) 96 | * Matrix4::from_angle_z(Rad(core::f32::consts::PI * 2.0 * self.params.rotation)); 97 | self.transform_dirty = false; 98 | } 99 | 100 | if self.ortho_dirty { 101 | self.ortho = ortho_from_bounds(&self.logical_size); 102 | self.ortho_dirty = false; 103 | } 104 | 105 | if self.ortho_transform_dirty { 106 | self.ortho_transform = self.ortho * self.transform; 107 | self.ortho_transform_dirty = false; 108 | } 109 | 110 | self.ortho_transform 111 | } 112 | 113 | /// Transforms the normalized position into a position in the transform. Typically you'll use 114 | /// the mouse's normalized position and convert that into world space. 115 | pub fn screen_to_world(&mut self, normalized_pos: Vector2) -> Vector2 { 116 | let matrix = self.matrix().invert().unwrap(); 117 | let value = Vector4::new(normalized_pos.x, normalized_pos.y, 0.0, 1.0); 118 | let value = matrix * value; 119 | Vector2::new(value.x, value.y) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/math/perspective.rs: -------------------------------------------------------------------------------- 1 | use crate::math::IDENTITY_MATRIX; 2 | use cgmath::*; 3 | 4 | /// Parameters 5 | pub struct PerspectiveParams { 6 | /// The position of the camera. 7 | pub eye: Vector3, 8 | /// The direction the camera is looking 9 | pub direction: Vector3, 10 | } 11 | 12 | /// Simple camera for perspective projections. +Y is up, right handed system. Depth is inverted. 13 | pub struct PerspectiveCamera { 14 | eye: Vector3, 15 | direction: Vector3, 16 | aspect: f32, 17 | fov: Rad, 18 | 19 | view: Matrix4, 20 | view_dirty: bool, 21 | proj: Matrix4, 22 | proj_dirty: bool, 23 | proj_transform: Matrix4, 24 | proj_transform_dirty: bool, 25 | } 26 | 27 | impl PerspectiveCamera { 28 | pub fn new(logical_size: Vector2) -> PerspectiveCamera { 29 | PerspectiveCamera { 30 | eye: Vector3::new(0.0, 0.0, 0.0), 31 | direction: Vector3::new(1.0, 0.0, 0.0), 32 | aspect: logical_size.x / logical_size.y, 33 | fov: Deg(90.0).into(), 34 | 35 | view: IDENTITY_MATRIX, 36 | view_dirty: false, 37 | proj: IDENTITY_MATRIX, 38 | proj_dirty: true, 39 | proj_transform: IDENTITY_MATRIX, 40 | proj_transform_dirty: true, 41 | } 42 | } 43 | 44 | /// The direction the camera is looking 45 | pub fn set_direction(&mut self, direction: Vector3) { 46 | self.direction = direction; 47 | self.view_dirty = true; 48 | self.proj_transform_dirty = true; 49 | } 50 | 51 | /// The position of the camera. 52 | pub fn set_eye(&mut self, eye: Vector3) { 53 | self.eye = eye; 54 | self.view_dirty = true; 55 | self.proj_transform_dirty = true; 56 | } 57 | 58 | /// Logical size of the viewport. 59 | pub fn set_size(&mut self, logical_size: Vector2) { 60 | self.aspect = logical_size.x / logical_size.y; 61 | self.proj_dirty = true; 62 | self.proj_transform_dirty = true; 63 | } 64 | 65 | /// Sets the FOV in degrees. 66 | pub fn set_fov(&mut self, fov: f32) { 67 | self.fov = Deg(fov).into(); 68 | self.proj_dirty = true; 69 | self.proj_transform_dirty = true; 70 | } 71 | 72 | /// Creates a new transform matix based on the parameters of the LayerTransform. 73 | pub fn matrix(&mut self) -> Matrix4 { 74 | if self.proj_transform_dirty { 75 | if self.view_dirty { 76 | self.view = view(self.eye, self.direction); 77 | self.view_dirty = false; 78 | } 79 | if self.proj_dirty { 80 | self.proj = projection(self.fov, self.aspect, 0.01); 81 | self.proj_dirty = false; 82 | } 83 | self.proj_transform = self.proj * self.view; 84 | self.proj_transform_dirty = false; 85 | } 86 | 87 | self.proj_transform 88 | } 89 | 90 | /// Given the mouse's normalized position, this returns a point on the near plane. 91 | pub fn screen_to_world_pos(&mut self, normalized_pos: Vector2) -> Vector3 { 92 | let matrix = self.matrix().invert().unwrap(); 93 | let value = Vector4::new(normalized_pos.x, normalized_pos.y, 1.0, 1.0); 94 | let value = matrix * value; 95 | Vector3::new(value.x, value.y, value.z) * value.w.recip() 96 | } 97 | 98 | /// Given the mouse's normalized position, this returns the direction from the camera to the 99 | /// position of the mouse transformed onto the near plane. 100 | pub fn screen_to_world_dir(&mut self, normalized_pos: Vector2) -> Vector3 { 101 | let world = self.screen_to_world_pos(normalized_pos); 102 | (world - self.eye).normalize() 103 | } 104 | } 105 | 106 | // Right handed, +Y is up. 107 | #[rustfmt::skip] 108 | fn view(eye: Vector3, dir: Vector3) -> Matrix4 { 109 | let f = dir.normalize(); 110 | 111 | // let s = f.cross(up).normalize(); 112 | let s = Vector3::new(-f.z, 0.0, f.x); 113 | let mag = (s.x * s.x + s.z * s.z).sqrt().recip(); 114 | let s = Vector3::new(s.x * mag, 0.0, s.z * mag); 115 | 116 | // let u = s.cross(f); 117 | let u = Vector3::new( 118 | -(s.z * f.y), 119 | (s.z * f.x) - (s.x * f.z), 120 | s.x * f.y, 121 | ); 122 | 123 | Matrix4::new( 124 | s.x, u.x, -f.x, 0.0, 125 | s.y, u.y, -f.y, 0.0, 126 | s.z, u.z, -f.z, 0.0, 127 | -eye.dot(s), -eye.dot(u), eye.dot(f), 1.0, 128 | ) 129 | } 130 | 131 | #[rustfmt::skip] 132 | fn projection(fovy: Rad, aspect: f32, near: f32) -> Matrix4 { 133 | let f = Rad::cot(fovy / 2.0); 134 | Matrix4::new( 135 | f / aspect, 0.0, 0.0, 0.0, 136 | 0.0, f, 0.0, 0.0, 137 | 0.0, 0.0, 0.0, -1.0, 138 | 0.0, 0.0, near, 0.0) 139 | } 140 | -------------------------------------------------------------------------------- /src/noise/add.rs: -------------------------------------------------------------------------------- 1 | use super::NoiseFn; 2 | use core::simd::{LaneCount, Simd, SupportedLaneCount}; 3 | 4 | /// Noise function that outputs the sum of the two noise function values. 5 | #[derive(Clone, Copy, Debug)] 6 | pub struct Add 7 | where 8 | A: NoiseFn, 9 | B: NoiseFn, 10 | { 11 | pub lhs: A, 12 | pub rhs: B, 13 | } 14 | 15 | impl, B: NoiseFn> Add { 16 | pub fn new(lhs: A, rhs: B) -> Self { 17 | Self { 18 | lhs, 19 | rhs, 20 | } 21 | } 22 | } 23 | 24 | impl NoiseFn for Add 25 | where 26 | A: NoiseFn, 27 | B: NoiseFn, 28 | { 29 | #[inline(always)] 30 | fn sample(&self, x: [Simd; DIM]) -> Simd 31 | where 32 | LaneCount: SupportedLaneCount, 33 | { 34 | self.lhs.sample(x) + self.rhs.sample(x) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/noise/constant.rs: -------------------------------------------------------------------------------- 1 | use super::NoiseFn; 2 | use core::simd::{LaneCount, Simd, SupportedLaneCount}; 3 | 4 | /// Noise function that outputs a constant value. 5 | /// 6 | /// This function takes a input, value, and returns that input for all points, 7 | /// producing a constant-valued field. 8 | /// 9 | /// This function is not very useful by itself, but can be used as a source 10 | /// function for other noise functions. 11 | #[derive(Clone, Copy, Debug)] 12 | pub struct Constant { 13 | pub value: f32, 14 | } 15 | 16 | impl Constant { 17 | pub fn new(value: f32) -> Self { 18 | Self { 19 | value, 20 | } 21 | } 22 | } 23 | 24 | impl Default for Constant { 25 | fn default() -> Self { 26 | Self { 27 | value: 0.0, 28 | } 29 | } 30 | } 31 | 32 | impl NoiseFn for Constant { 33 | #[inline(always)] 34 | fn sample(&self, _: [Simd; DIM]) -> Simd 35 | where 36 | LaneCount: SupportedLaneCount, 37 | { 38 | Simd::splat(self.value) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/noise/fbm.rs: -------------------------------------------------------------------------------- 1 | use super::NoiseFn; 2 | // use core::simd::{LaneCount, Simd, SupportedLaneCount}; 3 | 4 | /// Noise function that outputs fBm (fractal Brownian motion) noise. 5 | /// 6 | /// fBm is a _monofractal_ method. In essence, fBm has a _constant_ fractal dimension. It is as 7 | /// close to statistically _homogeneous_ and _isotropic_ as possible. Homogeneous means "the same 8 | /// everywhere" and isotropic means "the same in all directions" (note that the two do not mean the 9 | /// same 10 | /// thing). 11 | /// 12 | /// The main difference between fractal Brownian motion and regular Brownian motion is that while 13 | /// the increments in Brownian motion are independent, the increments in fractal Brownian motion 14 | /// depend on the previous increment. 15 | /// 16 | /// fBm is the result of several noise functions of ever-increasing frequency and ever-decreasing 17 | /// amplitude. 18 | #[derive(Clone, Copy, Debug)] 19 | struct Fbm> { 20 | pub source: T, 21 | /// Total number of frequency octaves to generate the noise with. 22 | /// 23 | /// The number of octaves control the _amount of detail_ in the noise 24 | /// function. Adding more octaves increases the detail, with the drawback 25 | /// of increasing the calculation time. 26 | pub octaves: usize, 27 | pub gain: f32, 28 | /// A multiplier that determines how quickly the frequency increases for 29 | /// each successive octave in the noise function. 30 | /// 31 | /// The frequency of each successive octave is equal to the product of the 32 | /// previous octave's frequency and the lacunarity value. 33 | /// 34 | /// A lacunarity of 2.0 results in the frequency doubling every octave. For 35 | /// almost all cases, 2.0 is a good value to use. 36 | pub lacunarity: f32, 37 | } 38 | 39 | impl> Fbm { 40 | pub fn new(source: T, octaves: usize, gain: f32, lacunarity: f32) -> Self { 41 | Self { 42 | source, 43 | octaves, 44 | gain, 45 | lacunarity, 46 | } 47 | } 48 | } 49 | 50 | // impl NoiseFn for Add 51 | // where 52 | // A: NoiseFn, 53 | // B: NoiseFn, 54 | // { 55 | // #[inline(always)] 56 | // fn sample(&self, x: [Simd; DIM]) -> Simd 57 | // where 58 | // LaneCount: SupportedLaneCount, 59 | // { 60 | // self.first.sample(x) + self.second.sample(x) 61 | // } 62 | // } 63 | 64 | // struct Fbm {} 65 | 66 | // fn fbm( 67 | // octaves: usize, 68 | // gain: f32, 69 | // lacunarity: f32, 70 | // point: [Simd; 2], 71 | // ) -> Sample 72 | // where 73 | // LaneCount: SupportedLaneCount, 74 | // { 75 | // const NOISE: Simplex2d = Simplex2d::new(); 76 | 77 | // let mut result = Sample::default(); 78 | 79 | // let mut frequency = 1.0; 80 | // let mut amplitude = 1.0; 81 | // let mut scale = 0.0; 82 | // for _ in 0..octaves { 83 | // result += NOISE.sample(point.map(|x| x * Simd::splat(frequency))) * Simd::splat(amplitude); 84 | // scale += amplitude; 85 | // frequency *= lacunarity; 86 | // amplitude *= gain; 87 | // } 88 | // result / Simd::splat(scale) 89 | // } 90 | -------------------------------------------------------------------------------- /src/noise/fill.rs: -------------------------------------------------------------------------------- 1 | use super::NoiseFn; 2 | use alloc::vec::Vec; 3 | use core::simd::{LaneCount, Simd, SupportedLaneCount}; 4 | 5 | /// Base trait for aggregating noise. 6 | /// 7 | /// A fill function is a struct that calculates and outputs a value given a n-dimensional input 8 | /// value and aggregates it into a 1 dimensional vector. 9 | pub trait FillFn { 10 | fn fill( 11 | &self, 12 | offset: [f32; DIM], 13 | size: [usize; DIM], 14 | step: f32, 15 | output: &mut Vec, 16 | ) where 17 | LaneCount: SupportedLaneCount; 18 | } 19 | 20 | fn increment(step: f32) -> Simd 21 | where 22 | LaneCount: SupportedLaneCount, 23 | { 24 | let mut inc = Simd::splat(0.0); 25 | let mut next = step; 26 | for i in 1..LANES { 27 | inc[i] += next; 28 | next += step; 29 | } 30 | inc 31 | } 32 | 33 | impl> FillFn<1> for T { 34 | fn fill( 35 | &self, 36 | [offset_x]: [f32; 1], 37 | [size_x]: [usize; 1], 38 | step: f32, 39 | output: &mut Vec, 40 | ) where 41 | LaneCount: SupportedLaneCount, 42 | { 43 | let inc_x = increment(step) + Simd::splat(offset_x); 44 | let mut x = 0; 45 | 'loop_x: loop { 46 | let px = Simd::splat(x as f32 * step) + inc_x; 47 | let sample = self.sample([px]); 48 | for i in 0..LANES { 49 | if x + i >= size_x { 50 | break 'loop_x; 51 | } 52 | output.push(sample[i]); 53 | } 54 | x += LANES; 55 | } 56 | } 57 | } 58 | 59 | impl> FillFn<2> for T { 60 | fn fill( 61 | &self, 62 | [offset_x, offset_y]: [f32; 2], 63 | [size_x, size_y]: [usize; 2], 64 | step: f32, 65 | output: &mut Vec, 66 | ) where 67 | LaneCount: SupportedLaneCount, 68 | { 69 | let inc_x = increment(step) + Simd::splat(offset_x); 70 | let offset_y = Simd::splat(offset_y); 71 | for y in 0..size_y { 72 | let py = Simd::splat(y as f32 * step) + offset_y; 73 | let mut x = 0; 74 | 'loop_x: loop { 75 | let px = Simd::splat(x as f32 * step) + inc_x; 76 | let sample = self.sample([px, py]); 77 | for i in 0..LANES { 78 | if x + i >= size_x { 79 | break 'loop_x; 80 | } 81 | output.push(sample[i]); 82 | } 83 | x += LANES; 84 | } 85 | } 86 | } 87 | } 88 | 89 | impl> FillFn<3> for T { 90 | fn fill( 91 | &self, 92 | [offset_x, offset_y, offset_z]: [f32; 3], 93 | [size_x, size_y, size_z]: [usize; 3], 94 | step: f32, 95 | output: &mut Vec, 96 | ) where 97 | LaneCount: SupportedLaneCount, 98 | { 99 | let inc_x = increment(step) + Simd::splat(offset_x); 100 | let offset_y = Simd::splat(offset_y); 101 | let offset_z = Simd::splat(offset_z); 102 | for z in 0..size_z { 103 | let pz = Simd::splat(z as f32 * step) + offset_z; 104 | for y in 0..size_y { 105 | let py = Simd::splat(y as f32 * step) + offset_y; 106 | let mut x = 0; 107 | 'loop_x: loop { 108 | let px = Simd::splat(x as f32 * step) + inc_x; 109 | let sample = self.sample([px, py, pz]); 110 | for i in 0..LANES { 111 | if x + i >= size_x { 112 | break 'loop_x; 113 | } 114 | output.push(sample[i]); 115 | } 116 | x += LANES; 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | impl> FillFn<4> for T { 124 | fn fill( 125 | &self, 126 | [offset_x, offset_y, offset_z, offset_w]: [f32; 4], 127 | [size_x, size_y, size_z, size_w]: [usize; 4], 128 | step: f32, 129 | output: &mut Vec, 130 | ) where 131 | LaneCount: SupportedLaneCount, 132 | { 133 | let inc_x = increment(step) + Simd::splat(offset_x); 134 | let offset_y = Simd::splat(offset_y); 135 | let offset_z = Simd::splat(offset_z); 136 | let offset_w = Simd::splat(offset_w); 137 | for w in 0..size_w { 138 | let pw = Simd::splat(w as f32 * step) + offset_w; 139 | for z in 0..size_z { 140 | let pz = Simd::splat(z as f32 * step) + offset_z; 141 | for y in 0..size_y { 142 | let py = Simd::splat(y as f32 * step) + offset_y; 143 | let mut x = 0; 144 | 'loop_x: loop { 145 | let px = Simd::splat(x as f32 * step) + inc_x; 146 | let sample = self.sample([px, py, pz, pw]); 147 | for i in 0..LANES { 148 | if x + i >= size_x { 149 | break 'loop_x; 150 | } 151 | output.push(sample[i]); 152 | } 153 | x += LANES; 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/noise/mod.rs: -------------------------------------------------------------------------------- 1 | mod add; 2 | mod constant; 3 | mod fbm; 4 | mod fill; 5 | mod multiply; 6 | mod simplex; 7 | 8 | pub use self::add::Add; 9 | pub use self::constant::Constant; 10 | pub use self::multiply::Multiply; 11 | pub use self::simplex::Simplex; 12 | pub use fill::FillFn; 13 | 14 | use core::simd::{LaneCount, Simd, SupportedLaneCount}; 15 | 16 | /// Base trait for noise functions. 17 | /// 18 | /// A noise function is a struct that calculates and outputs a value given a n-dimensional input 19 | /// value. 20 | pub trait NoiseFn: Sized { 21 | fn sample(&self, x: [Simd; DIM]) -> Simd 22 | where 23 | LaneCount: SupportedLaneCount; 24 | 25 | fn add(self, other: T) -> Add 26 | where 27 | T: NoiseFn, 28 | { 29 | Add::new(self, other) 30 | } 31 | 32 | fn multiply(self, other: T) -> Multiply 33 | where 34 | T: NoiseFn, 35 | { 36 | Multiply::new(self, other) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/noise/multiply.rs: -------------------------------------------------------------------------------- 1 | use super::NoiseFn; 2 | use core::simd::{LaneCount, Simd, SupportedLaneCount}; 3 | 4 | /// Noise function that outputs the product of the two noise function values. 5 | #[derive(Clone, Copy, Debug)] 6 | pub struct Multiply 7 | where 8 | A: NoiseFn, 9 | B: NoiseFn, 10 | { 11 | pub lhs: A, 12 | pub rhs: B, 13 | } 14 | 15 | impl, B: NoiseFn> Multiply { 16 | pub fn new(lhs: A, rhs: B) -> Self { 17 | Self { 18 | lhs, 19 | rhs, 20 | } 21 | } 22 | } 23 | 24 | impl NoiseFn for Multiply 25 | where 26 | A: NoiseFn, 27 | B: NoiseFn, 28 | { 29 | #[inline(always)] 30 | fn sample(&self, x: [Simd; DIM]) -> Simd 31 | where 32 | LaneCount: SupportedLaneCount, 33 | { 34 | self.lhs.sample(x) * self.rhs.sample(x) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/noise/simplex/dim1.rs: -------------------------------------------------------------------------------- 1 | // This file is licensed under multiple licenses, and is a modified version of 2 | // https://github.com/Ralith/clatter/blob/main/src/simplex.rs. A copy of the licenses can be found 3 | // here: in the path licenses/Ralith/clatter. 4 | 5 | use super::{hash, NoiseFn, Simplex}; 6 | use core::simd::{LaneCount, Simd, SimdPartialEq, SupportedLaneCount}; 7 | use std::simd::{SimdFloat, SimdInt, StdFloat}; 8 | 9 | impl NoiseFn<1> for Simplex { 10 | #[inline(always)] 11 | fn sample(&self, [x]: [Simd; 1]) -> Simd 12 | where 13 | LaneCount: SupportedLaneCount, 14 | { 15 | // Gradients are selected deterministically based on the whole part of `x` 16 | let i = x.floor(); 17 | let i0 = i.cast::(); 18 | let i1 = i0 + Simd::splat(1); 19 | 20 | // the fractional part of x, i.e. the distance to the left gradient node. 0 ≤ x0 < 1. 21 | let x0 = x - i; 22 | // signed distance to the right gradient node 23 | let x1 = x0 - Simd::splat(1.0); 24 | 25 | // Select gradients 26 | let gi0 = hash::pcg(Simd::splat(self.seed) ^ i0.cast()); 27 | let gi1 = hash::pcg(Simd::splat(self.seed) ^ i1.cast()); 28 | 29 | // Compute the contribution from the first gradient 30 | // n0 = grad0 * (1 - x0^2)^4 * x0 31 | let x20 = x0 * x0; 32 | let t0 = Simd::::splat(1.0) - x20; 33 | let t20 = t0 * t0; 34 | let t40 = t20 * t20; 35 | let gx0 = gradient_1d::(gi0); 36 | let n0 = t40 * gx0 * x0; 37 | 38 | // Compute the contribution from the second gradient 39 | // n1 = grad1 * (x0 - 1) * (1 - (x0 - 1)^2)^4 40 | let x21 = x1 * x1; 41 | let t1 = Simd::::splat(1.0) - x21; 42 | let t21 = t1 * t1; 43 | let t41 = t21 * t21; 44 | let gx1 = gradient_1d::(gi1); 45 | let n1 = t41 * gx1 * x1; 46 | 47 | // n0 + n1 = 48 | // grad0 * x0 * (1 - x0^2)^4 49 | // + grad1 * (x0 - 1) * (1 - (x0 - 1)^2)^4 50 | // 51 | // Assuming worst-case values for grad0 and grad1, we therefore need only determine the maximum of 52 | // 53 | // |x0 * (1 - x0^2)^4| + |(x0 - 1) * (1 - (x0 - 1)^2)^4| 54 | // 55 | // for 0 ≤ x0 < 1. This can be done by root-finding on the derivative, obtaining 81 / 256 when 56 | // x0 = 0.5, which we finally multiply by the maximum gradient to get the maximum value, 57 | // allowing us to scale into [-1, 1] 58 | const SCALE: f32 = 256.0 / (81.0 * 8.0); 59 | (n0 + n1) * Simd::splat(SCALE) 60 | } 61 | } 62 | 63 | /// Generates a random integer gradient in ±7 inclusive 64 | /// 65 | /// This differs from Gustavson's well-known implementation in that gradients can be zero, and the 66 | /// maximum gradient is 7 rather than 8. 67 | #[inline(always)] 68 | fn gradient_1d(hash: Simd) -> Simd 69 | where 70 | LaneCount: SupportedLaneCount, 71 | { 72 | let h = hash & Simd::splat(0xF); 73 | let v = (Simd::splat(1) + (h & Simd::splat(7))).cast::(); 74 | 75 | let h_and_8 = (h & Simd::splat(8)).simd_eq(Simd::splat(0)); 76 | h_and_8.select(v, Simd::splat(0.0) - v) 77 | } 78 | -------------------------------------------------------------------------------- /src/noise/simplex/dim2.rs: -------------------------------------------------------------------------------- 1 | // This file is licensed under multiple licenses, and is a modified version of 2 | // https://github.com/Ralith/clatter/blob/main/src/simplex.rs. A copy of the licenses can be found 3 | // here: in the path licenses/Ralith/clatter. 4 | 5 | use super::{hash, NoiseFn, Simplex}; 6 | use core::simd::{LaneCount, Simd, SimdFloat, SimdPartialEq, SimdPartialOrd, SupportedLaneCount}; 7 | use std::simd::{SimdInt, StdFloat}; 8 | 9 | impl NoiseFn<2> for Simplex { 10 | #[inline(always)] 11 | fn sample(&self, [x, y]: [Simd; 2]) -> Simd 12 | where 13 | LaneCount: SupportedLaneCount, 14 | { 15 | /// (((dimension + 1) as f32).sqrt() - 1.0) / dimension as f32 16 | const SKEW: f32 = 0.3660254037844386467637; 17 | /// -((1.0 / ((dimension + 1) as f32).sqrt()) - 1.0) / dimension as f32 18 | const UNSKEW: f32 = 0.2113248654051871177454; 19 | 20 | // Skew to distort simplexes with side length sqrt(2)/sqrt(3) until they make up 21 | // squares 22 | let s = (x + y) * Simd::splat(SKEW); 23 | let ips = (x + s).floor(); 24 | let jps = (y + s).floor(); 25 | 26 | // Integer coordinates for the base vertex of the triangle 27 | let i = ips.cast::(); 28 | let j = jps.cast::(); 29 | 30 | let t = (i + j).cast::() * Simd::splat(UNSKEW); 31 | 32 | // Unskewed distances to the first point of the enclosing simplex 33 | let x0 = x - (ips - t); 34 | let y0 = y - (jps - t); 35 | 36 | let i1 = x0.simd_ge(y0).to_int(); 37 | let j1 = y0.simd_gt(x0).to_int(); 38 | 39 | // Distances to the second and third points of the enclosing simplex 40 | let x1 = x0 + i1.cast() + Simd::splat(UNSKEW); 41 | let y1 = y0 + j1.cast() + Simd::splat(UNSKEW); 42 | let x2 = x0 + Simd::splat(-1.0) + Simd::splat(2.0 * UNSKEW); 43 | let y2 = y0 + Simd::splat(-1.0) + Simd::splat(2.0 * UNSKEW); 44 | 45 | let gi0 = hash::pcg_3d([i, j, Simd::splat(self.seed)])[0]; 46 | let gi1 = hash::pcg_3d([i - i1, j - j1, Simd::splat(self.seed)])[0]; 47 | let gi2 = hash::pcg_3d([i + Simd::splat(1), j + Simd::splat(1), Simd::splat(self.seed)])[0]; 48 | 49 | // Weights associated with the gradients at each corner 50 | // These FMA operations are equivalent to: let t = max(0, 0.5 - x*x - y*y) 51 | let t0 = y0.mul_add(-y0, x0.mul_add(-x0, Simd::splat(0.5))).simd_max(Simd::splat(0.0)); 52 | let t1 = y1.mul_add(-y1, x1.mul_add(-x1, Simd::splat(0.5))).simd_max(Simd::splat(0.0)); 53 | let t2 = y2.mul_add(-y2, x2.mul_add(-x2, Simd::splat(0.5))).simd_max(Simd::splat(0.0)); 54 | 55 | let t20 = t0 * t0; 56 | let t40 = t20 * t20; 57 | let t21 = t1 * t1; 58 | let t41 = t21 * t21; 59 | let t22 = t2 * t2; 60 | let t42 = t22 * t22; 61 | 62 | let [gx0, gy0] = gradient_2d(gi0); 63 | let g0 = gx0 * x0 + gy0 * y0; 64 | let n0 = t40 * g0; 65 | let [gx1, gy1] = gradient_2d(gi1); 66 | let g1 = gx1 * x1 + gy1 * y1; 67 | let n1 = t41 * g1; 68 | let [gx2, gy2] = gradient_2d(gi2); 69 | let g2 = gx2 * x2 + gy2 * y2; 70 | let n2 = t42 * g2; 71 | 72 | // Scaling factor found by numerical optimization 73 | const SCALE: f32 = 45.26450774985561631259; 74 | (n0 + n1 + n2) * Simd::splat(SCALE) 75 | } 76 | } 77 | 78 | #[inline(always)] 79 | fn gradient_2d(hash: Simd) -> [Simd; 2] 80 | where 81 | LaneCount: SupportedLaneCount, 82 | { 83 | let h = hash & Simd::splat(7); 84 | 85 | let mask = h.simd_lt(Simd::splat(4)); 86 | let x_magnitude = mask.select(Simd::splat(1.0), Simd::splat(2.0)); 87 | let y_magnitude = mask.select(Simd::splat(2.0), Simd::splat(1.0)); 88 | 89 | let h_and_1 = (h & Simd::splat(1)).simd_eq(Simd::splat(0)); 90 | let h_and_2 = (h & Simd::splat(2)).simd_eq(Simd::splat(0)); 91 | 92 | let gx = mask.select_mask(h_and_1, h_and_2).select(x_magnitude, -x_magnitude); 93 | let gy = mask.select_mask(h_and_2, h_and_1).select(y_magnitude, -y_magnitude); 94 | [gx, gy] 95 | } 96 | -------------------------------------------------------------------------------- /src/noise/simplex/dim3.rs: -------------------------------------------------------------------------------- 1 | // This file is licensed under multiple licenses, and is a modified version of 2 | // https://github.com/Ralith/clatter/blob/main/src/simplex.rs. A copy of the licenses can be found 3 | // here: in the path licenses/Ralith/clatter. 4 | 5 | use super::{hash, NoiseFn, Simplex}; 6 | use core::simd::{LaneCount, Mask, Simd, SimdFloat, SimdPartialEq, SimdPartialOrd, SupportedLaneCount}; 7 | use std::simd::{SimdInt, StdFloat}; 8 | 9 | impl NoiseFn<3> for Simplex { 10 | #[inline(always)] 11 | fn sample(&self, [x, y, z]: [Simd; 3]) -> Simd 12 | where 13 | LaneCount: SupportedLaneCount, 14 | { 15 | /// (((dimension + 1) as f32).sqrt() - 1.0) / dimension as f32 16 | const SKEW: f32 = 0.3333333333333333333333; 17 | /// ((1.0 / ((dimension + 1) as f32).sqrt()) - 1.0) / dimension as f32 18 | const UNSKEW: f32 = 0.1666666666666666666667; 19 | 20 | // Find skewed simplex grid coordinates associated with the input coordinates 21 | let f = (x + y + z) * Simd::splat(SKEW); 22 | let x0 = (x + f).floor(); 23 | let y0 = (y + f).floor(); 24 | let z0 = (z + f).floor(); 25 | 26 | // Integer grid coordinates 27 | let i = x0.cast::(); 28 | let j = y0.cast::(); 29 | let k = z0.cast::(); 30 | 31 | let g = Simd::splat(UNSKEW) * (x0 + y0 + z0); 32 | let x0 = x - (x0 - g); 33 | let y0 = y - (y0 - g); 34 | let z0 = z - (z0 - g); 35 | 36 | let x0_ge_y0 = x0.simd_ge(y0); 37 | let y0_ge_z0 = y0.simd_ge(z0); 38 | let x0_ge_z0 = x0.simd_ge(z0); 39 | 40 | let i1 = x0_ge_y0 & x0_ge_z0; 41 | let j1 = !x0_ge_y0 & y0_ge_z0; 42 | let k1 = !x0_ge_z0 & !y0_ge_z0; 43 | 44 | let i2 = x0_ge_y0 | x0_ge_z0; 45 | let j2 = !x0_ge_y0 | y0_ge_z0; 46 | let k2 = !(x0_ge_z0 & y0_ge_z0); 47 | 48 | let v1x = i + i1.select(Simd::splat(1), Simd::splat(0)); 49 | let v1y = j + j1.select(Simd::splat(1), Simd::splat(0)); 50 | let v1z = k + k1.select(Simd::splat(1), Simd::splat(0)); 51 | 52 | let v2x = i + i2.select(Simd::splat(1), Simd::splat(0)); 53 | let v2y = j + j2.select(Simd::splat(1), Simd::splat(0)); 54 | let v2z = k + k2.select(Simd::splat(1), Simd::splat(0)); 55 | 56 | let v3x = i + Simd::splat(1); 57 | let v3y = j + Simd::splat(1); 58 | let v3z = k + Simd::splat(1); 59 | 60 | let x1 = x0 - i1.select(Simd::splat(1.0), Simd::splat(0.0)) + Simd::splat(UNSKEW); 61 | let y1 = y0 - j1.select(Simd::splat(1.0), Simd::splat(0.0)) + Simd::splat(UNSKEW); 62 | let z1 = z0 - k1.select(Simd::splat(1.0), Simd::splat(0.0)) + Simd::splat(UNSKEW); 63 | 64 | let x2 = x0 - i2.select(Simd::splat(1.0), Simd::splat(0.0)) + Simd::splat(SKEW); 65 | let y2 = y0 - j2.select(Simd::splat(1.0), Simd::splat(0.0)) + Simd::splat(SKEW); 66 | let z2 = z0 - k2.select(Simd::splat(1.0), Simd::splat(0.0)) + Simd::splat(SKEW); 67 | 68 | let x3 = x0 + Simd::splat(-0.5); 69 | let y3 = y0 + Simd::splat(-0.5); 70 | let z3 = z0 + Simd::splat(-0.5); 71 | 72 | // Compute base weight factors associated with each vertex, `0.5 - v . v` where v is the 73 | // difference between the sample point and the vertex. 74 | let t0 = (Simd::splat(0.5) - x0 * x0 - y0 * y0 - z0 * z0).simd_max(Simd::splat(0.0)); 75 | let t1 = (Simd::splat(0.5) - x1 * x1 - y1 * y1 - z1 * z1).simd_max(Simd::splat(0.0)); 76 | let t2 = (Simd::splat(0.5) - x2 * x2 - y2 * y2 - z2 * z2).simd_max(Simd::splat(0.0)); 77 | let t3 = (Simd::splat(0.5) - x3 * x3 - y3 * y3 - z3 * z3).simd_max(Simd::splat(0.0)); 78 | 79 | // Square weights 80 | let t20 = t0 * t0; 81 | let t21 = t1 * t1; 82 | let t22 = t2 * t2; 83 | let t23 = t3 * t3; 84 | 85 | // ...twice! 86 | let t40 = t20 * t20; 87 | let t41 = t21 * t21; 88 | let t42 = t22 * t22; 89 | let t43 = t23 * t23; 90 | 91 | // Compute contribution from each vertex 92 | let g0 = Gradient3d::new(self.seed, [i, j, k]); 93 | let g0d = g0.dot([x0, y0, z0]); 94 | let v0 = t40 * g0d; 95 | 96 | let g1 = Gradient3d::new(self.seed, [v1x, v1y, v1z]); 97 | let g1d = g1.dot([x1, y1, z1]); 98 | let v1 = t41 * g1d; 99 | 100 | let g2 = Gradient3d::new(self.seed, [v2x, v2y, v2z]); 101 | let g2d = g2.dot([x2, y2, z2]); 102 | let v2 = t42 * g2d; 103 | 104 | let g3 = Gradient3d::new(self.seed, [v3x, v3y, v3z]); 105 | let g3d = g3.dot([x3, y3, z3]); 106 | let v3 = t43 * g3d; 107 | 108 | // Scaling factor found by numerical optimization 109 | const SCALE: f32 = 67.79816627147162; 110 | (v3 + v2 + v1 + v0) * Simd::splat(SCALE) 111 | } 112 | } 113 | 114 | /// Generates a random gradient vector from the origin towards the midpoint of an edge of a 115 | /// double-unit cube 116 | struct Gradient3d 117 | where 118 | LaneCount: SupportedLaneCount, 119 | { 120 | // Masks guiding dimension selection 121 | l8: Mask, 122 | l4: Mask, 123 | h12_or_14: Mask, 124 | 125 | // Signs for the selected dimensions 126 | h1: Simd, 127 | h2: Simd, 128 | } 129 | 130 | impl Gradient3d 131 | where 132 | LaneCount: SupportedLaneCount, 133 | { 134 | /// Compute hash values used by `grad3d` and `grad3d_dot` 135 | #[inline(always)] 136 | fn new(seed: i32, [i, j, k]: [Simd; 3]) -> Self { 137 | let hash = hash::pcg_4d([i, j, k, Simd::splat(seed)])[0]; 138 | let hasha13 = hash & Simd::splat(13); 139 | Self { 140 | l8: hasha13.simd_lt(Simd::splat(8)), 141 | l4: hasha13.simd_lt(Simd::splat(2)), 142 | h12_or_14: hasha13.simd_eq(Simd::splat(12)), 143 | 144 | h1: hash << Simd::splat(31), 145 | h2: (hash & Simd::splat(2)) << Simd::splat(30), 146 | } 147 | } 148 | 149 | /// Computes the dot product of a vector with the gradient vector 150 | #[inline(always)] 151 | fn dot(&self, [x, y, z]: [Simd; 3]) -> Simd { 152 | let u = self.l8.select(x, y); 153 | let v = self.l4.select(y, self.h12_or_14.select(x, z)); 154 | // Maybe flip sign bits, then sum 155 | Simd::::from_bits(u.to_bits() ^ self.h1.cast()) 156 | + Simd::::from_bits(v.to_bits() ^ self.h2.cast()) 157 | } 158 | 159 | /// The gradient vector generated by `dot` 160 | /// 161 | /// This is a separate function because it's slower than `grad3d_dot` and only needed when computing 162 | /// derivatives. 163 | #[inline(always)] 164 | fn vector(&self) -> [Simd; 3] { 165 | let first = Simd::::from_bits(self.h1.cast() | Simd::::splat(1.0).to_bits()); 166 | let gx = self.l8.select(first, Simd::splat(0.0)); 167 | let gy = (!self.l8).select(first, Simd::splat(0.0)); 168 | 169 | let second = Simd::::from_bits(self.h2.cast() | Simd::::splat(1.0).to_bits()); 170 | let gy = self.l4.select(second, gy); 171 | let gx = (!self.l4 & self.h12_or_14).select(second, gx); 172 | let gz = (!(self.h12_or_14 | self.l4)).select(second, Simd::splat(0.0)); 173 | 174 | [gx, gy, gz] 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/noise/simplex/hash.rs: -------------------------------------------------------------------------------- 1 | // This file is licensed under multiple licenses, and is a modified version of 2 | // https://github.com/Ralith/clatter/blob/main/src/simplex.rs. A copy of the licenses can be found 3 | // here: in the path licenses/Ralith/clatter. 4 | 5 | use core::simd::{LaneCount, Simd, SupportedLaneCount}; 6 | 7 | #[inline(always)] 8 | pub fn pcg(v: Simd) -> Simd 9 | where 10 | LaneCount: SupportedLaneCount, 11 | { 12 | // PCG hash function from "Hash Functions for GPU Rendering" 13 | let state = v * Simd::splat(747796405) + Simd::splat(2891336453u32 as i32); 14 | let word = ((state >> ((state >> Simd::splat(28)) + Simd::splat(4))) ^ state) * Simd::splat(277803737); 15 | (word >> Simd::splat(22)) ^ word 16 | } 17 | 18 | // For completeness 19 | #[allow(dead_code)] 20 | #[inline(always)] 21 | pub fn pcg_2d([mut vx, mut vy]: [Simd; 2]) -> [Simd; 2] 22 | where 23 | LaneCount: SupportedLaneCount, 24 | { 25 | vx *= Simd::splat(1664525) + Simd::splat(1013904223); 26 | vy *= Simd::splat(1664525) + Simd::splat(1013904223); 27 | 28 | vx += vy * Simd::splat(1664525); 29 | vy += vx * Simd::splat(1664525); 30 | 31 | vx ^= vx >> Simd::splat(16); 32 | vy ^= vy >> Simd::splat(16); 33 | 34 | vx += vy * Simd::splat(1664525); 35 | vy += vx * Simd::splat(1664525); 36 | 37 | vx ^= vx >> Simd::splat(16); 38 | vy ^= vy >> Simd::splat(16); 39 | 40 | [vx, vy] 41 | } 42 | 43 | #[inline(always)] 44 | pub fn pcg_3d([mut vx, mut vy, mut vz]: [Simd; 3]) -> [Simd; 3] 45 | where 46 | LaneCount: SupportedLaneCount, 47 | { 48 | // PCG3D hash function from "Hash Functions for GPU Rendering" 49 | vx = vx * Simd::splat(1664525) + Simd::splat(1013904223); 50 | vy = vy * Simd::splat(1664525) + Simd::splat(1013904223); 51 | vz = vz * Simd::splat(1664525) + Simd::splat(1013904223); 52 | 53 | vx += vy * vz; 54 | vy += vz * vx; 55 | vz += vx * vy; 56 | 57 | vx = vx ^ (vx >> Simd::splat(16)); 58 | vy = vy ^ (vy >> Simd::splat(16)); 59 | vz = vz ^ (vz >> Simd::splat(16)); 60 | 61 | vx += vy * vz; 62 | vy += vz * vx; 63 | vz += vx * vy; 64 | 65 | [vx, vy, vz] 66 | } 67 | 68 | #[inline(always)] 69 | pub fn pcg_4d( 70 | [mut vx, mut vy, mut vz, mut vw]: [Simd; 4], 71 | ) -> [Simd; 4] 72 | where 73 | LaneCount: SupportedLaneCount, 74 | { 75 | // PCG4D hash function from "Hash Functions for GPU Rendering" 76 | vx = vx * Simd::splat(1664525) + Simd::splat(1013904223); 77 | vy = vy * Simd::splat(1664525) + Simd::splat(1013904223); 78 | vz = vz * Simd::splat(1664525) + Simd::splat(1013904223); 79 | vw = vw * Simd::splat(1664525) + Simd::splat(1013904223); 80 | 81 | vx += vy * vw; 82 | vy += vz * vx; 83 | vz += vx * vy; 84 | vw += vy * vz; 85 | 86 | vx = vx ^ (vx >> Simd::splat(16)); 87 | vy = vy ^ (vy >> Simd::splat(16)); 88 | vz = vz ^ (vz >> Simd::splat(16)); 89 | vw = vw ^ (vw >> Simd::splat(16)); 90 | 91 | vx += vy * vw; 92 | vy += vz * vx; 93 | vz += vx * vy; 94 | vw += vy * vz; 95 | 96 | [vx, vy, vz, vw] 97 | } 98 | -------------------------------------------------------------------------------- /src/noise/simplex/mod.rs: -------------------------------------------------------------------------------- 1 | mod dim1; 2 | mod dim2; 3 | mod dim3; 4 | mod dim4; 5 | 6 | mod hash; 7 | 8 | use super::NoiseFn; 9 | 10 | /// Noise function that outputs 1/2/3/4-dimensional Simplex noise. 11 | #[derive(Clone, Copy, Debug)] 12 | pub struct Simplex { 13 | seed: i32, 14 | } 15 | 16 | impl Simplex { 17 | pub fn seed(seed: i32) -> Self { 18 | Self { seed } 19 | } 20 | } 21 | 22 | impl Default for Simplex { 23 | fn default() -> Self { 24 | Self { seed: 0 } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/storm.js: -------------------------------------------------------------------------------- 1 | var fs_callback; 2 | 3 | export function fs_load_files(index, paths) { 4 | let promises = paths.map(function (path) { 5 | return fetch(path).then(function (response) { 6 | if (response.status < 200 || response.status >= 300) { 7 | return response.status; 8 | } else { 9 | return response.arrayBuffer().then(function (buffer) { 10 | return new Uint8Array(buffer); 11 | }).catch(function (reason) { 12 | return 500; 13 | }); 14 | } 15 | }).catch(function (reason) { 16 | return 500; 17 | }); 18 | }); 19 | Promise.all(promises).then(function (array) { 20 | fs_callback(index, array); 21 | }); 22 | } 23 | 24 | export function fs_init_callback(callback) { 25 | fs_callback = callback; 26 | } 27 | -------------------------------------------------------------------------------- /src/sync/mod.rs: -------------------------------------------------------------------------------- 1 | mod signal; 2 | mod spsc; 3 | 4 | pub use signal::Signal; 5 | pub use spsc::{make, Consumer, Producer}; 6 | -------------------------------------------------------------------------------- /src/sync/signal.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::{Condvar, Mutex}; 2 | 3 | /// Simple Mutex + Condvar wait notify primitive. Can be used for waiting without spinning. This 4 | /// should be wrapped as an `Arc` and cloned. 5 | #[repr(align(8))] 6 | pub struct Signal { 7 | mutex: Mutex, 8 | cvar: Condvar, 9 | } 10 | 11 | impl Signal { 12 | /// Creates a new signal. 13 | pub fn new() -> Self { 14 | Self { 15 | mutex: Mutex::new(false), 16 | cvar: Condvar::new(), 17 | } 18 | } 19 | 20 | /// Wakes all threads awaiting this signal. 21 | pub fn notify(&self) { 22 | let mut lock = self.mutex.lock(); 23 | *lock = true; 24 | self.cvar.notify_all(); 25 | } 26 | 27 | /// Parks the current thread until notified. May spuriously wake on its own. 28 | pub fn wait(&self) { 29 | let mut lock = self.mutex.lock(); 30 | if *lock { 31 | *lock = false; 32 | return; 33 | } 34 | self.cvar.wait(&mut lock); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/time/convert.rs: -------------------------------------------------------------------------------- 1 | use core::time::Duration; 2 | 3 | /// The number of nanoseconds per microsecond. 4 | pub const NANOS_PER_MICRO: u64 = 1_000; 5 | /// The number of nanoseconds per millisecond. 6 | pub const NANOS_PER_MILLI: u64 = 1_000_000; 7 | /// The number of nanoseconds per second. 8 | pub const NANOS_PER_SEC: u64 = 1_000_000_000; 9 | /// The number of microseconds per second. 10 | pub const MICROS_PER_SEC: u64 = 1_000_000; 11 | /// The number of milliseconds per second. 12 | pub const MILLIS_PER_SEC: u64 = 1_000; 13 | /// The number of seconds per minute. 14 | pub const SECS_PER_MINUTE: u64 = 60; 15 | /// The number of seconds per hour. 16 | pub const SECS_PER_HOUR: u64 = 3_600; 17 | /// The number of (non-leap) seconds in days. 18 | pub const SECS_PER_DAY: u64 = 86_400; 19 | /// A const duration representing a second. 20 | pub const SECOND: Duration = Duration::from_secs(1); 21 | /// A const duration representing a millisecond. 22 | pub const MILLISECOND: Duration = Duration::from_millis(1); 23 | /// A const duration representing a microsecond. 24 | pub const MICROSECOND: Duration = Duration::from_micros(1); 25 | /// A const duration representing a nanosecond. 26 | pub const NANOSECOND: Duration = Duration::from_nanos(1); 27 | 28 | /// Converts a duration into a number of days, rounding down. 29 | #[inline] 30 | pub fn as_days(duration: &Duration) -> u64 { 31 | let secs = duration.as_secs(); 32 | secs / SECS_PER_DAY 33 | } 34 | 35 | /// Converts a duration into a number of hours, rounding down. 36 | #[inline] 37 | pub fn as_hours(duration: &Duration) -> u64 { 38 | let secs = duration.as_secs(); 39 | secs / SECS_PER_HOUR 40 | } 41 | 42 | /// Converts a duration into a number of minutes, rounding down. 43 | #[inline] 44 | pub fn as_minutes(duration: &Duration) -> u64 { 45 | let secs = duration.as_secs(); 46 | secs / SECS_PER_MINUTE 47 | } 48 | 49 | /// Converts a duration into a number of seconds, rounding down. 50 | #[inline] 51 | pub fn as_seconds(duration: &Duration) -> u64 { 52 | duration.as_secs() 53 | } 54 | 55 | /// Converts a duration into a number of milliseconds, rounding down. 56 | #[inline] 57 | pub fn as_milliseconds(duration: &Duration) -> u64 { 58 | let mut secs = duration.as_secs(); 59 | let mut nanos = duration.subsec_nanos() as u64; 60 | secs *= MILLIS_PER_SEC; 61 | nanos /= NANOS_PER_MILLI; 62 | secs + nanos 63 | } 64 | 65 | /// Converts a duration into a number of microseconds, rounding down. 66 | #[inline] 67 | pub fn as_microseconds(duration: &Duration) -> u64 { 68 | let mut secs = duration.as_secs(); 69 | let mut nanos = duration.subsec_nanos() as u64; 70 | secs *= MICROS_PER_SEC; 71 | nanos /= NANOS_PER_MICRO; 72 | secs + nanos 73 | } 74 | 75 | /// Converts a duration into a number of nanoseconds, rounding down. 76 | #[inline] 77 | pub fn as_nanoseconds(duration: &Duration) -> u64 { 78 | let mut secs = duration.as_secs(); 79 | let nanos = duration.subsec_nanos() as u64; 80 | secs *= NANOS_PER_SEC; 81 | secs + nanos 82 | } 83 | -------------------------------------------------------------------------------- /src/time/mod.rs: -------------------------------------------------------------------------------- 1 | mod convert; 2 | 3 | pub use self::convert::*; 4 | /// A measurement of a monotonically nondecreasing clock. 5 | /// Opaque and useful only with `Duration`. 6 | /// 7 | /// Instants are always guaranteed to be no less than any previously measured 8 | /// instant when created, and are often useful for tasks such as measuring 9 | /// benchmarks or timing how long an operation takes. 10 | /// 11 | /// Note, however, that instants are not guaranteed to be **steady**. In other 12 | /// words, each tick of the underlying clock might not be the same length (e.g. 13 | /// some seconds may be longer than others). An instant may jump forwards or 14 | /// experience time dilation (slow down or speed up), but it will never go 15 | /// backwards. 16 | /// 17 | /// Instants are opaque types that can only be compared to one another. There is 18 | /// no method to get "the number of seconds" from an instant. Instead, it only 19 | /// allows measuring the duration between two instants (or comparing two 20 | /// instants). 21 | /// 22 | /// The size of an `Instant` struct may vary depending on the target operating 23 | /// system. 24 | pub use instant::Instant; 25 | 26 | /// Returns an instant corresponding to “now”. 27 | pub fn now() -> Instant { 28 | Instant::now() 29 | } 30 | -------------------------------------------------------------------------------- /storm_macro/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /storm_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "storm_macro" 3 | version = "0.1.0" 4 | authors = ["Joe Cumbo "] 5 | description = "Proc macro crate for storm" 6 | license = "MIT" 7 | readme = "README.md" 8 | edition = "2021" 9 | documentation = "https://docs.rs/storm" 10 | homepage = "https://github.com/mooman219/storm" 11 | repository = "https://github.com/mooman219/storm" 12 | include = ["Cargo.toml", "src/", "README.md", "LICENSE"] 13 | 14 | [badges] 15 | maintenance = { status = "experimental" } 16 | 17 | [lib] 18 | proc-macro = true 19 | 20 | [dependencies] 21 | syn = "1.0" 22 | quote = "1.0" 23 | proc-macro2 = "1.0.66" 24 | -------------------------------------------------------------------------------- /storm_macro/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joseph Cumbo 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 | -------------------------------------------------------------------------------- /storm_macro/README.md: -------------------------------------------------------------------------------- 1 | Internal crate for storm -------------------------------------------------------------------------------- /storm_macro/rustfmt.toml: -------------------------------------------------------------------------------- 1 | # match_block_trailing_comma = true 2 | # comment_width = 100 3 | # wrap_comments = true 4 | max_width = 110 5 | use_small_heuristics = "Off" -------------------------------------------------------------------------------- /storm_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is licensed under the MIT license, and is a modified version of 2 | // https://github.com/RSSchermer/std140.rs. A copy of that license can be found here: in 3 | // the path licenses/RSSchermer/std140. 4 | // 5 | // This file was modified to support web, and use more hygenic module paths. 6 | 7 | extern crate proc_macro; 8 | 9 | use proc_macro::TokenStream; 10 | use proc_macro2::{Span, TokenStream as TokenStream2}; 11 | use quote::{quote, quote_spanned}; 12 | use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Ident}; 13 | 14 | #[proc_macro_attribute] 15 | pub fn uniform(args: TokenStream, input: TokenStream) -> TokenStream { 16 | assert!(args.is_empty(), "#[uniform] does not take arguments."); 17 | let input = parse_macro_input!(input as DeriveInput); 18 | parse_uniform(&input).unwrap_or_else(compile_error).into() 19 | } 20 | 21 | fn parse_uniform(input: &DeriveInput) -> Result { 22 | let data = match &input.data { 23 | Data::Struct(data) => data, 24 | _ => return Err("Cannot represent an enum or union with #[uniform], only a struct.".to_string()), 25 | }; 26 | 27 | if input.attrs.iter().any(|attr| attr.path.is_ident("repr")) { 28 | return Err("Cannot parse another #[repr] attribute on a struct marked with #[uniform]".to_string()); 29 | } 30 | 31 | let mod_path = quote!(std140); 32 | let struct_name = &input.ident; 33 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 34 | 35 | let asserts = data.fields.iter().map(|field| { 36 | let ty = &field.ty; 37 | let span = field.span(); 38 | 39 | quote_spanned!(span=> assert_std140_element::<#ty> { marker: core::marker::PhantomData };) 40 | }); 41 | 42 | let suffix = struct_name.to_string().trim_start_matches("r#").to_owned(); 43 | let dummy_const = Ident::new(&format!("_IMPL_STORM_UNIFORM_FOR_{}", suffix), Span::call_site()); 44 | 45 | let asserts = quote! { 46 | struct assert_std140_element where T: #mod_path::Std140Element { 47 | marker: core::marker::PhantomData 48 | } 49 | 50 | #(#asserts)* 51 | }; 52 | 53 | let impl_std140_struct = quote! { 54 | #[automatically_derived] 55 | unsafe impl #impl_generics #mod_path::Std140Struct for #struct_name #ty_generics #where_clause {} 56 | }; 57 | 58 | let generated = quote! { 59 | #[repr(C, align(16))] 60 | #input 61 | 62 | #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] 63 | const #dummy_const: () = { 64 | #[allow(unknown_lints)] 65 | #[cfg_attr(feature = "cargo-clippy", allow(useless_attribute))] 66 | #[allow(rust_2018_idioms)] 67 | 68 | #asserts 69 | 70 | #impl_std140_struct 71 | }; 72 | }; 73 | 74 | Ok(generated) 75 | } 76 | 77 | fn compile_error(message: String) -> proc_macro2::TokenStream { 78 | quote! { 79 | compile_error!(#message); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /texture.bat: -------------------------------------------------------------------------------- 1 | cargo build --example texture --release --target=wasm32-unknown-unknown 2 | @REM wasm-bindgen: cargo install -f wasm-bindgen-cli 3 | wasm-bindgen --target web --no-typescript --out-dir ./docs/ --out-name web ./target/wasm32-unknown-unknown/release/examples/texture.wasm -------------------------------------------------------------------------------- /web.bat: -------------------------------------------------------------------------------- 1 | cargo build --example pong --release --target=wasm32-unknown-unknown 2 | cargo build --example text --release --target=wasm32-unknown-unknown 3 | cargo build --example texture --release --target=wasm32-unknown-unknown 4 | cargo build --example particles --release --target=wasm32-unknown-unknown 5 | @REM wasm-bindgen: cargo install -f wasm-bindgen-cli 6 | wasm-bindgen --target web --no-typescript --out-dir ./docs/ --out-name pong ./target/wasm32-unknown-unknown/release/examples/pong.wasm 7 | wasm-bindgen --target web --no-typescript --out-dir ./docs/ --out-name text ./target/wasm32-unknown-unknown/release/examples/text.wasm 8 | wasm-bindgen --target web --no-typescript --out-dir ./docs/ --out-name texture ./target/wasm32-unknown-unknown/release/examples/texture.wasm 9 | wasm-bindgen --target web --no-typescript --out-dir ./docs/ --out-name particles ./target/wasm32-unknown-unknown/release/examples/particles.wasm --------------------------------------------------------------------------------