├── .gitignore ├── examples ├── wabbit_alpha.png └── bunnymark.rs ├── src ├── lib.rs ├── input │ ├── mouse.rs │ ├── key.rs │ ├── gamepad.rs │ └── event.rs ├── fs.rs ├── graphics │ ├── canvas.rs │ ├── scaling.rs │ ├── color.rs │ ├── packer.rs │ ├── mesh.rs │ ├── shader.rs │ ├── rectangle.rs │ ├── text.rs │ ├── texture.rs │ └── batch.rs ├── time.rs ├── app.rs ├── window.rs ├── ldtk.rs ├── input.rs └── graphics.rs ├── Cargo.toml ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock -------------------------------------------------------------------------------- /examples/wabbit_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/17cupsofcoffee/nova/HEAD/examples/wabbit_alpha.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::new_without_default)] 2 | #![doc = include_str!("../README.md")] 3 | 4 | // ===== Core ===== 5 | pub mod app; 6 | pub mod fs; 7 | pub mod graphics; 8 | pub mod input; 9 | pub mod time; 10 | pub mod window; 11 | 12 | pub use glam as math; 13 | 14 | // ===== Optional ===== 15 | 16 | #[cfg(feature = "ldtk")] 17 | pub mod ldtk; 18 | -------------------------------------------------------------------------------- /src/input/mouse.rs: -------------------------------------------------------------------------------- 1 | use sdl3_sys::mouse::*; 2 | 3 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 4 | pub enum MouseButton { 5 | Left, 6 | Middle, 7 | Right, 8 | X1, 9 | X2, 10 | } 11 | 12 | impl MouseButton { 13 | pub(crate) fn from_raw(raw: i32) -> Option { 14 | match raw { 15 | SDL_BUTTON_LEFT => Some(MouseButton::Left), 16 | SDL_BUTTON_MIDDLE => Some(MouseButton::Middle), 17 | SDL_BUTTON_RIGHT => Some(MouseButton::Right), 18 | SDL_BUTTON_X1 => Some(MouseButton::X1), 19 | SDL_BUTTON_X2 => Some(MouseButton::X2), 20 | _ => None, 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::OnceLock; 3 | 4 | pub fn base_path() -> &'static PathBuf { 5 | static BASE_PATH: OnceLock = OnceLock::new(); 6 | 7 | // TODO: Make this use SDL_GetBaseDir when packaging for release 8 | BASE_PATH.get_or_init(|| std::env::current_dir().unwrap()) 9 | } 10 | 11 | pub fn asset_path(path: &str) -> PathBuf { 12 | base_path().join(path) 13 | } 14 | 15 | pub fn read(path: &str) -> Vec { 16 | let full_path = asset_path(path); 17 | 18 | std::fs::read(full_path).unwrap() 19 | } 20 | 21 | pub fn read_to_string(path: &str) -> String { 22 | let full_path = asset_path(path); 23 | 24 | std::fs::read_to_string(full_path).unwrap() 25 | } 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nova" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | # Core 10 | sdl3-sys = { version = "0.1.3" } 11 | glow = "0.16" 12 | bytemuck = { version = "1.20", features = ["derive"] } 13 | glam = { version = "0.29", features = ["bytemuck"] } 14 | fontdue = "0.9" 15 | png = "0.17" 16 | 17 | # Optional 18 | serde = { version = "1.0", optional = true, features = ["derive"] } 19 | serde_json = { version = "1.0", optional = true } 20 | 21 | [dev-dependencies] 22 | rand = "0.8" 23 | 24 | [features] 25 | default = ["ldtk"] 26 | ldtk = ["serde", "serde_json"] 27 | static_bundled_build = ["sdl3-sys/build-from-source-static"] # TODO: Probably split this up 28 | serde = ["dep:serde", "glam/serde"] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nova 2 | 3 | Nova is a 2D game framework written in Rust. 4 | 5 | This is a sort of spiritual successor to [Tetra](https://github.com/17cupsofcoffee/tetra), a game engine I worked 6 | on between 2018 and 2022. It aims to be smaller and simpler, with less global state. 7 | 8 | **⚠️ Use at your own risk!** This framework is still very experimental, and the API is constantly in flux. No support is offered, but you are welcome to use to the code as reference or fork it for your own needs. 9 | 10 | ## Features 11 | 12 | - `ldtk` (enabled by default): enables a module to load [ldtk](https://ldtk.io/) files. 13 | - `static_bundled_build`: enables automatic SDL3 library building and linking. Building SDL3 can take a bit during that first build (usually 1 minute or more). 14 | 15 | ## Notes 16 | 17 | - This framework is very heavily inspired by [FNA](https://github.com/FNA-XNA/FNA), and NoelFB's lightweight game engines ([Blah](https://github.com/NoelFB/blah) and [Foster](https://github.com/NoelFB/Foster)). 18 | - It depends on [SDL3](https://www.libsdl.org/) for interacting with the underlying platform. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Joe Clay 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. -------------------------------------------------------------------------------- /src/graphics/canvas.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use glow::HasContext; 4 | 5 | use crate::graphics::{Graphics, Texture}; 6 | 7 | use super::RawTexture; 8 | 9 | #[derive(Clone)] 10 | pub struct Canvas { 11 | pub(crate) raw: Rc, 12 | texture: Texture, 13 | } 14 | 15 | impl Canvas { 16 | pub fn new(gfx: &Graphics, width: i32, height: i32) -> Canvas { 17 | let texture = Texture::empty(gfx, width, height); 18 | let raw = RawCanvas::new(gfx, &texture.raw); 19 | 20 | Canvas { 21 | raw: Rc::new(raw), 22 | texture, 23 | } 24 | } 25 | 26 | pub fn texture(&self) -> &Texture { 27 | &self.texture 28 | } 29 | 30 | pub fn width(&self) -> i32 { 31 | self.texture.width() 32 | } 33 | 34 | pub fn height(&self) -> i32 { 35 | self.texture.height() 36 | } 37 | 38 | pub fn size(&self) -> (i32, i32) { 39 | self.texture.size() 40 | } 41 | 42 | pub fn into_texture(self) -> Texture { 43 | self.texture 44 | } 45 | } 46 | 47 | pub struct RawCanvas { 48 | gfx: Graphics, 49 | pub(crate) id: glow::Framebuffer, 50 | } 51 | 52 | impl RawCanvas { 53 | pub fn new(gfx: &Graphics, texture: &RawTexture) -> RawCanvas { 54 | unsafe { 55 | let id = gfx.state.gl.create_framebuffer().unwrap(); 56 | 57 | gfx.bind_canvas(Some(id)); 58 | 59 | gfx.state.gl.framebuffer_texture_2d( 60 | glow::FRAMEBUFFER, 61 | glow::COLOR_ATTACHMENT0, 62 | glow::TEXTURE_2D, 63 | Some(texture.id), 64 | 0, 65 | ); 66 | 67 | RawCanvas { 68 | gfx: gfx.clone(), 69 | id, 70 | } 71 | } 72 | } 73 | } 74 | 75 | impl Drop for RawCanvas { 76 | fn drop(&mut self) { 77 | unsafe { 78 | self.gfx.state.gl.delete_framebuffer(self.id); 79 | 80 | if self.gfx.state.current_canvas.get() == Some(self.id) { 81 | self.gfx.state.current_canvas.set(None); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/graphics/scaling.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec2; 2 | 3 | use crate::graphics::Canvas; 4 | use crate::window::Window; 5 | 6 | use super::{Batcher, DrawParams, Graphics, Target}; 7 | 8 | pub struct Scaler { 9 | canvas: Canvas, 10 | 11 | offset: Vec2, 12 | scale: Vec2, 13 | } 14 | 15 | impl Scaler { 16 | pub fn new(gfx: &Graphics, width: i32, height: i32) -> Scaler { 17 | Scaler { 18 | canvas: Canvas::new(gfx, width, height), 19 | 20 | offset: Vec2::ZERO, 21 | scale: Vec2::ONE, 22 | } 23 | } 24 | 25 | pub fn draw(&mut self, batch: &mut Batcher, target: &Window) { 26 | let (offset, scale) = fit_canvas_to_window(&self.canvas, target); 27 | 28 | batch.texture( 29 | self.canvas.texture(), 30 | offset, 31 | DrawParams::new().scale(scale), 32 | ); 33 | 34 | batch.draw(target); 35 | 36 | self.offset = offset; 37 | self.scale = scale; 38 | } 39 | 40 | pub fn offset(&self) -> Vec2 { 41 | self.offset 42 | } 43 | 44 | pub fn scale(&self) -> Vec2 { 45 | self.scale 46 | } 47 | } 48 | 49 | impl Target for Scaler { 50 | const FLIPPED: bool = Canvas::FLIPPED; 51 | 52 | fn bind(&self, gfx: &Graphics) { 53 | self.canvas.bind(gfx) 54 | } 55 | 56 | fn size(&self) -> (i32, i32) { 57 | self.canvas.size() 58 | } 59 | } 60 | 61 | pub fn fit_canvas_to_window(canvas: &Canvas, window: &Window) -> (Vec2, Vec2) { 62 | let (canvas_width, canvas_height) = canvas.size(); 63 | let (window_width, window_height) = window.size(); 64 | 65 | let scale = i32::max( 66 | 1, 67 | i32::min( 68 | window_width as i32 / canvas_width, 69 | window_height as i32 / canvas_height, 70 | ), 71 | ); 72 | 73 | let screen_width = canvas_width * scale; 74 | let screen_height = canvas_height * scale; 75 | let screen_x = (window_width as i32 - screen_width) / 2; 76 | let screen_y = (window_height as i32 - screen_height) / 2; 77 | 78 | ( 79 | Vec2::new(screen_x as f32, screen_y as f32), 80 | Vec2::splat(scale as f32), 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | pub struct Timer { 4 | start_time: Instant, 5 | last_time: Instant, 6 | accumulated_time: Duration, 7 | target_time: Duration, 8 | max_lag: Duration, 9 | } 10 | 11 | impl Timer { 12 | pub fn new(tick_rate: f64) -> Timer { 13 | let target_time = Duration::from_secs_f64(1.0 / tick_rate); 14 | 15 | Timer { 16 | start_time: Instant::now(), 17 | last_time: Instant::now(), 18 | accumulated_time: Duration::ZERO, 19 | target_time, 20 | max_lag: target_time * 8, 21 | } 22 | } 23 | 24 | pub fn tick(&mut self) { 25 | self.advance_time(); 26 | self.cap_accumulated_time(); 27 | } 28 | 29 | pub fn tick_until_update_ready(&mut self) { 30 | self.advance_time(); 31 | 32 | // TODO: This isn't accurate enough - need to sleep and then spin. 33 | while self.accumulated_time < self.target_time { 34 | std::thread::sleep(Duration::from_millis(1)); 35 | 36 | self.advance_time(); 37 | } 38 | 39 | self.cap_accumulated_time(); 40 | } 41 | 42 | pub fn reset(&mut self) { 43 | self.last_time = Instant::now(); 44 | self.accumulated_time = Duration::ZERO; 45 | } 46 | 47 | pub fn consume_time(&mut self) -> bool { 48 | let ready = self.accumulated_time >= self.target_time; 49 | 50 | if ready { 51 | self.accumulated_time -= self.target_time; 52 | } 53 | 54 | ready 55 | } 56 | 57 | pub fn delta(&self) -> Duration { 58 | self.target_time 59 | } 60 | 61 | pub fn blend_factor(&self) -> f32 { 62 | self.accumulated_time.as_secs_f32() / self.target_time.as_secs_f32() 63 | } 64 | 65 | fn advance_time(&mut self) { 66 | let current_time = Instant::now(); 67 | let time_advanced = current_time - self.last_time; 68 | 69 | self.accumulated_time += time_advanced; 70 | self.last_time = current_time; 71 | } 72 | 73 | fn cap_accumulated_time(&mut self) { 74 | if self.accumulated_time > self.max_lag { 75 | self.accumulated_time = self.max_lag; 76 | } 77 | } 78 | 79 | pub fn total_time(&self) -> Duration { 80 | self.start_time.elapsed() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::Graphics; 2 | use crate::input::{Event, Input}; 3 | use crate::time::Timer; 4 | use crate::window::Window; 5 | 6 | /// The generic event handler for the game. You should implement this yourself 7 | /// 8 | /// ## Call order: 9 | /// 1. `event`: 0 to n times based on what events are received 10 | /// 2. `update`: 0 to n times based on the tick rate 11 | /// 3. `draw`: 1 time per frame 12 | pub trait EventHandler { 13 | /// Handle a raw event for the game. This is useful for one-off events, like key down events. 14 | /// For continuous events, like moving a character when you hold a key, use the update method. 15 | /// 16 | /// note: this will be called after the `app.input` has been updated with the event. 17 | fn event(&mut self, _app: &mut App, _event: Event) {} 18 | fn update(&mut self, _app: &mut App) {} 19 | fn draw(&mut self, _app: &mut App) {} 20 | } 21 | 22 | pub struct App { 23 | pub window: Window, 24 | pub gfx: Graphics, 25 | pub input: Input, 26 | pub timer: Timer, 27 | 28 | pub is_running: bool, 29 | } 30 | 31 | impl App { 32 | pub fn new(title: &str, width: i32, height: i32, tick_rate: f64) -> App { 33 | let mut window = Window::new(title, width, height); 34 | let gfx = Graphics::new(&mut window); 35 | let input = Input::new(); 36 | let timer = Timer::new(tick_rate); 37 | 38 | App { 39 | window, 40 | gfx, 41 | input, 42 | timer, 43 | 44 | is_running: true, 45 | } 46 | } 47 | 48 | pub fn run(&mut self, event_handler: &mut impl EventHandler) { 49 | self.timer.reset(); 50 | 51 | while self.is_running { 52 | self.timer.tick_until_update_ready(); 53 | 54 | self.handle_events(event_handler); 55 | 56 | while self.timer.consume_time() { 57 | event_handler.update(self); 58 | 59 | self.input.clear(); 60 | } 61 | 62 | event_handler.draw(self); 63 | 64 | self.window.present(); 65 | } 66 | } 67 | 68 | pub fn handle_events(&mut self, event_handler: &mut impl EventHandler) { 69 | while let Some(event) = self.window.next_event() { 70 | if let Event::Quit = event { 71 | self.is_running = false; 72 | } 73 | 74 | self.input.event(&event); 75 | 76 | event_handler.event(self, event); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/graphics/color.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Mul, MulAssign}; 2 | 3 | use bytemuck::{Pod, Zeroable}; 4 | 5 | #[repr(C)] 6 | #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Zeroable, Pod)] 7 | pub struct Color { 8 | pub r: f32, 9 | pub g: f32, 10 | pub b: f32, 11 | pub a: f32, 12 | } 13 | 14 | impl Color { 15 | pub const BLACK: Color = Color::rgb(0.0, 0.0, 0.0); 16 | pub const WHITE: Color = Color::rgb(1.0, 1.0, 1.0); 17 | pub const RED: Color = Color::rgb(1.0, 0.0, 0.0); 18 | pub const GREEN: Color = Color::rgb(0.0, 1.0, 0.0); 19 | pub const BLUE: Color = Color::rgb(0.0, 0.0, 1.0); 20 | pub const TRANSPARENT: Color = Color::rgba(0.0, 0.0, 0.0, 0.0); 21 | 22 | pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color { 23 | Color { r, g, b, a } 24 | } 25 | 26 | pub const fn rgb(r: f32, g: f32, b: f32) -> Color { 27 | Color { r, g, b, a: 1.0 } 28 | } 29 | 30 | pub fn hex(mut hex: &str) -> Color { 31 | if hex.starts_with('#') { 32 | hex = &hex[1..]; 33 | } 34 | 35 | assert!(hex.len() == 6 || hex.len() == 8); 36 | 37 | let r = u32::from_str_radix(&hex[0..2], 16).unwrap() as f32; 38 | let g = u32::from_str_radix(&hex[2..4], 16).unwrap() as f32; 39 | let b = u32::from_str_radix(&hex[4..6], 16).unwrap() as f32; 40 | 41 | let a = if hex.len() == 8 { 42 | u32::from_str_radix(&hex[6..8], 16).unwrap() as f32 43 | } else { 44 | 255.0 45 | }; 46 | 47 | Color::rgba(r / 255.0, g / 255.0, b / 255.0, a / 255.0) 48 | } 49 | 50 | // TODO: Not sure if this is the best API 51 | pub const fn alpha(a: f32) -> Color { 52 | Color::rgba(a, a, a, a) 53 | } 54 | } 55 | 56 | impl Mul for Color { 57 | type Output = Color; 58 | 59 | fn mul(self, rhs: Self) -> Self::Output { 60 | Color::rgba( 61 | self.r * rhs.r, 62 | self.g * rhs.g, 63 | self.b * rhs.b, 64 | self.a * rhs.a, 65 | ) 66 | } 67 | } 68 | 69 | impl Mul for Color { 70 | type Output = Color; 71 | 72 | fn mul(self, rhs: f32) -> Self::Output { 73 | Color::rgba(self.r * rhs, self.g * rhs, self.b * rhs, self.a * rhs) 74 | } 75 | } 76 | 77 | impl MulAssign for Color { 78 | fn mul_assign(&mut self, rhs: Self) { 79 | self.r *= rhs.r; 80 | self.g *= rhs.g; 81 | self.b *= rhs.b; 82 | self.a *= rhs.a; 83 | } 84 | } 85 | 86 | impl MulAssign for Color { 87 | fn mul_assign(&mut self, rhs: f32) { 88 | self.r *= rhs; 89 | self.g *= rhs; 90 | self.b *= rhs; 91 | self.a *= rhs; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/input/key.rs: -------------------------------------------------------------------------------- 1 | use sdl3_sys::scancode::*; 2 | 3 | macro_rules! keys { 4 | ($($key:ident => $raw:ident),*$(,)?) => { 5 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 6 | pub enum Key { 7 | $($key),* 8 | } 9 | 10 | impl Key { 11 | pub(crate) fn from_raw(raw: SDL_Scancode) -> Option { 12 | match raw { 13 | $($raw => Some(Key::$key)),*, 14 | _ => None, 15 | } 16 | } 17 | } 18 | }; 19 | } 20 | 21 | keys! { 22 | Space => SDL_SCANCODE_SPACE, 23 | Backspace => SDL_SCANCODE_BACKSPACE, 24 | Enter => SDL_SCANCODE_RETURN, 25 | Tab => SDL_SCANCODE_TAB, 26 | CapsLock => SDL_SCANCODE_CAPSLOCK, 27 | Escape => SDL_SCANCODE_ESCAPE, 28 | Delete => SDL_SCANCODE_DELETE, 29 | 30 | LeftShift => SDL_SCANCODE_LSHIFT, 31 | RightShift => SDL_SCANCODE_RSHIFT, 32 | LeftCtrl => SDL_SCANCODE_LCTRL, 33 | RightCtrl => SDL_SCANCODE_RCTRL, 34 | LeftAlt => SDL_SCANCODE_LALT, 35 | RightAlt => SDL_SCANCODE_RALT, 36 | LeftCommand => SDL_SCANCODE_LGUI, 37 | RightCommand => SDL_SCANCODE_RGUI, 38 | 39 | Up => SDL_SCANCODE_UP, 40 | Down => SDL_SCANCODE_DOWN, 41 | Left => SDL_SCANCODE_LEFT, 42 | Right => SDL_SCANCODE_RIGHT, 43 | 44 | A => SDL_SCANCODE_A, 45 | B => SDL_SCANCODE_B, 46 | C => SDL_SCANCODE_C, 47 | D => SDL_SCANCODE_D, 48 | E => SDL_SCANCODE_E, 49 | F => SDL_SCANCODE_F, 50 | G => SDL_SCANCODE_G, 51 | H => SDL_SCANCODE_H, 52 | I => SDL_SCANCODE_I, 53 | J => SDL_SCANCODE_J, 54 | K => SDL_SCANCODE_K, 55 | L => SDL_SCANCODE_L, 56 | M => SDL_SCANCODE_M, 57 | N => SDL_SCANCODE_N, 58 | O => SDL_SCANCODE_O, 59 | P => SDL_SCANCODE_P, 60 | Q => SDL_SCANCODE_Q, 61 | R => SDL_SCANCODE_R, 62 | S => SDL_SCANCODE_S, 63 | T => SDL_SCANCODE_T, 64 | U => SDL_SCANCODE_U, 65 | V => SDL_SCANCODE_V, 66 | W => SDL_SCANCODE_W, 67 | X => SDL_SCANCODE_X, 68 | Y => SDL_SCANCODE_Y, 69 | Z => SDL_SCANCODE_Z, 70 | 71 | Grave => SDL_SCANCODE_GRAVE, 72 | Num0 => SDL_SCANCODE_0, 73 | Num1 => SDL_SCANCODE_1, 74 | Num2 => SDL_SCANCODE_2, 75 | Num3 => SDL_SCANCODE_3, 76 | Num4 => SDL_SCANCODE_4, 77 | Num5 => SDL_SCANCODE_5, 78 | Num6 => SDL_SCANCODE_6, 79 | Num7 => SDL_SCANCODE_7, 80 | Num8 => SDL_SCANCODE_8, 81 | Num9 => SDL_SCANCODE_9, 82 | Minus => SDL_SCANCODE_MINUS, 83 | Equals => SDL_SCANCODE_EQUALS, 84 | 85 | F1 => SDL_SCANCODE_F1, 86 | F2 => SDL_SCANCODE_F2, 87 | F3 => SDL_SCANCODE_F3, 88 | F4 => SDL_SCANCODE_F4, 89 | F5 => SDL_SCANCODE_F5, 90 | F6 => SDL_SCANCODE_F6, 91 | F7 => SDL_SCANCODE_F7, 92 | F8 => SDL_SCANCODE_F8, 93 | F9 => SDL_SCANCODE_F9, 94 | F10 => SDL_SCANCODE_F10, 95 | F11 => SDL_SCANCODE_F11, 96 | F12 => SDL_SCANCODE_F12, 97 | } 98 | -------------------------------------------------------------------------------- /src/graphics/packer.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{Graphics, IRectangle, Texture}; 2 | 3 | /// An individual shelf within the packed atlas, tracking how much space 4 | /// is currently taken up. 5 | #[derive(Copy, Clone, Debug)] 6 | struct Shelf { 7 | current_x: i32, 8 | start_y: i32, 9 | height: i32, 10 | } 11 | 12 | /// Packs texture data into an atlas using a naive shelf-packing algorithm. 13 | pub struct ShelfPacker { 14 | texture: Texture, 15 | shelves: Vec, 16 | next_y: i32, 17 | } 18 | 19 | impl ShelfPacker { 20 | /// Creates a new `ShelfPacker`. 21 | pub fn new(gfx: &Graphics, texture_width: i32, texture_height: i32) -> ShelfPacker { 22 | ShelfPacker { 23 | texture: Texture::empty(gfx, texture_width, texture_height), 24 | shelves: Vec::new(), 25 | next_y: 0, 26 | } 27 | } 28 | 29 | /// Consumes the packer, returning the generated texture. 30 | pub fn into_texture(self) -> Texture { 31 | self.texture 32 | } 33 | 34 | /// Tries to insert RGBA data into the atlas, and returns the position. 35 | /// 36 | /// If the data will not fit into the remaining space, `None` will be returned. 37 | pub fn insert( 38 | &mut self, 39 | data: &[u8], 40 | width: i32, 41 | height: i32, 42 | padding: i32, 43 | ) -> Option { 44 | let padded_width = width + padding * 2; 45 | let padded_height = height + padding * 2; 46 | 47 | let space = self.find_space(padded_width, padded_height); 48 | 49 | if let Some(s) = space { 50 | self.texture 51 | .set_region(s.x + padding, s.y + padding, width, height, data); 52 | } 53 | 54 | space 55 | } 56 | 57 | /// Finds a space in the atlas that can fit a sprite of the specified width and height, 58 | /// and returns the position. 59 | /// 60 | /// If it would not fit into the remaining space, `None` will be returned. 61 | fn find_space(&mut self, source_width: i32, source_height: i32) -> Option { 62 | let texture_width = self.texture.width(); 63 | let texture_height = self.texture.height(); 64 | 65 | self.shelves 66 | .iter_mut() 67 | .find(|shelf| { 68 | shelf.height >= source_height && texture_width - shelf.current_x >= source_width 69 | }) 70 | .map(|shelf| { 71 | // Use existing shelf: 72 | let position = (shelf.current_x, shelf.start_y); 73 | shelf.current_x += source_width; 74 | 75 | IRectangle::new(position.0, position.1, source_width, source_height) 76 | }) 77 | .or_else(|| { 78 | if self.next_y + source_height < texture_height { 79 | // Create new shelf: 80 | let position = (0, self.next_y); 81 | 82 | self.shelves.push(Shelf { 83 | current_x: source_width, 84 | start_y: self.next_y, 85 | height: source_height, 86 | }); 87 | 88 | self.next_y += source_height; 89 | 90 | Some(IRectangle::new( 91 | position.0, 92 | position.1, 93 | source_width, 94 | source_height, 95 | )) 96 | } else { 97 | // Won't fit: 98 | None 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/graphics/mesh.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use bytemuck::{Pod, Zeroable}; 4 | use glam::Vec2; 5 | use glow::HasContext; 6 | 7 | use crate::graphics::Graphics; 8 | 9 | use super::Color; 10 | 11 | #[repr(C)] 12 | #[derive(Copy, Clone, Zeroable, Pod)] 13 | pub struct Vertex { 14 | pub pos: Vec2, 15 | pub uv: Vec2, 16 | pub color: Color, 17 | } 18 | 19 | impl Vertex { 20 | pub const fn new(pos: Vec2, uv: Vec2, color: Color) -> Vertex { 21 | Vertex { pos, uv, color } 22 | } 23 | } 24 | 25 | pub struct Mesh { 26 | pub(crate) raw: Rc, 27 | } 28 | 29 | impl Mesh { 30 | pub fn new(gfx: &Graphics, vertex_count: usize, index_count: usize) -> Mesh { 31 | let raw = RawMesh::new(gfx, vertex_count, index_count); 32 | 33 | Mesh { raw: Rc::new(raw) } 34 | } 35 | 36 | pub fn set_vertices(&self, data: &[Vertex]) { 37 | self.raw.set_vertices(data); 38 | } 39 | 40 | pub fn set_indices(&self, data: &[u32]) { 41 | self.raw.set_indices(data); 42 | } 43 | } 44 | 45 | #[derive(Clone)] 46 | pub struct RawMesh { 47 | pub(crate) gfx: Graphics, 48 | 49 | pub(crate) vertex_buffer: glow::Buffer, 50 | pub(crate) index_buffer: glow::Buffer, 51 | } 52 | 53 | impl RawMesh { 54 | pub fn new(gfx: &Graphics, vertex_count: usize, index_count: usize) -> RawMesh { 55 | unsafe { 56 | let vertex_buffer = gfx.state.gl.create_buffer().unwrap(); 57 | 58 | gfx.bind_vertex_buffer(Some(vertex_buffer)); 59 | 60 | gfx.state.gl.buffer_data_size( 61 | glow::ARRAY_BUFFER, 62 | (vertex_count * std::mem::size_of::()) as i32, 63 | glow::DYNAMIC_DRAW, 64 | ); 65 | 66 | let index_buffer = gfx.state.gl.create_buffer().unwrap(); 67 | 68 | gfx.bind_index_buffer(Some(index_buffer)); 69 | 70 | gfx.state.gl.buffer_data_size( 71 | glow::ELEMENT_ARRAY_BUFFER, 72 | (index_count * std::mem::size_of::()) as i32, 73 | glow::STATIC_DRAW, 74 | ); 75 | 76 | RawMesh { 77 | gfx: gfx.clone(), 78 | 79 | vertex_buffer, 80 | index_buffer, 81 | } 82 | } 83 | } 84 | 85 | pub fn set_vertices(&self, data: &[Vertex]) { 86 | unsafe { 87 | self.gfx.bind_vertex_buffer(Some(self.vertex_buffer)); 88 | 89 | self.gfx.state.gl.buffer_sub_data_u8_slice( 90 | glow::ARRAY_BUFFER, 91 | 0, 92 | bytemuck::cast_slice(data), 93 | ); 94 | } 95 | } 96 | 97 | pub fn set_indices(&self, data: &[u32]) { 98 | unsafe { 99 | self.gfx.bind_index_buffer(Some(self.index_buffer)); 100 | 101 | self.gfx.state.gl.buffer_sub_data_u8_slice( 102 | glow::ELEMENT_ARRAY_BUFFER, 103 | 0, 104 | bytemuck::cast_slice(data), 105 | ); 106 | } 107 | } 108 | } 109 | 110 | impl Drop for RawMesh { 111 | fn drop(&mut self) { 112 | unsafe { 113 | self.gfx.state.gl.delete_buffer(self.vertex_buffer); 114 | 115 | if self.gfx.state.current_vertex_buffer.get() == Some(self.vertex_buffer) { 116 | self.gfx.state.current_vertex_buffer.set(None); 117 | } 118 | 119 | self.gfx.state.gl.delete_buffer(self.index_buffer); 120 | 121 | if self.gfx.state.current_index_buffer.get() == Some(self.index_buffer) { 122 | self.gfx.state.current_index_buffer.set(None); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/graphics/shader.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use glow::HasContext; 4 | 5 | use crate::graphics::{Graphics, State}; 6 | 7 | pub const DEFAULT_VERTEX_SHADER: &str = " 8 | #version 150 9 | 10 | in vec2 a_pos; 11 | in vec2 a_uv; 12 | in vec4 a_color; 13 | 14 | uniform mat4 u_projection; 15 | 16 | out vec2 v_uv; 17 | out vec4 v_color; 18 | 19 | void main() { 20 | v_uv = a_uv; 21 | v_color = a_color; 22 | 23 | gl_Position = u_projection * vec4(a_pos, 0.0, 1.0); 24 | } 25 | "; 26 | 27 | pub const DEFAULT_FRAGMENT_SHADER: &str = " 28 | #version 150 29 | 30 | in vec2 v_uv; 31 | in vec4 v_color; 32 | 33 | uniform sampler2D u_texture; 34 | 35 | out vec4 o_color; 36 | 37 | void main() { 38 | o_color = texture(u_texture, v_uv) * v_color; 39 | } 40 | "; 41 | 42 | #[derive(Clone)] 43 | pub struct Shader { 44 | pub(crate) raw: Rc, 45 | } 46 | 47 | impl Shader { 48 | pub fn from_str(gfx: &Graphics, vertex_src: &str, fragment_src: &str) -> Shader { 49 | let raw = RawShader::new(gfx, vertex_src, fragment_src); 50 | 51 | Shader { raw: Rc::new(raw) } 52 | } 53 | } 54 | 55 | pub struct RawShader { 56 | state: Rc, 57 | pub(crate) id: glow::Program, 58 | } 59 | 60 | impl RawShader { 61 | pub fn new(gfx: &Graphics, vertex_src: &str, fragment_src: &str) -> RawShader { 62 | unsafe { 63 | let program = gfx.state.gl.create_program().unwrap(); 64 | 65 | gfx.state.gl.bind_attrib_location(program, 0, "a_pos"); 66 | gfx.state.gl.bind_attrib_location(program, 1, "a_uv"); 67 | 68 | let vertex_shader = gfx.state.gl.create_shader(glow::VERTEX_SHADER).unwrap(); 69 | 70 | gfx.state.gl.shader_source(vertex_shader, vertex_src); 71 | gfx.state.gl.compile_shader(vertex_shader); 72 | gfx.state.gl.attach_shader(program, vertex_shader); 73 | 74 | if !gfx.state.gl.get_shader_compile_status(vertex_shader) { 75 | panic!("{}", gfx.state.gl.get_shader_info_log(vertex_shader)); 76 | } 77 | 78 | let fragment_shader = gfx.state.gl.create_shader(glow::FRAGMENT_SHADER).unwrap(); 79 | 80 | gfx.state.gl.shader_source(fragment_shader, fragment_src); 81 | gfx.state.gl.compile_shader(fragment_shader); 82 | gfx.state.gl.attach_shader(program, fragment_shader); 83 | 84 | if !gfx.state.gl.get_shader_compile_status(fragment_shader) { 85 | panic!("{}", gfx.state.gl.get_shader_info_log(fragment_shader)); 86 | } 87 | 88 | gfx.state.gl.link_program(program); 89 | 90 | if !gfx.state.gl.get_program_link_status(program) { 91 | panic!("{}", gfx.state.gl.get_program_info_log(program)); 92 | } 93 | 94 | gfx.state.gl.delete_shader(vertex_shader); 95 | gfx.state.gl.delete_shader(fragment_shader); 96 | 97 | gfx.bind_shader(Some(program)); 98 | 99 | let sampler = gfx 100 | .state 101 | .gl 102 | .get_uniform_location(program, "u_texture") 103 | .unwrap(); 104 | 105 | gfx.state.gl.uniform_1_i32(Some(&sampler), 0); 106 | 107 | RawShader { 108 | state: Rc::clone(&gfx.state), 109 | id: program, 110 | } 111 | } 112 | } 113 | } 114 | 115 | impl Drop for RawShader { 116 | fn drop(&mut self) { 117 | unsafe { 118 | self.state.gl.delete_program(self.id); 119 | 120 | if self.state.current_shader.get() == Some(self.id) { 121 | self.state.current_shader.set(None); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /examples/bunnymark.rs: -------------------------------------------------------------------------------- 1 | //! Based on https://github.com/openfl/openfl-samples/tree/master/demos/BunnyMark 2 | //! Original BunnyMark (and sprite) by Iain Lobb 3 | 4 | use nova::app::{App, EventHandler}; 5 | use nova::graphics::{Batcher, Color, DrawParams, Texture}; 6 | use nova::input::{GamepadButton, Key, MouseButton}; 7 | use nova::math::Vec2; 8 | use rand::rngs::ThreadRng; 9 | use rand::{self, Rng}; 10 | 11 | const INITIAL_BUNNIES: usize = 1000; 12 | const MAX_X: f32 = 1280.0 - 26.0; 13 | const MAX_Y: f32 = 720.0 - 37.0; 14 | const GRAVITY: f32 = 0.5; 15 | 16 | fn main() { 17 | let mut app = App::new("Bunnymark", 1280, 720, 60.0); 18 | let mut state = GameState::new(&app); 19 | 20 | app.run(&mut state); 21 | } 22 | 23 | struct Bunny { 24 | position: Vec2, 25 | velocity: Vec2, 26 | } 27 | 28 | impl Bunny { 29 | fn new(rng: &mut ThreadRng) -> Bunny { 30 | let x_vel = rng.gen::() * 5.0; 31 | let y_vel = (rng.gen::() * 5.0) - 2.5; 32 | 33 | Bunny { 34 | position: Vec2::new(0.0, 0.0), 35 | velocity: Vec2::new(x_vel, y_vel), 36 | } 37 | } 38 | } 39 | 40 | struct GameState { 41 | batch: Batcher, 42 | 43 | rng: ThreadRng, 44 | texture: Texture, 45 | bunnies: Vec, 46 | 47 | auto_spawn: bool, 48 | spawn_timer: i32, 49 | } 50 | 51 | impl GameState { 52 | fn new(app: &App) -> GameState { 53 | let mut rng = rand::thread_rng(); 54 | 55 | let texture = Texture::from_file(&app.gfx, "examples/wabbit_alpha.png", true); 56 | 57 | let mut bunnies = Vec::with_capacity(INITIAL_BUNNIES); 58 | 59 | for _ in 0..INITIAL_BUNNIES { 60 | bunnies.push(Bunny::new(&mut rng)); 61 | } 62 | 63 | GameState { 64 | batch: Batcher::new(&app.gfx), 65 | 66 | rng, 67 | texture, 68 | bunnies, 69 | 70 | auto_spawn: false, 71 | spawn_timer: 0, 72 | } 73 | } 74 | } 75 | 76 | impl EventHandler for GameState { 77 | fn update(&mut self, app: &mut App) { 78 | if self.spawn_timer > 0 { 79 | self.spawn_timer -= 1; 80 | } 81 | 82 | if app.input.is_key_pressed(Key::A) { 83 | self.auto_spawn = !self.auto_spawn; 84 | } 85 | 86 | let button_down = app.input.is_key_down(Key::Space) 87 | || app.input.is_mouse_button_down(MouseButton::Left) 88 | || app.input.is_gamepad_button_down(0, GamepadButton::A); 89 | 90 | let should_spawn = self.spawn_timer == 0 && (button_down || self.auto_spawn); 91 | 92 | if should_spawn { 93 | for _ in 0..INITIAL_BUNNIES { 94 | self.bunnies.push(Bunny::new(&mut self.rng)); 95 | } 96 | self.spawn_timer = 10; 97 | } 98 | 99 | for bunny in &mut self.bunnies { 100 | bunny.position += bunny.velocity; 101 | bunny.velocity.y += GRAVITY; 102 | 103 | if bunny.position.x > MAX_X { 104 | bunny.velocity.x *= -1.0; 105 | bunny.position.x = MAX_X; 106 | } else if bunny.position.x < 0.0 { 107 | bunny.velocity.x *= -1.0; 108 | bunny.position.x = 0.0; 109 | } 110 | 111 | if bunny.position.y > MAX_Y { 112 | bunny.velocity.y *= -0.8; 113 | bunny.position.y = MAX_Y; 114 | 115 | if self.rng.gen::() { 116 | bunny.velocity.y -= 3.0 + (self.rng.gen::() * 4.0); 117 | } 118 | } else if bunny.position.y < 0.0 { 119 | bunny.velocity.y = 0.0; 120 | bunny.position.y = 0.0; 121 | } 122 | } 123 | 124 | app.window 125 | .set_title(&format!("BunnyMark - {} bunnies", self.bunnies.len())); 126 | } 127 | 128 | fn draw(&mut self, app: &mut App) { 129 | app.gfx.clear(&app.window, Color::rgb(0.392, 0.584, 0.929)); 130 | 131 | for bunny in &self.bunnies { 132 | self.batch 133 | .texture(&self.texture, bunny.position, DrawParams::new()); 134 | } 135 | 136 | self.batch.draw(&app.window); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/input/gamepad.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, rc::Rc}; 2 | 3 | use sdl3_sys::gamepad::*; 4 | use sdl3_sys::joystick::*; 5 | 6 | /// This is a unique ID for a joystick for the time it is connected to the 7 | /// system. 8 | /// 9 | /// It is never reused for the lifetime of the application. If the joystick is 10 | /// disconnected and reconnected, it will get a new ID. 11 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] 12 | #[repr(transparent)] 13 | pub struct JoystickID(SDL_JoystickID); 14 | 15 | impl JoystickID { 16 | pub fn from_raw(id: SDL_JoystickID) -> JoystickID { 17 | JoystickID(id) 18 | } 19 | } 20 | 21 | #[derive(Clone)] 22 | pub struct Gamepad(Rc); 23 | 24 | impl PartialEq for Gamepad { 25 | fn eq(&self, other: &Self) -> bool { 26 | Rc::ptr_eq(&self.0, &other.0) 27 | } 28 | } 29 | 30 | impl fmt::Debug for Gamepad { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | write!(f, "Gamepad(...)") 33 | } 34 | } 35 | 36 | struct GamepadInner { 37 | handle: *mut SDL_Gamepad, 38 | } 39 | 40 | impl Gamepad { 41 | pub fn from_raw(raw: *mut SDL_Gamepad) -> Gamepad { 42 | Gamepad(Rc::new(GamepadInner { handle: raw })) 43 | } 44 | } 45 | 46 | impl Drop for GamepadInner { 47 | fn drop(&mut self) { 48 | unsafe { 49 | SDL_CloseGamepad(self.handle); 50 | } 51 | } 52 | } 53 | 54 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 55 | pub enum GamepadButton { 56 | A, 57 | B, 58 | X, 59 | Y, 60 | Back, 61 | Guide, 62 | Start, 63 | LeftStick, 64 | RightStick, 65 | LeftShoulder, 66 | RightShoulder, 67 | Up, 68 | Down, 69 | Left, 70 | Right, 71 | } 72 | 73 | impl GamepadButton { 74 | pub(crate) fn from_raw(raw: SDL_GamepadButton) -> Option { 75 | match raw { 76 | SDL_GAMEPAD_BUTTON_SOUTH => Some(GamepadButton::A), 77 | SDL_GAMEPAD_BUTTON_EAST => Some(GamepadButton::B), 78 | SDL_GAMEPAD_BUTTON_WEST => Some(GamepadButton::X), 79 | SDL_GAMEPAD_BUTTON_NORTH => Some(GamepadButton::Y), 80 | SDL_GAMEPAD_BUTTON_BACK => Some(GamepadButton::Back), 81 | SDL_GAMEPAD_BUTTON_GUIDE => Some(GamepadButton::Guide), 82 | SDL_GAMEPAD_BUTTON_START => Some(GamepadButton::Start), 83 | SDL_GAMEPAD_BUTTON_LEFT_STICK => Some(GamepadButton::LeftStick), 84 | SDL_GAMEPAD_BUTTON_RIGHT_STICK => Some(GamepadButton::RightStick), 85 | SDL_GAMEPAD_BUTTON_LEFT_SHOULDER => Some(GamepadButton::LeftShoulder), 86 | SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER => Some(GamepadButton::RightShoulder), 87 | SDL_GAMEPAD_BUTTON_DPAD_UP => Some(GamepadButton::Up), 88 | SDL_GAMEPAD_BUTTON_DPAD_DOWN => Some(GamepadButton::Down), 89 | SDL_GAMEPAD_BUTTON_DPAD_LEFT => Some(GamepadButton::Left), 90 | SDL_GAMEPAD_BUTTON_DPAD_RIGHT => Some(GamepadButton::Right), 91 | _ => None, 92 | } 93 | } 94 | } 95 | 96 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 97 | pub enum GamepadAxis { 98 | LeftStickX, 99 | LeftStickY, 100 | RightStickX, 101 | RightStickY, 102 | LeftTrigger, 103 | RightTrigger, 104 | } 105 | 106 | impl GamepadAxis { 107 | pub(crate) fn from_raw(raw: SDL_GamepadAxis) -> Option { 108 | match raw { 109 | SDL_GAMEPAD_AXIS_LEFTX => Some(GamepadAxis::LeftStickX), 110 | SDL_GAMEPAD_AXIS_LEFTY => Some(GamepadAxis::LeftStickY), 111 | SDL_GAMEPAD_AXIS_RIGHTX => Some(GamepadAxis::RightStickX), 112 | SDL_GAMEPAD_AXIS_RIGHTY => Some(GamepadAxis::RightStickY), 113 | SDL_GAMEPAD_AXIS_LEFT_TRIGGER => Some(GamepadAxis::LeftTrigger), 114 | SDL_GAMEPAD_AXIS_RIGHT_TRIGGER => Some(GamepadAxis::RightTrigger), 115 | _ => None, 116 | } 117 | } 118 | } 119 | 120 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 121 | pub enum GamepadStick { 122 | LeftStick, 123 | RightStick, 124 | } 125 | 126 | impl GamepadStick { 127 | pub fn to_axes(&self) -> (GamepadAxis, GamepadAxis) { 128 | match self { 129 | GamepadStick::LeftStick => (GamepadAxis::LeftStickX, GamepadAxis::LeftStickY), 130 | GamepadStick::RightStick => (GamepadAxis::RightStickX, GamepadAxis::RightStickY), 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/graphics/rectangle.rs: -------------------------------------------------------------------------------- 1 | use glam::{IVec2, Vec2}; 2 | 3 | macro_rules! rect { 4 | ($rect:ident, $t:ty, $point:path, $zero:literal) => { 5 | #[derive(Copy, Clone, Debug, PartialEq)] 6 | pub struct $rect { 7 | pub x: $t, 8 | pub y: $t, 9 | pub width: $t, 10 | pub height: $t, 11 | } 12 | 13 | impl $rect { 14 | pub const ZERO: $rect = $rect::new($zero, $zero, $zero, $zero); 15 | 16 | pub const fn new(x: $t, y: $t, width: $t, height: $t) -> $rect { 17 | $rect { 18 | x, 19 | y, 20 | width, 21 | height, 22 | } 23 | } 24 | 25 | pub const fn from_point(point: $point, width: $t, height: $t) -> $rect { 26 | $rect { 27 | x: point.x, 28 | y: point.y, 29 | width, 30 | height, 31 | } 32 | } 33 | 34 | pub fn left(&self) -> $t { 35 | self.x 36 | } 37 | 38 | pub fn right(&self) -> $t { 39 | self.x + self.width 40 | } 41 | 42 | pub fn top(&self) -> $t { 43 | self.y 44 | } 45 | 46 | pub fn bottom(&self) -> $t { 47 | self.y + self.height 48 | } 49 | 50 | pub fn top_left(&self) -> $point { 51 | <$point>::new(self.x, self.y) 52 | } 53 | 54 | pub fn top_right(&self) -> $point { 55 | <$point>::new(self.x + self.width, self.y) 56 | } 57 | 58 | pub fn bottom_left(&self) -> $point { 59 | <$point>::new(self.x, self.y + self.height) 60 | } 61 | 62 | pub fn bottom_right(&self) -> $point { 63 | <$point>::new(self.x + self.width, self.y + self.height) 64 | } 65 | 66 | pub fn intersects(&self, other: &$rect) -> bool { 67 | self.x < other.x + other.width 68 | && self.x + self.width > other.x 69 | && self.y < other.y + other.height 70 | && self.y + self.height > other.y 71 | } 72 | 73 | pub fn contains(&self, other: &$rect) -> bool { 74 | self.x <= other.x 75 | && other.x + other.width <= self.x + self.width 76 | && self.y <= other.y 77 | && other.y + other.height <= self.y + self.height 78 | } 79 | 80 | pub fn contains_point(&self, point: $point) -> bool { 81 | self.x <= point.x 82 | && point.x < self.x + self.width 83 | && self.y <= point.y 84 | && point.y < self.y + self.height 85 | } 86 | 87 | pub fn combine(&self, other: &$rect) -> $rect { 88 | let x = if self.x < other.x { self.x } else { other.x }; 89 | let y = if self.y < other.y { self.y } else { other.y }; 90 | 91 | let right = if self.right() > other.right() { 92 | self.right() 93 | } else { 94 | other.right() 95 | }; 96 | 97 | let bottom = if self.bottom() > other.bottom() { 98 | self.bottom() 99 | } else { 100 | other.bottom() 101 | }; 102 | 103 | $rect { 104 | x, 105 | y, 106 | width: right - x, 107 | height: bottom - y, 108 | } 109 | } 110 | } 111 | }; 112 | } 113 | 114 | rect!(Rectangle, f32, Vec2, 0.0); 115 | 116 | impl Rectangle { 117 | pub fn as_irectangle(&self) -> IRectangle { 118 | IRectangle { 119 | x: self.x as i32, 120 | y: self.y as i32, 121 | width: self.width as i32, 122 | height: self.height as i32, 123 | } 124 | } 125 | } 126 | 127 | rect!(IRectangle, i32, IVec2, 0); 128 | 129 | impl IRectangle { 130 | pub fn as_rectangle(&self) -> Rectangle { 131 | Rectangle { 132 | x: self.x as f32, 133 | y: self.y as f32, 134 | width: self.width as f32, 135 | height: self.height as f32, 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/graphics/text.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::collections::HashMap; 3 | 4 | use fontdue::{Font as FontdueFont, FontSettings}; 5 | use glam::Vec2; 6 | 7 | use crate::fs; 8 | use crate::graphics::packer::ShelfPacker; 9 | use crate::graphics::{Color, Graphics, Rectangle, Texture}; 10 | 11 | const ATLAS_PADDING: i32 = 1; 12 | 13 | pub struct Font { 14 | data: FontdueFont, 15 | } 16 | 17 | impl Font { 18 | pub fn from_file(path: &str) -> Font { 19 | let bytes = fs::read(path); 20 | Font::from_data(&bytes) 21 | } 22 | 23 | pub fn from_data(data: &[u8]) -> Font { 24 | Font { 25 | data: FontdueFont::from_bytes(data, FontSettings::default()).unwrap(), 26 | } 27 | } 28 | } 29 | 30 | pub struct SpriteFontGlyph { 31 | pub advance: f32, 32 | pub image: Option, 33 | } 34 | 35 | pub struct SpriteFontGlyphImage { 36 | pub offset: Vec2, 37 | pub uv: Rectangle, 38 | } 39 | 40 | pub struct SpriteFont { 41 | pub ascent: f32, 42 | pub descent: f32, 43 | pub line_gap: f32, 44 | 45 | texture: Texture, 46 | cache: HashMap, 47 | kerning: HashMap<(char, char), f32>, 48 | } 49 | 50 | impl SpriteFont { 51 | pub fn new(gfx: &Graphics, font: &Font, size: f32) -> SpriteFont { 52 | // TODO: Refactor to pack then allocate 53 | let mut packer = ShelfPacker::new(gfx, 256, 256); 54 | let mut cache = HashMap::new(); 55 | let mut kerning = HashMap::new(); 56 | 57 | let line_metrics = font.data.horizontal_line_metrics(size).unwrap(); 58 | 59 | for ch in 32u8..128 { 60 | let ch = ch as char; 61 | 62 | let (metrics, data) = font.data.rasterize(ch, size); 63 | 64 | let image = if !data.is_empty() { 65 | let data: Vec = data.into_iter().flat_map(|x| [x, x, x, x]).collect(); 66 | 67 | let uv = packer 68 | .insert( 69 | &data, 70 | metrics.width as i32, 71 | metrics.height as i32, 72 | ATLAS_PADDING, 73 | ) 74 | .expect("out of space"); 75 | 76 | Some(SpriteFontGlyphImage { 77 | offset: Vec2::new( 78 | metrics.bounds.xmin - ATLAS_PADDING as f32, 79 | -metrics.bounds.height - metrics.bounds.ymin - ATLAS_PADDING as f32, 80 | ), 81 | uv: Rectangle::new(uv.x as f32, uv.y as f32, uv.width as f32, uv.height as f32), 82 | }) 83 | } else { 84 | None 85 | }; 86 | 87 | cache.insert( 88 | ch, 89 | SpriteFontGlyph { 90 | advance: metrics.advance_width, 91 | image, 92 | }, 93 | ); 94 | 95 | for ch2 in 32u8..128 { 96 | let ch2 = ch2 as char; 97 | 98 | if let Some(k) = font.data.horizontal_kern(ch, ch2, size) { 99 | kerning.insert((ch, ch2), k); 100 | } 101 | } 102 | } 103 | 104 | SpriteFont { 105 | ascent: line_metrics.ascent, 106 | descent: line_metrics.descent, 107 | line_gap: line_metrics.line_gap, 108 | 109 | texture: packer.into_texture(), 110 | cache, 111 | kerning, 112 | } 113 | } 114 | 115 | pub fn texture(&self) -> &Texture { 116 | &self.texture 117 | } 118 | 119 | pub fn glyph(&self, ch: char) -> Option<&SpriteFontGlyph> { 120 | self.cache.get(&ch) 121 | } 122 | 123 | pub fn line_height(&self) -> f32 { 124 | self.ascent - self.descent + self.line_gap 125 | } 126 | 127 | pub fn kerning(&self, a: char, b: char) -> Option { 128 | self.kerning.get(&(a, b)).copied() 129 | } 130 | } 131 | 132 | pub struct TextSegment<'a> { 133 | pub content: Cow<'a, str>, 134 | pub color: Color, 135 | } 136 | 137 | impl<'a> TextSegment<'a> { 138 | pub fn new(content: impl Into>) -> TextSegment<'a> { 139 | TextSegment { 140 | content: content.into(), 141 | color: Color::WHITE, 142 | } 143 | } 144 | 145 | pub fn color(mut self, color: Color) -> Self { 146 | self.color = color; 147 | self 148 | } 149 | 150 | pub fn into_owned(self) -> TextSegment<'static> { 151 | TextSegment { 152 | content: self.content.into_owned().into(), 153 | color: self.color, 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CStr, CString}; 2 | use std::mem::MaybeUninit; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | 5 | use sdl3_sys::error::*; 6 | use sdl3_sys::events::*; 7 | use sdl3_sys::init::*; 8 | use sdl3_sys::keyboard::*; 9 | use sdl3_sys::version::*; 10 | use sdl3_sys::video::*; 11 | 12 | use glow::Context; 13 | 14 | static SDL_INIT: AtomicBool = AtomicBool::new(false); 15 | 16 | pub struct Window { 17 | window: *mut SDL_Window, 18 | gl: SDL_GLContext, 19 | 20 | visible: bool, 21 | } 22 | 23 | impl Window { 24 | pub fn new(title: &str, width: i32, height: i32) -> Window { 25 | unsafe { 26 | if SDL_INIT.load(Ordering::Relaxed) { 27 | panic!("SDL already initialized"); 28 | } 29 | 30 | if !SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMEPAD) { 31 | sdl_panic!(); 32 | } 33 | 34 | SDL_INIT.store(true, Ordering::Relaxed); 35 | 36 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); 37 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); 38 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); 39 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); 40 | SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); 41 | 42 | let c_title = CString::new(title).unwrap(); 43 | 44 | let window = SDL_CreateWindow( 45 | c_title.as_ptr(), 46 | width, 47 | height, 48 | SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN, 49 | ); 50 | 51 | if window.is_null() { 52 | sdl_panic!(); 53 | } 54 | 55 | SDL_DisableScreenSaver(); 56 | 57 | let gl = SDL_GL_CreateContext(window); 58 | 59 | if gl.is_null() { 60 | sdl_panic!(); 61 | } 62 | 63 | SDL_GL_SetSwapInterval(1); 64 | 65 | let version = SDL_GetVersion(); 66 | 67 | println!( 68 | "SDL Version: {}.{}.{}", 69 | SDL_VERSIONNUM_MAJOR(version), 70 | SDL_VERSIONNUM_MINOR(version), 71 | SDL_VERSIONNUM_MICRO(version), 72 | ); 73 | 74 | Window { 75 | window, 76 | gl, 77 | 78 | visible: false, 79 | } 80 | } 81 | } 82 | 83 | pub fn size(&self) -> (u32, u32) { 84 | unsafe { 85 | let mut w = 0; 86 | let mut h = 0; 87 | 88 | SDL_GetWindowSizeInPixels(self.window, &mut w, &mut h); 89 | 90 | (w as u32, h as u32) 91 | } 92 | } 93 | 94 | pub fn load_gl(&self) -> Context { 95 | unsafe { 96 | Context::from_loader_function_cstr(|s| { 97 | if let Some(ptr) = SDL_GL_GetProcAddress(s.as_ptr()) { 98 | ptr as *mut _ 99 | } else { 100 | std::ptr::null() 101 | } 102 | }) 103 | } 104 | } 105 | 106 | pub fn next_event(&mut self) -> Option { 107 | unsafe { 108 | let mut raw_event = MaybeUninit::uninit(); 109 | 110 | if SDL_PollEvent(raw_event.as_mut_ptr()) { 111 | let raw_event = raw_event.assume_init(); 112 | 113 | Event::from_raw(&raw_event) 114 | } else { 115 | None 116 | } 117 | } 118 | } 119 | 120 | pub fn present(&mut self) { 121 | unsafe { 122 | SDL_GL_SwapWindow(self.window); 123 | 124 | if !self.visible { 125 | SDL_ShowWindow(self.window); 126 | self.visible = true; 127 | } 128 | } 129 | } 130 | 131 | pub fn set_title(&mut self, title: &str) { 132 | let c_title = CString::new(title).unwrap(); 133 | 134 | unsafe { 135 | SDL_SetWindowTitle(self.window, c_title.as_ptr()); 136 | } 137 | } 138 | 139 | pub fn start_text_input(&mut self) { 140 | unsafe { 141 | SDL_StartTextInput(self.window); 142 | } 143 | } 144 | 145 | pub fn stop_text_input(&mut self) { 146 | unsafe { 147 | SDL_StopTextInput(self.window); 148 | } 149 | } 150 | } 151 | 152 | impl Drop for Window { 153 | fn drop(&mut self) { 154 | unsafe { 155 | SDL_GL_DestroyContext(self.gl); 156 | SDL_DestroyWindow(self.window); 157 | } 158 | } 159 | } 160 | 161 | pub(crate) unsafe fn get_err() -> String { 162 | unsafe { 163 | CStr::from_ptr(SDL_GetError()) 164 | .to_string_lossy() 165 | .into_owned() 166 | } 167 | } 168 | 169 | macro_rules! sdl_panic { 170 | () => { 171 | panic!("SDL error: {}", $crate::window::get_err()); 172 | }; 173 | } 174 | 175 | pub(crate) use sdl_panic; 176 | 177 | use crate::input::Event; 178 | -------------------------------------------------------------------------------- /src/graphics/texture.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use glow::{HasContext, PixelUnpackData}; 4 | use png::{BitDepth, ColorType, Decoder}; 5 | 6 | use crate::fs; 7 | use crate::graphics::Graphics; 8 | 9 | #[derive(Clone)] 10 | pub struct Texture { 11 | pub(crate) raw: Rc, 12 | } 13 | 14 | impl Texture { 15 | pub fn from_file(gfx: &Graphics, path: &str, premultiply: bool) -> Texture { 16 | let bytes = fs::read(path); 17 | 18 | let decoder = Decoder::new(bytes.as_slice()); 19 | let mut reader = decoder.read_info().unwrap(); 20 | let mut buf = vec![0; reader.output_buffer_size()]; 21 | let info = reader.next_frame(&mut buf).unwrap(); 22 | 23 | assert!(info.color_type == ColorType::Rgba); 24 | assert!(info.bit_depth == BitDepth::Eight); 25 | 26 | if premultiply { 27 | for pixel in buf.chunks_mut(4) { 28 | let a = pixel[3]; 29 | 30 | if a == 0 { 31 | pixel[0] = 0; 32 | pixel[1] = 0; 33 | pixel[2] = 0; 34 | } else if a < 255 { 35 | pixel[0] = ((pixel[0] as u16 * a as u16) >> 8) as u8; 36 | pixel[1] = ((pixel[1] as u16 * a as u16) >> 8) as u8; 37 | pixel[2] = ((pixel[2] as u16 * a as u16) >> 8) as u8; 38 | } 39 | } 40 | } 41 | 42 | Texture::from_data(gfx, info.width as i32, info.height as i32, &buf) 43 | } 44 | 45 | pub fn from_data(gfx: &Graphics, width: i32, height: i32, data: &[u8]) -> Texture { 46 | let raw = RawTexture::new(gfx, width, height, data); 47 | 48 | Texture { raw: Rc::new(raw) } 49 | } 50 | 51 | pub fn empty(gfx: &Graphics, width: i32, height: i32) -> Texture { 52 | Texture::from_data( 53 | gfx, 54 | width, 55 | height, 56 | &vec![0; width as usize * height as usize * 4], 57 | ) 58 | } 59 | 60 | pub fn width(&self) -> i32 { 61 | self.raw.width 62 | } 63 | 64 | pub fn height(&self) -> i32 { 65 | self.raw.height 66 | } 67 | 68 | pub fn size(&self) -> (i32, i32) { 69 | (self.raw.width, self.raw.height) 70 | } 71 | 72 | pub fn set_data(&self, data: &[u8]) { 73 | self.raw 74 | .set_region(0, 0, self.raw.width, self.raw.height, data); 75 | } 76 | 77 | pub fn set_region(&self, x: i32, y: i32, width: i32, height: i32, data: &[u8]) { 78 | self.raw.set_region(x, y, width, height, data); 79 | } 80 | } 81 | 82 | impl PartialEq for Texture { 83 | fn eq(&self, other: &Self) -> bool { 84 | self.raw.id == other.raw.id 85 | } 86 | } 87 | 88 | pub struct RawTexture { 89 | gfx: Graphics, 90 | pub(crate) id: glow::Texture, 91 | width: i32, 92 | height: i32, 93 | } 94 | 95 | impl RawTexture { 96 | pub fn new(gfx: &Graphics, width: i32, height: i32, data: &[u8]) -> RawTexture { 97 | unsafe { 98 | assert_eq!(width as usize * height as usize * 4, data.len()); 99 | 100 | let id = gfx.state.gl.create_texture().unwrap(); 101 | 102 | gfx.bind_texture(Some(id)); 103 | 104 | gfx.state.gl.tex_parameter_i32( 105 | glow::TEXTURE_2D, 106 | glow::TEXTURE_MIN_FILTER, 107 | glow::NEAREST as i32, 108 | ); 109 | 110 | gfx.state.gl.tex_parameter_i32( 111 | glow::TEXTURE_2D, 112 | glow::TEXTURE_MAG_FILTER, 113 | glow::NEAREST as i32, 114 | ); 115 | 116 | gfx.state.gl.tex_parameter_i32( 117 | glow::TEXTURE_2D, 118 | glow::TEXTURE_WRAP_S, 119 | glow::CLAMP_TO_EDGE as i32, 120 | ); 121 | 122 | gfx.state.gl.tex_parameter_i32( 123 | glow::TEXTURE_2D, 124 | glow::TEXTURE_WRAP_T, 125 | glow::CLAMP_TO_EDGE as i32, 126 | ); 127 | 128 | gfx.state 129 | .gl 130 | .tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_BASE_LEVEL, 0); 131 | 132 | gfx.state 133 | .gl 134 | .tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAX_LEVEL, 0); 135 | 136 | gfx.state.gl.tex_image_2d( 137 | glow::TEXTURE_2D, 138 | 0, 139 | glow::RGBA8 as i32, 140 | width, 141 | height, 142 | 0, 143 | glow::RGBA, 144 | glow::UNSIGNED_BYTE, 145 | PixelUnpackData::Slice(Some(data)), 146 | ); 147 | 148 | RawTexture { 149 | gfx: gfx.clone(), 150 | id, 151 | width, 152 | height, 153 | } 154 | } 155 | } 156 | 157 | pub fn set_region(&self, x: i32, y: i32, width: i32, height: i32, data: &[u8]) { 158 | unsafe { 159 | assert_eq!(width as usize * height as usize * 4, data.len()); 160 | assert!(x >= 0 && y >= 0 && x + width <= self.width && y + height <= self.height); 161 | 162 | self.gfx.bind_texture(Some(self.id)); 163 | 164 | self.gfx.state.gl.tex_sub_image_2d( 165 | glow::TEXTURE_2D, 166 | 0, 167 | x, 168 | y, 169 | width, 170 | height, 171 | glow::RGBA, 172 | glow::UNSIGNED_BYTE, 173 | PixelUnpackData::Slice(Some(data)), 174 | ) 175 | } 176 | } 177 | } 178 | 179 | impl Drop for RawTexture { 180 | fn drop(&mut self) { 181 | unsafe { 182 | self.gfx.state.gl.delete_texture(self.id); 183 | 184 | if self.gfx.state.current_texture.get() == Some(self.id) { 185 | self.gfx.state.current_texture.set(None); 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/input/event.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec2; 2 | use sdl3_sys::events::*; 3 | use sdl3_sys::gamepad::*; 4 | 5 | use super::{Gamepad, GamepadAxis, GamepadButton, JoystickID, Key, MouseButton}; 6 | 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub enum Event { 9 | Quit, 10 | KeyDown(Key), 11 | KeyUp(Key), 12 | MouseButtonDown(MouseButton), 13 | MouseButtonUp(MouseButton), 14 | 15 | MouseMotion { 16 | new_position: Vec2, 17 | }, 18 | 19 | ControllerDeviceAdded { 20 | joystick: JoystickID, 21 | gamepad: Gamepad, 22 | }, 23 | 24 | ControllerDeviceRemoved { 25 | joystick: JoystickID, 26 | }, 27 | 28 | ControllerButtonDown { 29 | joystick: JoystickID, 30 | button: GamepadButton, 31 | }, 32 | 33 | ControllerButtonUp { 34 | joystick: JoystickID, 35 | button: GamepadButton, 36 | }, 37 | 38 | ControllerAxisMotion { 39 | joystick: JoystickID, 40 | axis: GamepadAxis, 41 | value: f32, 42 | }, 43 | 44 | WindowResized { 45 | width: u32, 46 | height: u32, 47 | }, 48 | 49 | TextInput { 50 | text: String, 51 | }, 52 | } 53 | 54 | impl Event { 55 | pub fn from_raw(event: &SDL_Event) -> Option { 56 | unsafe { 57 | match SDL_EventType(event.r#type) { 58 | SDL_EVENT_QUIT => { 59 | return Some(Event::Quit); 60 | } 61 | 62 | SDL_EVENT_KEY_DOWN if !event.key.repeat => { 63 | if let Some(key) = Key::from_raw(event.key.scancode) { 64 | return Some(Event::KeyDown(key)); 65 | } 66 | } 67 | 68 | SDL_EVENT_KEY_UP if !event.key.repeat => { 69 | if let Some(key) = Key::from_raw(event.key.scancode) { 70 | return Some(Event::KeyUp(key)); 71 | } 72 | } 73 | 74 | SDL_EVENT_MOUSE_BUTTON_DOWN => { 75 | if let Some(button) = MouseButton::from_raw(event.button.button as i32) { 76 | return Some(Event::MouseButtonDown(button)); 77 | } 78 | } 79 | 80 | SDL_EVENT_MOUSE_BUTTON_UP => { 81 | if let Some(button) = MouseButton::from_raw(event.button.button as i32) { 82 | return Some(Event::MouseButtonUp(button)); 83 | } 84 | } 85 | 86 | SDL_EVENT_MOUSE_MOTION => { 87 | return Some(Event::MouseMotion { 88 | new_position: Vec2::new(event.motion.x, event.motion.y), 89 | }); 90 | } 91 | 92 | SDL_EVENT_GAMEPAD_ADDED => { 93 | let handle = SDL_OpenGamepad(event.gdevice.which); 94 | 95 | if handle.is_null() { 96 | // TODO: Should probably log here 97 | return None; 98 | } 99 | 100 | let joystick = JoystickID::from_raw(event.gdevice.which); 101 | let gamepad = Gamepad::from_raw(handle); 102 | 103 | return Some(Event::ControllerDeviceAdded { joystick, gamepad }); 104 | } 105 | 106 | SDL_EVENT_GAMEPAD_REMOVED => { 107 | return Some(Event::ControllerDeviceRemoved { 108 | joystick: JoystickID::from_raw(event.gdevice.which), 109 | }); 110 | } 111 | 112 | SDL_EVENT_GAMEPAD_BUTTON_DOWN => { 113 | if let Some(button) = 114 | GamepadButton::from_raw(SDL_GamepadButton(event.gbutton.button as i32)) 115 | { 116 | return Some(Event::ControllerButtonDown { 117 | joystick: JoystickID::from_raw(event.gdevice.which), 118 | button, 119 | }); 120 | } 121 | } 122 | 123 | SDL_EVENT_GAMEPAD_BUTTON_UP => { 124 | if let Some(button) = 125 | GamepadButton::from_raw(SDL_GamepadButton(event.gbutton.button as i32)) 126 | { 127 | return Some(Event::ControllerButtonUp { 128 | joystick: JoystickID::from_raw(event.gdevice.which), 129 | button, 130 | }); 131 | } 132 | } 133 | 134 | SDL_EVENT_GAMEPAD_AXIS_MOTION => { 135 | if let Some(axis) = 136 | GamepadAxis::from_raw(SDL_GamepadAxis(event.gaxis.axis as i32)) 137 | { 138 | let mut value = if event.gaxis.value > 0 { 139 | event.gaxis.value as f32 / 32767.0 140 | } else { 141 | event.gaxis.value as f32 / 32768.0 142 | }; 143 | 144 | // TODO: Add less hacky deadzone logic 145 | if value.abs() < 0.2 { 146 | value = 0.0; 147 | } 148 | return Some(Event::ControllerAxisMotion { 149 | joystick: JoystickID::from_raw(event.gdevice.which), 150 | axis, 151 | value, 152 | }); 153 | } 154 | } 155 | 156 | SDL_EVENT_WINDOW_RESIZED => { 157 | let e = &event.window; 158 | if e.data1 > 0 && e.data2 > 0 { 159 | let width = e.data1 as u32; 160 | let height = e.data2 as u32; 161 | return Some(Event::WindowResized { width, height }); 162 | } 163 | } 164 | 165 | SDL_EVENT_TEXT_INPUT => { 166 | let text = std::ffi::CStr::from_ptr(event.text.text) 167 | .to_string_lossy() 168 | .into_owned(); 169 | 170 | return Some(Event::TextInput { text }); 171 | } 172 | 173 | _ => {} 174 | } 175 | } 176 | 177 | None 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/ldtk.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use glam::{BVec2, IVec2}; 4 | use serde::{Deserialize, Deserializer}; 5 | 6 | #[derive(Clone, Debug, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct Project { 9 | pub levels: Vec, 10 | pub defs: Defs, 11 | } 12 | 13 | impl Project { 14 | pub fn from_file(path: impl AsRef) -> Project { 15 | let json = std::fs::read_to_string(path).unwrap(); 16 | serde_json::from_str(&json).unwrap() 17 | } 18 | 19 | pub fn get_level(&self, id: &str) -> Option<&Level> { 20 | self.levels.iter().find(|l| l.identifier == id) 21 | } 22 | 23 | pub fn get_level_by_iid(&self, id: &str) -> Option<&Level> { 24 | self.levels.iter().find(|l| l.iid == id) 25 | } 26 | } 27 | 28 | #[derive(Clone, Debug, Deserialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct Defs { 31 | pub tilesets: Vec, 32 | } 33 | 34 | impl Defs { 35 | pub fn get_tileset(&self, id: &str) -> Option<&Tileset> { 36 | self.tilesets.iter().find(|t| t.identifier == id) 37 | } 38 | 39 | pub fn get_tileset_by_uid(&self, uid: i32) -> Option<&Tileset> { 40 | self.tilesets.iter().find(|t| t.uid == uid) 41 | } 42 | } 43 | 44 | #[derive(Clone, Debug, Deserialize)] 45 | #[serde(rename_all = "camelCase")] 46 | pub struct Tileset { 47 | pub identifier: String, 48 | pub uid: i32, 49 | pub enum_tags: Vec, 50 | } 51 | 52 | impl Tileset { 53 | pub fn get_enum_tag(&self, id: &str) -> Option<&EnumTag> { 54 | self.enum_tags.iter().find(|l| l.enum_value_id == id) 55 | } 56 | } 57 | 58 | #[derive(Clone, Debug, Deserialize)] 59 | #[serde(rename_all = "camelCase")] 60 | pub struct EnumTag { 61 | pub enum_value_id: String, 62 | pub tile_ids: Vec, 63 | } 64 | 65 | #[derive(Clone, Debug, Deserialize)] 66 | #[serde(rename_all = "camelCase")] 67 | pub struct Level { 68 | #[serde(rename = "__neighbours")] 69 | pub neighbours: Vec, 70 | 71 | pub identifier: String, 72 | 73 | pub iid: String, 74 | 75 | #[serde(rename = "pxWid")] 76 | pub width: i32, 77 | 78 | #[serde(rename = "pxHei")] 79 | pub height: i32, 80 | 81 | #[serde(default = "Vec::new")] 82 | pub layer_instances: Vec, 83 | } 84 | 85 | impl Level { 86 | pub fn get_layer(&self, id: &str) -> Option<&LayerInstance> { 87 | self.layer_instances.iter().find(|l| l.identifier == id) 88 | } 89 | } 90 | 91 | #[derive(Clone, Debug, Deserialize)] 92 | #[serde(rename_all = "camelCase")] 93 | pub struct Neighbour { 94 | pub dir: NeighbourDirection, 95 | pub level_iid: String, 96 | } 97 | 98 | #[derive(Clone, Debug, Deserialize)] 99 | pub enum NeighbourDirection { 100 | #[serde(rename = "n")] 101 | North, 102 | 103 | #[serde(rename = "s")] 104 | South, 105 | 106 | #[serde(rename = "e")] 107 | East, 108 | 109 | #[serde(rename = "w")] 110 | West, 111 | } 112 | 113 | #[derive(Clone, Debug, Deserialize)] 114 | #[serde(rename_all = "camelCase")] 115 | pub struct LayerInstance { 116 | #[serde(rename = "__cWid")] 117 | pub width: i32, 118 | 119 | #[serde(rename = "__cHei")] 120 | pub height: i32, 121 | 122 | #[serde(rename = "__gridSize")] 123 | pub grid_size: i32, 124 | 125 | #[serde(rename = "__identifier")] 126 | pub identifier: String, 127 | 128 | #[serde(rename = "__tilesetDefUid")] 129 | pub tileset_def_uid: Option, 130 | 131 | #[serde(rename = "__tilesetRelPath")] 132 | pub tileset_rel_path: Option, 133 | 134 | pub entity_instances: Vec, 135 | 136 | pub grid_tiles: Vec, 137 | 138 | pub auto_layer_tiles: Vec, 139 | 140 | pub int_grid_csv: Vec, 141 | } 142 | 143 | impl LayerInstance { 144 | pub fn get_int_grid(&self) -> impl Iterator + '_ { 145 | let width = self.width; 146 | 147 | self.int_grid_csv 148 | .iter() 149 | .enumerate() 150 | .filter_map(move |(i, val)| { 151 | if *val > 0 { 152 | let x = i % width as usize; 153 | let y = i / width as usize; 154 | 155 | Some(IntGridTile { 156 | position: IVec2::new(x as i32, y as i32), 157 | value: *val, 158 | }) 159 | } else { 160 | None 161 | } 162 | }) 163 | } 164 | } 165 | 166 | #[derive(Clone, Debug, Deserialize)] 167 | #[serde(rename_all = "camelCase")] 168 | pub struct Tile { 169 | #[serde(rename = "t")] 170 | pub id: i32, 171 | 172 | #[serde(deserialize_with = "deserialize_ldtk_point")] 173 | pub px: IVec2, 174 | 175 | #[serde(deserialize_with = "deserialize_ldtk_point")] 176 | pub src: IVec2, 177 | 178 | #[serde(rename = "f")] 179 | #[serde(deserialize_with = "deserialize_ldtk_flags")] 180 | pub flip: BVec2, 181 | } 182 | 183 | pub struct IntGridTile { 184 | pub position: IVec2, 185 | pub value: i32, 186 | } 187 | 188 | #[derive(Clone, Debug, Deserialize)] 189 | #[serde(rename_all = "camelCase")] 190 | pub struct EntityInstance { 191 | #[serde(rename = "__grid")] 192 | #[serde(deserialize_with = "deserialize_ldtk_point")] 193 | pub grid: IVec2, 194 | 195 | pub field_instances: Vec, 196 | } 197 | 198 | impl EntityInstance { 199 | pub fn get_field_instance(&self, id: &str) -> Option<&FieldInstance> { 200 | self.field_instances.iter().find(|x| x.identifier == id) 201 | } 202 | } 203 | 204 | #[derive(Clone, Debug, Deserialize)] 205 | #[serde(rename_all = "camelCase")] 206 | pub struct FieldInstance { 207 | #[serde(rename = "__identifier")] 208 | pub identifier: String, 209 | 210 | // TODO: Make this nicer 211 | #[serde(rename = "__value")] 212 | pub value: Option, 213 | } 214 | 215 | fn deserialize_ldtk_point<'de, D>(de: D) -> Result 216 | where 217 | D: Deserializer<'de>, 218 | { 219 | let vals: [i32; 2] = Deserialize::deserialize(de)?; 220 | 221 | Ok(IVec2::from(vals)) 222 | } 223 | 224 | fn deserialize_ldtk_flags<'de, D>(de: D) -> Result 225 | where 226 | D: Deserializer<'de>, 227 | { 228 | let flags: u8 = Deserialize::deserialize(de)?; 229 | 230 | let x = flags == 1 || flags == 3; 231 | let y = flags == 2 || flags == 3; 232 | 233 | Ok(BVec2::new(x, y)) 234 | } 235 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | mod event; 2 | mod gamepad; 3 | mod key; 4 | mod mouse; 5 | 6 | use std::collections::{HashMap, HashSet}; 7 | use std::hash::Hash; 8 | 9 | use glam::Vec2; 10 | 11 | pub use self::event::*; 12 | pub use self::gamepad::*; 13 | pub use self::key::*; 14 | pub use self::mouse::*; 15 | 16 | pub struct Input { 17 | keys: ButtonState, 18 | mouse_buttons: ButtonState, 19 | gamepad_buttons: ButtonState<(usize, GamepadButton)>, 20 | 21 | axes: AxisState, 22 | mouse_position: Vec2, 23 | 24 | gamepads: Vec>, 25 | joystick_ids: HashMap, 26 | } 27 | 28 | impl Input { 29 | pub fn new() -> Input { 30 | Input { 31 | keys: ButtonState::new(), 32 | mouse_buttons: ButtonState::new(), 33 | gamepad_buttons: ButtonState::new(), 34 | 35 | axes: AxisState::new(), 36 | mouse_position: Vec2::ZERO, 37 | 38 | gamepads: Vec::new(), 39 | joystick_ids: HashMap::new(), 40 | } 41 | } 42 | 43 | pub fn event(&mut self, event: &Event) { 44 | match event { 45 | Event::KeyDown(key) => self.keys.set_down(*key), 46 | Event::KeyUp(key) => self.keys.set_up(*key), 47 | Event::MouseButtonDown(button) => self.mouse_buttons.set_down(*button), 48 | Event::MouseButtonUp(button) => self.mouse_buttons.set_up(*button), 49 | Event::MouseMotion { new_position } => self.mouse_position = *new_position, 50 | 51 | Event::ControllerDeviceAdded { joystick, gamepad } => { 52 | let empty_slot = self.gamepads.iter().position(Option::is_none); 53 | 54 | let gamepad_id = match empty_slot { 55 | Some(slot) => { 56 | self.gamepads[slot] = Some(gamepad.clone()); 57 | slot 58 | } 59 | None => { 60 | self.gamepads.push(Some(gamepad.clone())); 61 | self.gamepads.len() - 1 62 | } 63 | }; 64 | 65 | self.joystick_ids.insert(*joystick, gamepad_id); 66 | } 67 | 68 | Event::ControllerDeviceRemoved { joystick } => { 69 | if let Some(gamepad_id) = self.joystick_ids.remove(joystick) { 70 | self.gamepads[gamepad_id] = None; 71 | } 72 | } 73 | 74 | Event::ControllerButtonDown { joystick, button } => { 75 | if let Some(gamepad_id) = self.joystick_ids.get(joystick) { 76 | self.gamepad_buttons.set_down((*gamepad_id, *button)); 77 | } 78 | } 79 | 80 | Event::ControllerButtonUp { joystick, button } => { 81 | if let Some(gamepad_id) = self.joystick_ids.get(joystick) { 82 | self.gamepad_buttons.set_up((*gamepad_id, *button)); 83 | } 84 | } 85 | 86 | Event::ControllerAxisMotion { 87 | joystick, 88 | axis, 89 | value, 90 | } => { 91 | if let Some(gamepad_id) = self.joystick_ids.get(joystick) { 92 | self.axes.set_value(*gamepad_id, *axis, *value); 93 | } 94 | } 95 | 96 | _ => {} 97 | } 98 | } 99 | 100 | pub fn clear(&mut self) { 101 | self.keys.clear(); 102 | self.mouse_buttons.clear(); 103 | self.gamepad_buttons.clear(); 104 | self.axes.clear(); 105 | } 106 | 107 | pub fn is_key_down(&self, key: Key) -> bool { 108 | self.keys.is_down(key) 109 | } 110 | 111 | pub fn is_key_up(&self, key: Key) -> bool { 112 | self.keys.is_up(key) 113 | } 114 | 115 | pub fn is_key_pressed(&self, key: Key) -> bool { 116 | self.keys.is_pressed(key) 117 | } 118 | 119 | pub fn is_key_released(&self, key: Key) -> bool { 120 | self.keys.is_released(key) 121 | } 122 | 123 | pub fn is_mouse_button_down(&self, button: MouseButton) -> bool { 124 | self.mouse_buttons.is_down(button) 125 | } 126 | 127 | pub fn is_mouse_button_up(&self, button: MouseButton) -> bool { 128 | self.mouse_buttons.is_up(button) 129 | } 130 | 131 | pub fn is_mouse_button_pressed(&self, button: MouseButton) -> bool { 132 | self.mouse_buttons.is_pressed(button) 133 | } 134 | 135 | pub fn is_mouse_button_released(&self, button: MouseButton) -> bool { 136 | self.mouse_buttons.is_released(button) 137 | } 138 | 139 | pub fn mouse_position(&self) -> Vec2 { 140 | self.mouse_position 141 | } 142 | 143 | pub fn is_gamepad_button_down(&self, player: usize, button: GamepadButton) -> bool { 144 | self.gamepad_buttons.is_down((player, button)) 145 | } 146 | 147 | pub fn is_gamepad_button_up(&self, player: usize, button: GamepadButton) -> bool { 148 | self.gamepad_buttons.is_up((player, button)) 149 | } 150 | 151 | pub fn is_gamepad_button_pressed(&self, player: usize, button: GamepadButton) -> bool { 152 | self.gamepad_buttons.is_pressed((player, button)) 153 | } 154 | 155 | pub fn is_gamepad_button_released(&self, player: usize, button: GamepadButton) -> bool { 156 | self.gamepad_buttons.is_released((player, button)) 157 | } 158 | 159 | pub fn gamepad_axis(&self, player: usize, axis: GamepadAxis) -> f32 { 160 | self.axes.get_value(player, axis) 161 | } 162 | 163 | pub fn has_gamepad_axis_moved(&self, player: usize, axis: GamepadAxis) -> bool { 164 | self.axes.has_moved(player, axis) 165 | } 166 | 167 | pub fn gamepad_stick(&self, player: usize, stick: GamepadStick) -> Vec2 { 168 | let (x, y) = stick.to_axes(); 169 | 170 | let x_val = self.axes.get_value(player, x); 171 | let y_val = self.axes.get_value(player, y); 172 | 173 | Vec2::new(x_val, y_val) 174 | } 175 | 176 | pub fn has_gamepad_stick_moved(&self, player: usize, stick: GamepadStick) -> bool { 177 | let (x, y) = stick.to_axes(); 178 | 179 | self.axes.has_moved(player, x) || self.axes.has_moved(player, y) 180 | } 181 | } 182 | 183 | pub(crate) struct ButtonState { 184 | down: HashSet, 185 | pressed: HashSet, 186 | released: HashSet, 187 | } 188 | 189 | impl ButtonState { 190 | fn new() -> ButtonState { 191 | ButtonState { 192 | down: HashSet::new(), 193 | pressed: HashSet::new(), 194 | released: HashSet::new(), 195 | } 196 | } 197 | 198 | fn clear(&mut self) { 199 | self.pressed.clear(); 200 | self.released.clear(); 201 | } 202 | 203 | fn set_down(&mut self, button: T) { 204 | let was_up = self.down.insert(button); 205 | 206 | if was_up { 207 | self.pressed.insert(button); 208 | } 209 | } 210 | 211 | fn set_up(&mut self, button: T) { 212 | let was_down = self.down.remove(&button); 213 | 214 | if was_down { 215 | self.released.insert(button); 216 | } 217 | } 218 | 219 | fn is_down(&self, button: T) -> bool { 220 | self.down.contains(&button) 221 | } 222 | 223 | fn is_up(&self, button: T) -> bool { 224 | !self.down.contains(&button) 225 | } 226 | 227 | fn is_pressed(&self, button: T) -> bool { 228 | self.pressed.contains(&button) 229 | } 230 | 231 | fn is_released(&self, button: T) -> bool { 232 | self.released.contains(&button) 233 | } 234 | } 235 | 236 | pub(crate) struct AxisState { 237 | curr: HashMap<(usize, GamepadAxis), f32>, 238 | prev: HashMap<(usize, GamepadAxis), f32>, 239 | } 240 | 241 | impl AxisState { 242 | fn new() -> AxisState { 243 | AxisState { 244 | curr: HashMap::new(), 245 | prev: HashMap::new(), 246 | } 247 | } 248 | 249 | fn clear(&mut self) { 250 | self.prev = self.curr.clone(); 251 | } 252 | 253 | fn get_value(&self, player: usize, axis: GamepadAxis) -> f32 { 254 | *self.curr.get(&(player, axis)).unwrap_or(&0.0) 255 | } 256 | 257 | fn set_value(&mut self, player: usize, axis: GamepadAxis, value: f32) { 258 | self.curr.insert((player, axis), value); 259 | } 260 | 261 | fn has_moved(&self, player: usize, axis: GamepadAxis) -> bool { 262 | self.curr.get(&(player, axis)) != self.prev.get(&(player, axis)) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/graphics.rs: -------------------------------------------------------------------------------- 1 | mod batch; 2 | mod canvas; 3 | mod color; 4 | mod mesh; 5 | mod packer; 6 | mod rectangle; 7 | mod scaling; 8 | mod shader; 9 | mod text; 10 | mod texture; 11 | 12 | use std::rc::Rc; 13 | use std::{cell::Cell, sync::Arc}; 14 | 15 | use glam::Mat4; 16 | use glow::{Context, HasContext}; 17 | 18 | pub use batch::*; 19 | pub use canvas::*; 20 | pub use color::*; 21 | pub use mesh::*; 22 | pub use rectangle::*; 23 | pub use scaling::*; 24 | pub use shader::*; 25 | pub use text::*; 26 | pub use texture::*; 27 | 28 | use crate::window::Window; 29 | 30 | struct State { 31 | gl: Arc, 32 | 33 | vao: glow::NativeVertexArray, 34 | current_vertex_buffer: Cell>, 35 | current_index_buffer: Cell>, 36 | current_shader: Cell>, 37 | current_texture: Cell>, 38 | current_canvas: Cell>, 39 | } 40 | 41 | #[derive(Clone)] 42 | pub struct Graphics { 43 | state: Rc, 44 | } 45 | 46 | impl Graphics { 47 | pub fn new(window: &mut Window) -> Graphics { 48 | let gl = window.load_gl(); 49 | 50 | let vao: glow::NativeVertexArray; 51 | unsafe { 52 | vao = gl.create_vertex_array().unwrap(); 53 | gl.bind_vertex_array(Some(vao)); 54 | 55 | gl.enable(glow::CULL_FACE); 56 | gl.enable(glow::BLEND); 57 | gl.blend_func_separate( 58 | glow::ONE, 59 | glow::ONE_MINUS_SRC_ALPHA, 60 | glow::ONE, 61 | glow::ONE_MINUS_SRC_ALPHA, 62 | ); 63 | 64 | println!("OpenGL Version: {}", gl.get_parameter_string(glow::VERSION)); 65 | println!("Renderer: {}", gl.get_parameter_string(glow::RENDERER)); 66 | } 67 | 68 | Graphics { 69 | state: Rc::new(State { 70 | gl: Arc::new(gl), 71 | 72 | vao, 73 | current_vertex_buffer: Cell::new(None), 74 | current_index_buffer: Cell::new(None), 75 | current_shader: Cell::new(None), 76 | current_texture: Cell::new(None), 77 | current_canvas: Cell::new(None), 78 | }), 79 | } 80 | } 81 | 82 | pub fn clear(&self, target: &impl Target, color: Color) { 83 | unsafe { 84 | target.bind(self); 85 | 86 | self.state 87 | .gl 88 | .clear_color(color.r, color.g, color.b, color.a); 89 | 90 | self.state.gl.clear(glow::COLOR_BUFFER_BIT); 91 | } 92 | } 93 | 94 | pub fn draw(&self, pass: RenderPass<'_, T>) 95 | where 96 | T: Target, 97 | { 98 | unsafe { 99 | pass.target.bind(self); 100 | 101 | self.state.gl.bind_vertex_array(Some(self.state.vao)); 102 | self.bind_vertex_buffer(Some(pass.mesh.raw.vertex_buffer)); 103 | self.bind_index_buffer(Some(pass.mesh.raw.index_buffer)); 104 | self.bind_shader(Some(pass.shader.raw.id)); 105 | self.bind_texture(Some(pass.texture.raw.id)); 106 | 107 | let proj = self 108 | .state 109 | .gl 110 | .get_uniform_location(pass.shader.raw.id, "u_projection") 111 | .unwrap(); 112 | 113 | let (target_width, target_height) = pass.target.size(); 114 | 115 | self.state.gl.uniform_matrix_4_f32_slice( 116 | Some(&proj), 117 | false, 118 | Mat4::orthographic_rh_gl( 119 | 0.0, 120 | target_width as f32, 121 | if T::FLIPPED { 122 | 0.0 123 | } else { 124 | target_height as f32 125 | }, 126 | if T::FLIPPED { 127 | target_height as f32 128 | } else { 129 | 0.0 130 | }, 131 | -1.0, 132 | 1.0, 133 | ) 134 | .as_ref(), 135 | ); 136 | 137 | self.state.gl.viewport(0, 0, target_width, target_height); 138 | 139 | self.state.gl.draw_elements( 140 | glow::TRIANGLES, 141 | pass.index_count as i32, 142 | glow::UNSIGNED_INT, 143 | (pass.index_start * std::mem::size_of::()) as i32, 144 | ); 145 | } 146 | } 147 | 148 | pub fn bind_vertex_buffer(&self, buffer: Option) { 149 | unsafe { 150 | if self.state.current_vertex_buffer.get() != buffer { 151 | self.state.gl.bind_buffer(glow::ARRAY_BUFFER, buffer); 152 | 153 | if buffer.is_some() { 154 | // TODO: If I ever want to use something other than `Vertex` in a buffer 155 | // I'll need to rethink this code, but it's fine for now. 156 | 157 | self.state.gl.vertex_attrib_pointer_f32( 158 | 0, 159 | 2, 160 | glow::FLOAT, 161 | false, 162 | std::mem::size_of::() as i32, 163 | 0, 164 | ); 165 | 166 | self.state.gl.vertex_attrib_pointer_f32( 167 | 1, 168 | 2, 169 | glow::FLOAT, 170 | false, 171 | std::mem::size_of::() as i32, 172 | 8, 173 | ); 174 | 175 | self.state.gl.vertex_attrib_pointer_f32( 176 | 2, 177 | 4, 178 | glow::FLOAT, 179 | false, 180 | std::mem::size_of::() as i32, 181 | 16, 182 | ); 183 | 184 | self.state.gl.enable_vertex_attrib_array(0); 185 | self.state.gl.enable_vertex_attrib_array(1); 186 | self.state.gl.enable_vertex_attrib_array(2); 187 | } else { 188 | self.state.gl.disable_vertex_attrib_array(0); 189 | self.state.gl.disable_vertex_attrib_array(1); 190 | self.state.gl.disable_vertex_attrib_array(2); 191 | } 192 | 193 | self.state.current_vertex_buffer.set(buffer); 194 | } 195 | } 196 | } 197 | 198 | pub fn bind_index_buffer(&self, buffer: Option) { 199 | unsafe { 200 | if self.state.current_index_buffer.get() != buffer { 201 | self.state 202 | .gl 203 | .bind_buffer(glow::ELEMENT_ARRAY_BUFFER, buffer); 204 | self.state.current_index_buffer.set(buffer); 205 | } 206 | } 207 | } 208 | 209 | pub fn bind_shader(&self, shader: Option) { 210 | unsafe { 211 | if self.state.current_shader.get() != shader { 212 | self.state.gl.use_program(shader); 213 | self.state.current_shader.set(shader); 214 | } 215 | } 216 | } 217 | 218 | pub fn bind_texture(&self, texture: Option) { 219 | unsafe { 220 | if self.state.current_texture.get() != texture { 221 | self.state.gl.active_texture(glow::TEXTURE0); 222 | self.state.gl.bind_texture(glow::TEXTURE_2D, texture); 223 | self.state.current_texture.set(texture); 224 | } 225 | } 226 | } 227 | 228 | pub fn bind_canvas(&self, canvas: Option) { 229 | unsafe { 230 | if self.state.current_canvas.get() != canvas { 231 | self.state.gl.bind_framebuffer(glow::FRAMEBUFFER, canvas); 232 | self.state.current_canvas.set(canvas); 233 | } 234 | } 235 | } 236 | 237 | /// Get the raw OpenGL context. 238 | /// 239 | /// # Safety 240 | /// 241 | /// You have full access to the raw OpenGL context, so you can do anything you want with it. 242 | pub unsafe fn gl(&self) -> &Arc { 243 | &self.state.gl 244 | } 245 | 246 | /// Rebind everything to the current state. 247 | /// 248 | /// You should never need to call this, unless you're manipulating `.gl()` directly 249 | pub fn rebind(&self) { 250 | unsafe { 251 | self.state.gl.bind_vertex_array(Some(self.state.vao)); 252 | } 253 | self.bind_vertex_buffer(self.state.current_vertex_buffer.take()); 254 | self.bind_index_buffer(self.state.current_index_buffer.take()); 255 | self.bind_shader(self.state.current_shader.take()); 256 | self.bind_texture(self.state.current_texture.take()); 257 | self.bind_canvas(self.state.current_canvas.take()); 258 | } 259 | } 260 | 261 | pub trait Target { 262 | const FLIPPED: bool; 263 | 264 | fn bind(&self, gfx: &Graphics); 265 | fn size(&self) -> (i32, i32); 266 | } 267 | 268 | impl Target for Window { 269 | const FLIPPED: bool = false; 270 | 271 | fn bind(&self, gfx: &Graphics) { 272 | gfx.bind_canvas(None); 273 | 274 | unsafe { 275 | gfx.state.gl.front_face(glow::CCW); 276 | } 277 | } 278 | 279 | fn size(&self) -> (i32, i32) { 280 | let (width, height) = self.size(); 281 | (width as i32, height as i32) 282 | } 283 | } 284 | 285 | impl Target for Canvas { 286 | const FLIPPED: bool = true; 287 | 288 | fn bind(&self, gfx: &Graphics) { 289 | gfx.bind_canvas(Some(self.raw.id)); 290 | 291 | unsafe { 292 | gfx.state.gl.front_face(glow::CW); 293 | } 294 | } 295 | 296 | fn size(&self) -> (i32, i32) { 297 | self.size() 298 | } 299 | } 300 | 301 | pub struct RenderPass<'a, T> { 302 | pub target: &'a T, 303 | 304 | pub mesh: &'a Mesh, 305 | pub texture: &'a Texture, 306 | pub shader: &'a Shader, 307 | 308 | pub index_start: usize, 309 | pub index_count: usize, 310 | } 311 | -------------------------------------------------------------------------------- /src/graphics/batch.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{Pod, Zeroable}; 2 | use glam::{BVec2, Mat3, Vec2}; 3 | 4 | use crate::graphics::{ 5 | Color, Graphics, Mesh, Rectangle, RenderPass, Shader, SpriteFont, Target, TextSegment, Texture, 6 | Vertex, DEFAULT_FRAGMENT_SHADER, DEFAULT_VERTEX_SHADER, 7 | }; 8 | 9 | const MAX_SPRITES: usize = 2048; 10 | const MAX_VERTICES: usize = MAX_SPRITES * 4; // Cannot be greater than 32767! 11 | const MAX_INDICES: usize = MAX_SPRITES * 6; 12 | const INDEX_ARRAY: [u32; 6] = [0, 1, 2, 2, 3, 0]; 13 | 14 | pub struct DrawParams { 15 | color: Color, 16 | origin: Vec2, 17 | scale: Vec2, 18 | rotation: f32, 19 | flip: BVec2, 20 | } 21 | 22 | impl DrawParams { 23 | pub fn new() -> DrawParams { 24 | DrawParams { 25 | color: Color::WHITE, 26 | origin: Vec2::ZERO, 27 | scale: Vec2::ONE, 28 | rotation: 0.0, 29 | flip: BVec2::FALSE, 30 | } 31 | } 32 | 33 | pub fn color(mut self, color: Color) -> Self { 34 | self.color = color; 35 | self 36 | } 37 | 38 | pub fn origin(mut self, origin: Vec2) -> Self { 39 | self.origin = origin; 40 | self 41 | } 42 | 43 | pub fn scale(mut self, scale: Vec2) -> Self { 44 | self.scale = scale; 45 | self 46 | } 47 | 48 | pub fn rotation(mut self, rotation: f32) -> Self { 49 | self.rotation = rotation; 50 | self 51 | } 52 | 53 | pub fn flip_x(mut self, flip: bool) -> Self { 54 | self.flip.x = flip; 55 | self 56 | } 57 | 58 | pub fn flip_y(mut self, flip: bool) -> Self { 59 | self.flip.y = flip; 60 | self 61 | } 62 | } 63 | 64 | #[derive(Copy, Clone, Zeroable, Pod)] 65 | #[repr(C)] 66 | struct Sprite { 67 | // The order of these fields matters, as it'll determine the 68 | // winding order of the quad. 69 | top_left: Vertex, 70 | bottom_left: Vertex, 71 | bottom_right: Vertex, 72 | top_right: Vertex, 73 | } 74 | 75 | #[derive(Clone, Default)] 76 | struct Batch { 77 | sprites: usize, 78 | texture: Option, 79 | } 80 | 81 | pub struct Batcher { 82 | mesh: Mesh, 83 | default_texture: Texture, 84 | default_shader: Shader, 85 | 86 | sprites: Vec, 87 | batches: Vec, 88 | matrices: Vec, 89 | } 90 | 91 | impl Batcher { 92 | pub fn new(gfx: &Graphics) -> Batcher { 93 | let mesh = Mesh::new(gfx, MAX_VERTICES, MAX_INDICES); 94 | 95 | let indices: Vec = INDEX_ARRAY 96 | .iter() 97 | .cycle() 98 | .take(MAX_INDICES) 99 | .enumerate() 100 | .map(|(i, vertex)| vertex + i as u32 / 6 * 4) 101 | .collect(); 102 | 103 | mesh.set_indices(&indices); 104 | 105 | let default_shader = Shader::from_str(gfx, DEFAULT_VERTEX_SHADER, DEFAULT_FRAGMENT_SHADER); 106 | let default_texture = Texture::from_data(gfx, 1, 1, &[255, 255, 255, 255]); 107 | 108 | Batcher { 109 | mesh, 110 | default_texture, 111 | default_shader, 112 | 113 | sprites: Vec::new(), 114 | batches: vec![Batch::default()], 115 | matrices: Vec::new(), 116 | } 117 | } 118 | 119 | pub fn draw(&mut self, target: &impl Target) { 120 | let mut index = 0; 121 | 122 | for mut batch in self.batches.drain(..) { 123 | let texture = batch.texture.as_ref().unwrap_or(&self.default_texture); 124 | 125 | while batch.sprites > 0 { 126 | let num_sprites = usize::min(batch.sprites, MAX_SPRITES); 127 | 128 | let sprites = &self.sprites[index..index + num_sprites]; 129 | let vertices = bytemuck::cast_slice(sprites); 130 | 131 | self.mesh.set_vertices(vertices); 132 | 133 | self.mesh.raw.gfx.draw(RenderPass { 134 | target, 135 | 136 | mesh: &self.mesh, 137 | texture, 138 | shader: &self.default_shader, 139 | 140 | index_start: 0, 141 | index_count: num_sprites * 6, 142 | }); 143 | 144 | index += num_sprites; 145 | batch.sprites -= num_sprites; 146 | } 147 | } 148 | 149 | self.sprites.clear(); 150 | self.batches.push(Batch::default()); 151 | self.matrices.clear(); 152 | } 153 | 154 | pub fn push_matrix(&mut self, matrix: Mat3) { 155 | // TODO: Support relative transforms 156 | self.matrices.push(matrix); 157 | } 158 | 159 | pub fn pop_matrix(&mut self) { 160 | self.matrices.pop(); 161 | } 162 | 163 | pub fn rect(&mut self, rect: Rectangle, params: DrawParams) { 164 | self.push_sprite(None, Rectangle::ZERO, rect, params); 165 | } 166 | 167 | pub fn texture(&mut self, texture: &Texture, position: Vec2, params: DrawParams) { 168 | self.push_sprite( 169 | Some(texture), 170 | Rectangle::new(0.0, 0.0, 1.0, 1.0), 171 | Rectangle::new( 172 | position.x, 173 | position.y, 174 | texture.width() as f32, 175 | texture.height() as f32, 176 | ), 177 | params, 178 | ); 179 | } 180 | 181 | pub fn texture_region( 182 | &mut self, 183 | texture: &Texture, 184 | position: Vec2, 185 | src: Rectangle, 186 | params: DrawParams, 187 | ) { 188 | self.push_sprite( 189 | Some(texture), 190 | Rectangle::new( 191 | src.x / texture.width() as f32, 192 | src.y / texture.height() as f32, 193 | src.width / texture.width() as f32, 194 | src.height / texture.height() as f32, 195 | ), 196 | Rectangle::new(position.x, position.y, src.width, src.height), 197 | params, 198 | ); 199 | } 200 | 201 | pub fn texture_dest( 202 | &mut self, 203 | texture: &Texture, 204 | src: Rectangle, 205 | dest: Rectangle, 206 | params: DrawParams, 207 | ) { 208 | self.push_sprite( 209 | Some(texture), 210 | Rectangle::new( 211 | src.x / texture.width() as f32, 212 | src.y / texture.height() as f32, 213 | src.width / texture.width() as f32, 214 | src.height / texture.height() as f32, 215 | ), 216 | dest, 217 | params, 218 | ); 219 | } 220 | 221 | pub fn text_segments( 222 | &mut self, 223 | font: &SpriteFont, 224 | position: Vec2, 225 | text: &[TextSegment<'_>], 226 | max_chars: Option, 227 | ) { 228 | let mut cursor = Vec2::new(0.0, font.ascent.floor()); 229 | let mut last_char = None; 230 | let mut chars = 0; 231 | 232 | for segment in text { 233 | for ch in segment.content.chars() { 234 | chars += 1; 235 | 236 | if max_chars.map(|m| chars > m) == Some(true) { 237 | return; 238 | } 239 | 240 | if ch.is_control() { 241 | if ch == '\n' { 242 | cursor.x = 0.0; 243 | cursor.y += font.line_height().floor(); 244 | } 245 | 246 | continue; 247 | } 248 | 249 | if let Some(glyph) = font.glyph(ch) { 250 | if let Some(kerning) = last_char.and_then(|l| font.kerning(l, ch)) { 251 | cursor.x += kerning; 252 | } 253 | 254 | if let Some(image) = &glyph.image { 255 | self.texture_region( 256 | font.texture(), 257 | (position + cursor + image.offset).floor(), 258 | image.uv, 259 | DrawParams::new().color(segment.color), 260 | ); 261 | } 262 | 263 | cursor.x += glyph.advance; 264 | 265 | last_char = Some(ch); 266 | } 267 | } 268 | } 269 | } 270 | 271 | pub fn text( 272 | &mut self, 273 | font: &SpriteFont, 274 | position: Vec2, 275 | text: &str, 276 | max_chars: Option, 277 | ) { 278 | self.text_segments(font, position, &[TextSegment::new(text)], max_chars) 279 | } 280 | 281 | fn push_sprite( 282 | &mut self, 283 | texture: Option<&Texture>, 284 | source: Rectangle, 285 | dest: Rectangle, 286 | params: DrawParams, 287 | ) { 288 | let fx = -params.origin.x * params.scale.x; 289 | let fy = -params.origin.y * params.scale.y; 290 | let fx2 = (dest.width - params.origin.x) * params.scale.x; 291 | let fy2 = (dest.height - params.origin.y) * params.scale.y; 292 | 293 | let sin = params.rotation.sin(); 294 | let cos = params.rotation.cos(); 295 | 296 | let mut tl = Vec2::new( 297 | dest.x + (cos * fx) - (sin * fy), 298 | dest.y + (sin * fx) + (cos * fy), 299 | ); 300 | 301 | let mut bl = Vec2::new( 302 | dest.x + (cos * fx) - (sin * fy2), 303 | dest.y + (sin * fx) + (cos * fy2), 304 | ); 305 | 306 | let mut br = Vec2::new( 307 | dest.x + (cos * fx2) - (sin * fy2), 308 | dest.y + (sin * fx2) + (cos * fy2), 309 | ); 310 | 311 | let mut tr = Vec2::new( 312 | dest.x + (cos * fx2) - (sin * fy), 313 | dest.y + (sin * fx2) + (cos * fy), 314 | ); 315 | 316 | // TODO: It may be faster to do this on the GPU, but that would cause a new batch every 317 | // time that a new matrix is pushed. Would need to benchmark. 318 | if let Some(m) = self.matrices.last() { 319 | tl = m.transform_point2(tl); 320 | bl = m.transform_point2(bl); 321 | br = m.transform_point2(br); 322 | tr = m.transform_point2(tr); 323 | } 324 | 325 | let (l_offset, r_offset) = if params.flip.x { 326 | (source.width, 0.0) 327 | } else { 328 | (0.0, source.width) 329 | }; 330 | 331 | let (t_offset, b_offset) = if params.flip.y { 332 | (source.height, 0.0) 333 | } else { 334 | (0.0, source.height) 335 | }; 336 | 337 | let tl_uv = Vec2::new(source.x + l_offset, source.y + t_offset); 338 | let bl_uv = Vec2::new(source.x + l_offset, source.y + b_offset); 339 | let br_uv = Vec2::new(source.x + r_offset, source.y + b_offset); 340 | let tr_uv = Vec2::new(source.x + r_offset, source.y + t_offset); 341 | 342 | self.sprites.push(Sprite { 343 | top_left: Vertex::new(tl, tl_uv, params.color), 344 | bottom_left: Vertex::new(bl, bl_uv, params.color), 345 | bottom_right: Vertex::new(br, br_uv, params.color), 346 | top_right: Vertex::new(tr, tr_uv, params.color), 347 | }); 348 | 349 | let batch = self.batches.last_mut().expect("should always exist"); 350 | 351 | if batch.sprites == 0 { 352 | batch.sprites = 1; 353 | batch.texture = texture.cloned(); 354 | } else if batch.texture.as_ref() != texture { 355 | let mut new_batch = batch.clone(); 356 | 357 | new_batch.sprites = 1; 358 | new_batch.texture = texture.cloned(); 359 | 360 | self.batches.push(new_batch); 361 | } else { 362 | batch.sprites += 1; 363 | } 364 | } 365 | } 366 | --------------------------------------------------------------------------------