From b422d47dc0931579710601a70975bea058e15d7a Mon Sep 17 00:00:00 2001 From: Melody Madeline Lyons Date: Sun, 16 Jun 2024 12:17:07 -0700 Subject: [PATCH] Tilemap optimizations (#132) * Use the layout from the old branch * Remove support for push constants * Use bytes_of and from_bytes * Use vec2 instead of vec3 * Move grid.wgsl * Don't use an instance buffer for the grid * Move the tilepicker mostly into graphics * Fix collision shader * Remove vertex buffer from grid * Remove tilemap vertex buffer * Remove vertex buffer from collision * Use shared placeholder png * Update alox-48 * Massively refactor things (still broken) - Splits viewport into a transform and screen size - Fundamentally changes how things are drawn - Ditches Arc everywhere, so now there are practically 0 multithreading primitives - Breaks things * Fix event position bug * Actually render the grid * Fix viewport translations * Fix autotile animation * Fix some grid issues * Reimplement event preview * Fix collision * Fix align bug * Fix event preview rendering * Fix some webgl issues --- Cargo.lock | 2 + .../graphics/data => assets}/placeholder.png | Bin crates/components/src/map_view.rs | 389 +++++++---------- crates/components/src/tilepicker.rs | 178 ++------ crates/data/src/rgss_structs.rs | 8 +- crates/graphics/Cargo.toml | 2 + crates/graphics/src/collision/collision.wgsl | 50 --- crates/graphics/src/collision/mod.rs | 214 --------- .../src/{collision/vertex.rs => data/mod.rs} | 37 +- crates/graphics/src/{ => data}/quad.rs | 15 +- crates/graphics/src/data/transform.rs | 101 +++++ crates/graphics/src/{ => data}/vertex.rs | 4 +- crates/graphics/src/data/viewport.rs | 98 +++++ crates/graphics/src/event.rs | 232 +++++----- crates/graphics/src/grid/display.rs | 129 ------ crates/graphics/src/grid/grid.wgsl | 85 ---- crates/graphics/src/grid/instance.rs | 154 ------- crates/graphics/src/grid/mod.rs | 122 ------ crates/graphics/src/lib.rs | 113 +++-- .../src/{atlas_loader.rs => loaders/atlas.rs} | 2 +- crates/graphics/src/loaders/mod.rs | 26 ++ .../{texture_loader.rs => loaders/texture.rs} | 133 ++++-- crates/graphics/src/map.rs | 406 ++++++------------ crates/graphics/src/plane.rs | 29 +- .../{ => primitives}/collision/instance.rs | 89 +--- .../graphics/src/primitives/collision/mod.rs | 226 ++++++++++ .../src/{ => primitives}/collision/shader.rs | 73 +--- .../graphics/src/primitives/grid/display.rs | 100 +++++ .../vertex.rs => primitives/grid/instance.rs} | 22 +- crates/graphics/src/primitives/grid/mod.rs | 115 +++++ .../src/{ => primitives}/grid/shader.rs | 88 ++-- crates/graphics/src/primitives/mod.rs | 89 ++++ .../src/primitives/shaders/collision.wgsl | 65 +++ .../src/primitives/shaders/gamma.wgsl | 14 + .../graphics/src/primitives/shaders/grid.wgsl | 93 ++++ .../graphics/src/primitives/shaders/hue.wgsl | 23 + .../src/primitives/shaders/sprite.wgsl | 64 +++ .../shaders}/tilemap.wgsl | 109 +++-- .../src/primitives/shaders/translation.wgsl | 20 + .../src/{ => primitives}/sprite/graphic.rs | 75 ++-- .../src/{ => primitives}/sprite/mod.rs | 99 +++-- .../graphics/src/primitives/sprite/shader.rs | 144 +++++++ .../src/{ => primitives}/sprite/vertices.rs | 2 +- .../src/{ => primitives}/tiles/atlas.rs | 69 +-- .../src/primitives/tiles/autotile_ids.rs | 70 +++ .../src/{ => primitives}/tiles/autotiles.rs | 52 +-- .../graphics/src/primitives/tiles/display.rs | 179 ++++++++ .../src/{ => primitives}/tiles/instance.rs | 45 +- crates/graphics/src/primitives/tiles/mod.rs | 187 ++++++++ .../src/{ => primitives}/tiles/shader.rs | 70 +-- crates/graphics/src/sprite/shader.rs | 159 ------- crates/graphics/src/sprite/sprite.wgsl | 115 ----- crates/graphics/src/tilepicker.rs | 185 ++++++++ crates/graphics/src/tiles/autotile_ids.rs | 369 ---------------- crates/graphics/src/tiles/mod.rs | 179 -------- crates/graphics/src/tiles/opacity.rs | 92 ---- crates/graphics/src/viewport.rs | 103 ----- crates/ui/src/tabs/map/mod.rs | 34 +- src/main.rs | 14 +- 59 files changed, 2697 insertions(+), 3264 deletions(-) rename {crates/graphics/data => assets}/placeholder.png (100%) delete mode 100644 crates/graphics/src/collision/collision.wgsl delete mode 100644 crates/graphics/src/collision/mod.rs rename crates/graphics/src/{collision/vertex.rs => data/mod.rs} (50%) rename crates/graphics/src/{ => data}/quad.rs (89%) create mode 100644 crates/graphics/src/data/transform.rs rename crates/graphics/src/{ => data}/vertex.rs (92%) create mode 100644 crates/graphics/src/data/viewport.rs delete mode 100644 crates/graphics/src/grid/display.rs delete mode 100644 crates/graphics/src/grid/grid.wgsl delete mode 100644 crates/graphics/src/grid/instance.rs delete mode 100644 crates/graphics/src/grid/mod.rs rename crates/graphics/src/{atlas_loader.rs => loaders/atlas.rs} (97%) create mode 100644 crates/graphics/src/loaders/mod.rs rename crates/graphics/src/{texture_loader.rs => loaders/texture.rs} (58%) rename crates/graphics/src/{ => primitives}/collision/instance.rs (57%) create mode 100644 crates/graphics/src/primitives/collision/mod.rs rename crates/graphics/src/{ => primitives}/collision/shader.rs (54%) create mode 100644 crates/graphics/src/primitives/grid/display.rs rename crates/graphics/src/{grid/vertex.rs => primitives/grid/instance.rs} (58%) create mode 100644 crates/graphics/src/primitives/grid/mod.rs rename crates/graphics/src/{ => primitives}/grid/shader.rs (51%) create mode 100644 crates/graphics/src/primitives/mod.rs create mode 100644 crates/graphics/src/primitives/shaders/collision.wgsl create mode 100644 crates/graphics/src/primitives/shaders/gamma.wgsl create mode 100644 crates/graphics/src/primitives/shaders/grid.wgsl create mode 100644 crates/graphics/src/primitives/shaders/hue.wgsl create mode 100644 crates/graphics/src/primitives/shaders/sprite.wgsl rename crates/graphics/src/{tiles => primitives/shaders}/tilemap.wgsl (59%) create mode 100644 crates/graphics/src/primitives/shaders/translation.wgsl rename crates/graphics/src/{ => primitives}/sprite/graphic.rs (56%) rename crates/graphics/src/{ => primitives}/sprite/mod.rs (60%) create mode 100644 crates/graphics/src/primitives/sprite/shader.rs rename crates/graphics/src/{ => primitives}/sprite/vertices.rs (98%) rename crates/graphics/src/{ => primitives}/tiles/atlas.rs (84%) create mode 100644 crates/graphics/src/primitives/tiles/autotile_ids.rs rename crates/graphics/src/{ => primitives}/tiles/autotiles.rs (59%) create mode 100644 crates/graphics/src/primitives/tiles/display.rs rename crates/graphics/src/{ => primitives}/tiles/instance.rs (69%) create mode 100644 crates/graphics/src/primitives/tiles/mod.rs rename crates/graphics/src/{ => primitives}/tiles/shader.rs (71%) delete mode 100644 crates/graphics/src/sprite/shader.rs delete mode 100644 crates/graphics/src/sprite/sprite.wgsl create mode 100644 crates/graphics/src/tilepicker.rs delete mode 100644 crates/graphics/src/tiles/autotile_ids.rs delete mode 100644 crates/graphics/src/tiles/mod.rs delete mode 100644 crates/graphics/src/tiles/opacity.rs delete mode 100644 crates/graphics/src/viewport.rs diff --git a/Cargo.lock b/Cargo.lock index c6939aa1..ea500a3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3186,8 +3186,10 @@ dependencies = [ "luminol-data", "luminol-egui-wgpu", "luminol-filesystem", + "luminol-macros", "naga", "naga_oil", + "parking_lot", "wgpu", ] diff --git a/crates/graphics/data/placeholder.png b/assets/placeholder.png similarity index 100% rename from crates/graphics/data/placeholder.png rename to assets/placeholder.png diff --git a/crates/components/src/map_view.rs b/crates/components/src/map_view.rs index e237efd7..2b6596e2 100644 --- a/crates/components/src/map_view.rs +++ b/crates/components/src/map_view.rs @@ -17,6 +17,8 @@ use color_eyre::eyre::{ContextCompat, WrapErr}; use itertools::Itertools; +use luminol_graphics::{Drawable, Renderable}; +use std::collections::HashMap; use std::io::Write; pub struct MapView { @@ -30,13 +32,13 @@ pub struct MapView { /// The first sprite is for drawing on the tilemap, /// and the second sprite is for the hover preview. - pub events: luminol_data::OptionVec<(luminol_graphics::Event, luminol_graphics::Event)>, + preview_events: HashMap, + last_events: HashMap, pub map: luminol_graphics::Map, pub selected_layer: SelectedLayer, pub selected_event_id: Option, pub cursor_pos: egui::Pos2, - pub event_enabled: bool, pub snap_to_grid: bool, /// The map coordinates of the tile being hovered over @@ -62,6 +64,11 @@ pub struct MapView { pub data_id: egui::Id, } +struct PreviewEvent { + viewport: luminol_graphics::Viewport, + sprite: luminol_graphics::Event, +} + #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] pub enum SelectedLayer { #[default] @@ -81,7 +88,7 @@ impl MapView { let tileset = &tilesets.data[map.tileset_id]; let mut passages = luminol_data::Table2::new(map.data.xsize(), map.data.ysize()); - luminol_graphics::collision::calculate_passages( + luminol_graphics::Collision::calculate_passages( &tileset.passages, &tileset.priorities, &map.data, @@ -90,36 +97,6 @@ impl MapView { |x, y, passage| passages[(x, y)] = passage, ); - let atlas = update_state.graphics.atlas_loader.load_atlas( - &update_state.graphics, - update_state.filesystem, - tileset, - )?; - let events = map - .events - .iter() - .map(|(id, e)| -> color_eyre::Result<_> { - let sprite = luminol_graphics::Event::new( - &update_state.graphics, - update_state.filesystem, - e, - &atlas, - )?; - let preview_sprite = luminol_graphics::Event::new( - &update_state.graphics, - update_state.filesystem, - e, - &atlas, - )?; - - Ok(if let Some(sprite) = sprite { - preview_sprite.map(|preview_sprite| (id, (sprite, preview_sprite))) - } else { - None - }) - }) - .flatten_ok() - .try_collect()?; let map = luminol_graphics::Map::new( &update_state.graphics, update_state.filesystem, @@ -151,13 +128,13 @@ impl MapView { pan, inter_tile_pan, - events, + preview_events: HashMap::new(), + last_events: HashMap::new(), map, selected_layer: SelectedLayer::default(), selected_event_id: None, cursor_pos, - event_enabled: true, snap_to_grid: false, darken_unselected_layers: true, @@ -182,7 +159,7 @@ impl MapView { pub fn ui( &mut self, ui: &mut egui::Ui, - graphics_state: &std::sync::Arc, + update_state: &luminol_core::UpdateState<'_>, map: &luminol_data::rpg::Map, tilepicker: &crate::Tilepicker, dragging_event: bool, @@ -237,6 +214,14 @@ impl MapView { self.previous_scale = self.scale; let grid_inner_thickness = if self.scale >= 50. { 1. } else { 0. }; + self.map + .grid + .display + .set_inner_thickness(&update_state.graphics.render_state, grid_inner_thickness); + self.map.grid.display.set_pixels_per_point( + &update_state.graphics.render_state, + ui.ctx().pixels_per_point(), + ); let ctrl_drag = ui.input(|i| { if is_focused { @@ -316,36 +301,37 @@ impl MapView { max: canvas_pos + pos, }; + self.map.tiles.selected_layer = match self.selected_layer { + SelectedLayer::Events => None, + SelectedLayer::Tiles(selected_layer) if self.darken_unselected_layers => { + Some(selected_layer) + } + SelectedLayer::Tiles(_) => None, + }; + + // no idea why this math works (could probably be simplified) let proj_center_x = width2 * 32. - (self.pan.x + clip_offset.x) / scale; let proj_center_y = height2 * 32. - (self.pan.y + clip_offset.y) / scale; let proj_width2 = canvas_rect.width() / scale / 2.; let proj_height2 = canvas_rect.height() / scale / 2.; + self.map.viewport.set( + &update_state.graphics.render_state, + glam::vec2(canvas_rect.width(), canvas_rect.height()), + glam::vec2(proj_width2 - proj_center_x, proj_height2 - proj_center_y) * scale, + glam::Vec2::splat(scale), + ); - let graphics_state = graphics_state.clone(); + self.map + .update_animation(&update_state.graphics.render_state, ui.input(|i| i.time)); + ui.ctx() + .request_repaint_after(std::time::Duration::from_secs_f32(16. / 60.)); - self.map.set_proj( - &graphics_state.render_state, - glam::Mat4::orthographic_rh( - proj_center_x - proj_width2, - proj_center_x + proj_width2, - proj_center_y + proj_height2, - proj_center_y - proj_height2, - -1., - 1., - ), - ); - self.map.paint( - graphics_state.clone(), - ui.painter(), - match self.selected_layer { - SelectedLayer::Events => None, - SelectedLayer::Tiles(selected_layer) if self.darken_unselected_layers => { - Some(selected_layer) - } - SelectedLayer::Tiles(_) => None, - }, - canvas_rect, - ); + let painter = luminol_graphics::Painter::new(self.map.prepare(&update_state.graphics)); + ui.painter() + .add(luminol_egui_wgpu::Callback::new_paint_callback( + canvas_rect, + painter, + )); ui.painter().rect_stroke( map_rect, @@ -375,26 +361,30 @@ impl MapView { ) .intersect(map_rect); - if !self.event_enabled || !matches!(self.selected_layer, SelectedLayer::Events) { + if !self.map.event_enabled || !matches!(self.selected_layer, SelectedLayer::Events) { self.selected_event_id = None; } self.selected_event_is_hovered = false; - if self.event_enabled { + if self.map.event_enabled { let mut selected_event = None; let mut selected_event_rect = None; for (_, event) in map.events.iter() { - let sprites = self.events.get(event.id); - let event_size = sprites - .map(|e| e.0.sprite_size) + let sprite = self.map.events.get_mut(event.id); + let has_sprite = sprite.is_some(); + let event_size = sprite + .as_ref() + .map(|e| e.sprite_size) .unwrap_or(egui::vec2(32., 32.)); let scaled_event_size = event_size * scale; - // Darken the graphic if required - if let Some((sprite, _)) = sprites { - sprite.sprite().graphic.set_opacity_multiplier( - &graphics_state.render_state, + // update relevant properties + if let Some(sprite) = sprite { + // FIXME only update if necessary + sprite.set_position(&update_state.graphics.render_state, event.x, event.y); + sprite.sprite.graphic.set_opacity_multiplier( + &update_state.graphics.render_state, if self.darken_unselected_layers && !matches!(self.selected_layer, SelectedLayer::Events) { @@ -414,25 +404,6 @@ impl MapView { scaled_event_size, ); - if let Some((sprite, _)) = sprites { - if canvas_rect.intersects(box_rect) { - let x = event.x as f32 * 32. + (32. - event_size.x) / 2.; - let y = event.y as f32 * 32. + (32. - event_size.y); - sprite.set_proj( - &graphics_state.render_state, - glam::Mat4::orthographic_rh( - proj_center_x - proj_width2 - x, - proj_center_x + proj_width2 - x, - proj_center_y + proj_height2 - y, - proj_center_y - proj_height2 - y, - -1., - 1., - ), - ); - sprite.paint(graphics_state.clone(), ui.painter(), canvas_rect); - } - } - if matches!(self.selected_layer, SelectedLayer::Events) && ui.input(|i| !i.modifiers.shift) { @@ -473,39 +444,52 @@ impl MapView { response = response.on_hover_ui_at_pointer(|ui| { ui.label(format!("Event {:0>3}: {:?}", event.id, event.name)); - let (response, painter) = ui.allocate_painter( + let (response, _painter) = ui.allocate_painter( event_size * ui.ctx().pixels_per_point(), egui::Sense::click(), ); - if let Some((_, preview_sprite)) = sprites { + + if has_sprite { + let mut preview = + self.last_events.remove(&event.id).unwrap_or_else(|| { + let viewport = luminol_graphics::Viewport::new( + &update_state.graphics, + glam::vec2(event_size.x, event_size.y), + ); + let sprite = luminol_graphics::Event::new_standalone( + &update_state.graphics, + update_state.filesystem, + &viewport, + event, + &self.map.atlas, + ) + .unwrap() + .unwrap(); // FIXME: handle error + PreviewEvent { viewport, sprite } + }); + if response.rect.is_positive() { let clipped_rect = ui.ctx().screen_rect().intersect(response.rect); - let proj_rect = egui::Rect::from_min_size( - (ui.ctx().screen_rect().min - response.rect.min) - .max(Default::default()) - .to_pos2(), - preview_sprite.sprite_size * clipped_rect.size() - / response.rect.size(), + preview.viewport.set_size( + &update_state.graphics.render_state, + glam::vec2(clipped_rect.width(), clipped_rect.height()), ); - preview_sprite.set_proj( - &graphics_state.render_state, - glam::Mat4::orthographic_rh( - proj_rect.left(), - proj_rect.right(), - proj_rect.bottom(), - proj_rect.top(), - -1., - 1., - ), + + let painter = luminol_graphics::Painter::new( + preview.sprite.prepare(&update_state.graphics), ); - preview_sprite.paint( - graphics_state.clone(), - &painter, - clipped_rect, + ui.painter().add( + luminol_egui_wgpu::Callback::new_paint_callback( + clipped_rect, + painter, + ), ); + + self.preview_events.insert(event.id, preview); } } + match self.selected_event_id { Some(id) if id == event.id => ui.painter().rect_stroke( response.rect, @@ -578,15 +562,10 @@ impl MapView { } } - self.selected_event_id = selected_event.map(|e| e.id); + self.last_events.clear(); + std::mem::swap(&mut self.preview_events, &mut self.last_events); // swap and clear preview events, so we only keep the ones used this frame - // Draw the fog and collision layers - self.map.paint_overlay( - graphics_state.clone(), - ui.painter(), - grid_inner_thickness, - canvas_rect, - ); + self.selected_event_id = selected_event.map(|e| e.id); // Draw white rectangles on the border of all events while let Some(rect) = self.event_rects.pop() { @@ -608,14 +587,6 @@ impl MapView { } } } - } else { - // Draw the fog and collision layers - self.map.paint_overlay( - graphics_state.clone(), - ui.painter(), - grid_inner_thickness, - canvas_rect, - ); } // FIXME: If we want to be fast, we should be rendering all the tile ids to a texture once and then just rendering that texture here @@ -705,7 +676,7 @@ impl MapView { /// This function returns a future that you need to `.await` to finish saving the image, but /// the future doesn't borrow anything so you don't need to worry about lifetime-related issues. pub fn save_as_image( - &self, + &mut self, graphics_state: &std::sync::Arc, map: &luminol_data::rpg::Map, ) -> impl std::future::Future> { @@ -732,6 +703,11 @@ impl MapView { .min(max_texture_dimension_2d) .min(max_buffer_size / (max_texture_width * 4)); + let mut command_encoder = graphics_state + .render_state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + let buffers = (0..screenshot_height) .step_by(max_texture_height as usize) .cartesian_product((0..screenshot_width).step_by(max_texture_width as usize)) @@ -739,10 +715,6 @@ impl MapView { let width = max_texture_width.min(screenshot_width - x_offset); let height = max_texture_height.min(screenshot_height - y_offset); let width_padded = width.next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT / 4); - let viewport_rect = egui::Rect::from_min_size( - egui::pos2(x_offset as f32, y_offset as f32), - egui::vec2(width as f32, height as f32), - ); let texture = graphics_state @@ -775,117 +747,55 @@ impl MapView { mapped_at_creation: false, }); - self.map.set_proj( + self.map.viewport.set( &graphics_state.render_state, - glam::Mat4::orthographic_rh( - x_offset as f32, - (x_offset + width) as f32, - (y_offset + height) as f32, - y_offset as f32, - -1., - 1., - ), + glam::vec2(width as f32, height as f32), + glam::vec2(x_offset as f32, y_offset as f32), + glam::Vec2::ONE, ); - let mut command_encoder = graphics_state - .render_state - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); - { - let map_callback = self.map.callback( - graphics_state.clone(), - match self.selected_layer { - SelectedLayer::Events => None, - SelectedLayer::Tiles(selected_layer) - if self.darken_unselected_layers => + self.map.tiles.selected_layer = match self.selected_layer { + SelectedLayer::Events => None, + SelectedLayer::Tiles(selected_layer) if self.darken_unselected_layers => { + Some(selected_layer) + } + SelectedLayer::Tiles(_) => None, + }; + + for (_, event) in map.events.iter() { + if let Some(sprite) = self.map.events.get_mut(event.id) { + sprite.sprite.graphic.set_opacity_multiplier( + &graphics_state.render_state, + if self.darken_unselected_layers + && !matches!(self.selected_layer, SelectedLayer::Events) { - Some(selected_layer) - } - SelectedLayer::Tiles(_) => None, - }, - ); - let map_overlay_callback = - self.map.overlay_callback(graphics_state.clone(), 1.); - - let event_callbacks = map - .events - .iter() - .filter_map(|(_, event)| { - let sprites = self.events.get(event.id); - let tile_size = 32.; - let event_size = sprites - .map(|e| e.0.sprite_size) - .unwrap_or(egui::vec2(32., 32.)); - - if let Some((sprite, _)) = sprites { - sprite.sprite().graphic.set_opacity_multiplier( - &graphics_state.render_state, - if self.darken_unselected_layers - && !matches!(self.selected_layer, SelectedLayer::Events) - { - 0.5 - } else { - 1. - }, - ); - } + 0.5 + } else { + 1. + }, + ); + } + } - let rect = egui::Rect::from_min_size( - egui::pos2( - (event.x as f32 * tile_size) + (tile_size - event_size.x) / 2., - (event.y as f32 * tile_size) + (tile_size - event_size.y), - ), - event_size, - ); + // we probably don't need to prepare the map every time, but it's not that expensive + let prepared = self.map.prepare(graphics_state); + + let mut render_pass = + command_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("map editor screenshot render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); - sprites.and_then(|(sprite, _)| { - viewport_rect.intersects(rect).then(|| { - let x = event.x as f32 * 32. + (32. - event_size.x) / 2.; - let y = event.y as f32 * 32. + (32. - event_size.y); - sprite.set_proj( - &graphics_state.render_state, - glam::Mat4::orthographic_rh( - x_offset as f32 - x, - (x_offset + width) as f32 - x, - (y_offset + height) as f32 - y, - y_offset as f32 - y, - -1., - 1., - ), - ); - sprite.callback(graphics_state.clone()) - }) - }) - }) - .collect_vec(); - - let mut render_pass = - command_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("map editor screenshot render pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations::default(), - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); + prepared.draw(&mut render_pass); - map_callback.paint(&mut render_pass); - for event_callback in event_callbacks.iter() { - event_callback.paint(&mut render_pass); - } - map_overlay_callback.paint( - egui::PaintCallbackInfo { - viewport: viewport_rect, - clip_rect: viewport_rect, - pixels_per_point: 1., - screen_size_px: [screenshot_width, screenshot_height], - }, - &mut render_pass, - ); - } + drop(render_pass); command_encoder.copy_texture_to_buffer( wgpu::ImageCopyTexture { @@ -908,15 +818,16 @@ impl MapView { depth_or_array_layers: 1, }, ); - graphics_state - .render_state - .queue - .submit(Some(command_encoder.finish())); buffer }) .collect_vec(); + graphics_state + .render_state + .queue + .submit(std::iter::once(command_encoder.finish())); + let graphics_state = graphics_state.clone(); let mut vec = vec![0; screenshot_width as usize * screenshot_height as usize * 4]; async move { diff --git a/crates/components/src/tilepicker.rs b/crates/components/src/tilepicker.rs index 4e63f9a6..81775c43 100644 --- a/crates/components/src/tilepicker.rs +++ b/crates/components/src/tilepicker.rs @@ -15,10 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use fragile::Fragile; -use itertools::Itertools; -use std::sync::Arc; -use std::time::Duration; +use luminol_graphics::Renderable; pub struct Tilepicker { pub selected_tiles_left: i16, @@ -26,60 +23,16 @@ pub struct Tilepicker { pub selected_tiles_right: i16, pub selected_tiles_bottom: i16, - pub coll_enabled: bool, - pub grid_enabled: bool, + pub view: luminol_graphics::Tilepicker, drag_origin: Option, - resources: Arc, - viewport: Arc, - ani_time: Option, - /// When true, brush tile ID randomization is enabled. pub brush_random: bool, /// Seed for the PRNG used for the brush when brush tile ID randomization is enabled. brush_seed: [u8; 16], } -struct Resources { - tiles: luminol_graphics::tiles::Tiles, - collision: luminol_graphics::collision::Collision, - grid: luminol_graphics::grid::Grid, -} - -// wgpu types are not Send + Sync on webassembly, so we use fragile to make sure we never access any wgpu resources across thread boundaries -struct Callback { - resources: Fragile>, - graphics_state: Fragile>, - - coll_enabled: bool, - grid_enabled: bool, -} - -impl luminol_egui_wgpu::CallbackTrait for Callback { - fn paint<'a>( - &'a self, - info: egui::PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'a>, - _callback_resources: &'a luminol_egui_wgpu::CallbackResources, - ) { - let resources = self.resources.get(); - let graphics_state = self.graphics_state.get(); - - resources - .tiles - .draw(graphics_state, &[true], None, render_pass); - - if self.coll_enabled { - resources.collision.draw(graphics_state, render_pass); - } - - if self.grid_enabled { - resources.grid.draw(graphics_state, &info, render_pass); - } - } -} - #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum SelectedTile { Autotile(i16), @@ -120,65 +73,12 @@ impl Tilepicker { let tilesets = update_state.data.tilesets(); let tileset = &tilesets.data[map.tileset_id]; - let atlas = update_state.graphics.atlas_loader.load_atlas( + let view = luminol_graphics::Tilepicker::new( &update_state.graphics, - update_state.filesystem, tileset, + update_state.filesystem, )?; - let tilepicker_data = (47..(384 + 47)) - .step_by(48) - .chain(384..(atlas.tileset_height as i16 / 32 * 8 + 384)) - .collect_vec(); - let tilepicker_data = luminol_data::Table3::new_data( - 8, - 1 + (atlas.tileset_height / 32) as usize, - 1, - tilepicker_data, - ); - - let viewport = Arc::new(luminol_graphics::viewport::Viewport::new( - &update_state.graphics, - 256., - atlas.tileset_height as f32 + 32., - )); - - let tiles = luminol_graphics::tiles::Tiles::new( - &update_state.graphics, - viewport.clone(), - atlas, - &tilepicker_data, - ); - - let grid = luminol_graphics::grid::Grid::new( - &update_state.graphics, - viewport.clone(), - tilepicker_data.xsize(), - tilepicker_data.ysize(), - ); - - let mut passages = - luminol_data::Table2::new(tilepicker_data.xsize(), tilepicker_data.ysize()); - for x in 0..8 { - passages[(x, 0)] = { - let tile_id = tilepicker_data[(x, 0, 0)].try_into().unwrap_or_default(); - if tile_id >= tileset.passages.len() { - 0 - } else { - tileset.passages[tile_id] - } - }; - } - let length = - (passages.len().saturating_sub(8)).min(tileset.passages.len().saturating_sub(384)); - passages.as_mut_slice()[8..8 + length] - .copy_from_slice(&tileset.passages.as_slice()[384..384 + length]); - let collision = luminol_graphics::collision::Collision::new( - &update_state.graphics, - viewport.clone(), - &passages, - ); - let mut brush_seed = [0u8; 16]; brush_seed[0..8].copy_from_slice( &update_state @@ -192,19 +92,13 @@ impl Tilepicker { brush_seed[8..16].copy_from_slice(&(map_id as u64).to_le_bytes()); Ok(Self { - resources: Arc::new(Resources { - tiles, - collision, - grid, - }), - viewport, - ani_time: None, + view, + selected_tiles_left: 0, selected_tiles_top: 0, selected_tiles_right: 0, selected_tiles_bottom: 0, - coll_enabled: false, - grid_enabled: true, + drag_origin: None, brush_seed, brush_random: false, @@ -258,26 +152,8 @@ impl Tilepicker { ) -> egui::Response { self.brush_random = update_state.toolbar.brush_random != ui.input(|i| i.modifiers.alt); - let time = ui.ctx().input(|i| i.time); - let graphics_state = update_state.graphics.clone(); - - if let Some(ani_time) = self.ani_time { - if time - ani_time >= 16. / 60. { - self.ani_time = Some(time); - self.resources - .tiles - .autotiles - .inc_ani_index(&graphics_state.render_state); - } - } else { - self.ani_time = Some(time); - } - - ui.ctx() - .request_repaint_after(Duration::from_secs_f64(16. / 60.)); - let (canvas_rect, response) = ui.allocate_exact_size( - egui::vec2(256., self.resources.tiles.atlas.tileset_height as f32 + 32.), + egui::vec2(256., self.view.atlas.tileset_height as f32 + 32.), egui::Sense::click_and_drag(), ); @@ -287,27 +163,29 @@ impl Tilepicker { .intersect(scroll_rect.translate(canvas_rect.min.to_vec2())); let scroll_rect = absolute_scroll_rect.translate(-canvas_rect.min.to_vec2()); - self.viewport.set_proj( - &graphics_state.render_state, - glam::Mat4::orthographic_rh( - scroll_rect.left(), - scroll_rect.right(), - scroll_rect.bottom(), - scroll_rect.top(), - -1., - 1., - ), + self.view.grid.display.set_pixels_per_point( + &update_state.graphics.render_state, + ui.ctx().pixels_per_point(), + ); + + self.view.set_position( + &update_state.graphics.render_state, + glam::vec2(0.0, -scroll_rect.top()), ); - // FIXME: move this into graphics + self.view.viewport.set( + &update_state.graphics.render_state, + glam::vec2(scroll_rect.width(), scroll_rect.height()), + glam::Vec2::ZERO, + glam::Vec2::ONE, + ); + self.view + .update_animation(&update_state.graphics.render_state, ui.input(|i| i.time)); + + let painter = luminol_graphics::Painter::new(self.view.prepare(&update_state.graphics)); ui.painter() .add(luminol_egui_wgpu::Callback::new_paint_callback( absolute_scroll_rect, - Callback { - resources: Fragile::new(self.resources.clone()), - graphics_state: Fragile::new(graphics_state.clone()), - coll_enabled: self.coll_enabled, - grid_enabled: self.grid_enabled, - }, + painter, )); let rect = egui::Rect::from_x_y_ranges( @@ -333,7 +211,7 @@ impl Tilepicker { pos }; let rect = egui::Rect::from_two_pos(drag_origin, pos); - let bottom = self.resources.tiles.atlas.tileset_height as i16 / 32; + let bottom = self.view.atlas.tileset_height as i16 / 32; self.selected_tiles_left = (rect.left() as i16).clamp(0, 7); self.selected_tiles_right = (rect.right() as i16).clamp(0, 7); self.selected_tiles_top = (rect.top() as i16).clamp(0, bottom); diff --git a/crates/data/src/rgss_structs.rs b/crates/data/src/rgss_structs.rs index ba7607f1..c07b97c6 100644 --- a/crates/data/src/rgss_structs.rs +++ b/crates/data/src/rgss_structs.rs @@ -18,7 +18,7 @@ pub struct Color { impl From for Color { fn from(value: alox_48::Userdata) -> Self { - bytemuck::cast_slice(&value.data)[0] + *bytemuck::from_bytes(&value.data) } } @@ -26,7 +26,7 @@ impl From for alox_48::Userdata { fn from(value: Color) -> Self { alox_48::Userdata { class: "Color".into(), - data: bytemuck::cast_slice(&[value]).to_vec(), + data: bytemuck::bytes_of(&value).to_vec(), } } } @@ -67,7 +67,7 @@ pub struct Tone { impl From for Tone { fn from(value: alox_48::Userdata) -> Self { - bytemuck::cast_slice(&value.data)[0] + *bytemuck::from_bytes(&value.data) } } @@ -75,7 +75,7 @@ impl From for alox_48::Userdata { fn from(value: Tone) -> Self { alox_48::Userdata { class: "Tone".into(), - data: bytemuck::cast_slice(&[value]).to_vec(), + data: bytemuck::bytes_of(&value).to_vec(), } } } diff --git a/crates/graphics/Cargo.toml b/crates/graphics/Cargo.toml index 9efca02d..b3385645 100644 --- a/crates/graphics/Cargo.toml +++ b/crates/graphics/Cargo.toml @@ -29,6 +29,7 @@ naga = "0.19.0" crossbeam.workspace = true dashmap.workspace = true +parking_lot.workspace = true color-eyre.workspace = true @@ -40,5 +41,6 @@ camino.workspace = true luminol-data.workspace = true luminol-filesystem.workspace = true +luminol-macros.workspace = true fragile.workspace = true diff --git a/crates/graphics/src/collision/collision.wgsl b/crates/graphics/src/collision/collision.wgsl deleted file mode 100644 index 5e46e76b..00000000 --- a/crates/graphics/src/collision/collision.wgsl +++ /dev/null @@ -1,50 +0,0 @@ -struct VertexInput { - @location(0) position: vec3, - @location(1) direction: u32, -} - -struct InstanceInput { - @location(2) tile_position: vec3, - @location(3) passage: u32, -} - -struct VertexOutput { - @builtin(position) clip_position: vec4, -} - -struct Viewport { - proj: mat4x4, -} - -#if USE_PUSH_CONSTANTS == true -struct PushConstants { - viewport: Viewport, -} -var push_constants: PushConstants; -#else -@group(0) @binding(0) -var viewport: Viewport; -#endif - -@vertex -fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput { - var out: VertexOutput; - -#if USE_PUSH_CONSTANTS == true - let viewport = push_constants.viewport; -#endif - - if (instance.passage & vertex.direction) == 0u { - return out; - } - - let position = viewport.proj * vec4(vertex.position.xy + (instance.tile_position.xy * 32.), 0.0, 1.0); - out.clip_position = vec4(position.xy, instance.tile_position.z, 1.0); - - return out; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - return vec4(1., 0., 0., 0.4); -} diff --git a/crates/graphics/src/collision/mod.rs b/crates/graphics/src/collision/mod.rs deleted file mode 100644 index 7134b24d..00000000 --- a/crates/graphics/src/collision/mod.rs +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -use std::sync::Arc; - -use crate::{ - viewport::{self, Viewport}, - BindGroupBuilder, BindGroupLayoutBuilder, GraphicsState, -}; - -use instance::Instances; -use itertools::Itertools; -use vertex::Vertex; - -mod instance; -pub(crate) mod shader; -mod vertex; - -#[derive(Debug)] -pub struct Collision { - pub instances: Instances, - pub viewport: Arc, - pub bind_group: Option, -} - -#[derive(Debug, Clone)] -pub enum CollisionType { - /// An event - Event, - /// A tile whose ID is less than 48 (i.e. a blank autotile) - BlankTile, - /// A tile whose ID is greater than or equal to 48 - Tile, -} - -/// Determines the passage values for every position on the map, running `f(x, y, passage)` for -/// every position. -/// -/// `layers` should be an iterator over the enabled layer numbers of the map from top to bottom. -pub fn calculate_passages( - passages: &luminol_data::Table1, - priorities: &luminol_data::Table1, - tiles: &luminol_data::Table3, - events: Option<&luminol_data::OptionVec>, - layers: impl Iterator + Clone, - mut f: impl FnMut(usize, usize, i16), -) { - let tileset_size = passages.len().min(priorities.len()); - - let mut event_map = if let Some(events) = events { - events - .iter() - .filter_map(|(_, event)| { - let page = event.pages.first()?; - if page.through { - return None; - } - let tile_event = page - .graphic - .tile_id - .map_or((15, 1, CollisionType::Event), |id| { - let tile_id = id + 1; - if tile_id >= tileset_size { - (0, 0, CollisionType::Event) - } else { - (passages[tile_id], priorities[tile_id], CollisionType::Event) - } - }); - Some(((event.x as usize, event.y as usize), tile_event)) - }) - .collect() - } else { - std::collections::HashMap::new() - }; - - for (y, x) in (0..tiles.ysize()).cartesian_product(0..tiles.xsize()) { - let tile_event = event_map.remove(&(x, y)); - - f( - x, - y, - calculate_passage(tile_event.into_iter().chain(layers.clone().map(|z| { - let tile_id = tiles[(x, y, z)].try_into().unwrap_or_default(); - let collision_type = if tile_id < 48 { - CollisionType::BlankTile - } else { - CollisionType::Tile - }; - if tile_id >= tileset_size { - (0, 0, collision_type) - } else { - (passages[tile_id], priorities[tile_id], collision_type) - } - }))), - ); - } -} - -/// Determines the passage value for a position on the map given an iterator over the -/// `(passage, priority, collision_type)` values for the tiles in each layer on that position. -/// The iterator should iterate over the layers from top to bottom. -pub fn calculate_passage(layers: impl Iterator + Clone) -> i16 { - let mut computed_passage = 0; - - for direction in [1, 2, 4, 8] { - let mut at_least_one_layer_not_blank = false; - let mut layers = layers.clone().peekable(); - while let Some((passage, priority, collision_type)) = layers.next() { - if matches!( - collision_type, - CollisionType::Tile | CollisionType::BlankTile - ) { - if matches!(collision_type, CollisionType::BlankTile) - && (at_least_one_layer_not_blank || layers.peek().is_some()) - { - continue; - } else { - at_least_one_layer_not_blank = true; - } - } - if passage & direction != 0 { - computed_passage |= direction; - break; - } else if priority == 0 { - break; - } - } - } - - computed_passage -} - -impl Collision { - pub fn new( - graphics_state: &GraphicsState, - viewport: Arc, - passages: &luminol_data::Table2, - ) -> Self { - let instances = Instances::new(&graphics_state.render_state, passages); - - let bind_group = (!graphics_state.push_constants_supported()).then(|| { - let mut bind_group_builder = BindGroupBuilder::new(); - bind_group_builder.append_buffer(viewport.as_buffer().unwrap()); - bind_group_builder.build( - &graphics_state.render_state.device, - Some("collision bind group"), - &graphics_state.bind_group_layouts.collision, - ) - }); - - Self { - instances, - viewport, - bind_group, - } - } - - pub fn set_passage( - &self, - render_state: &luminol_egui_wgpu::RenderState, - passage: i16, - position: (usize, usize), - ) { - self.instances.set_passage(render_state, passage, position) - } - - pub fn draw<'rpass>( - &'rpass self, - graphics_state: &'rpass GraphicsState, - render_pass: &mut wgpu::RenderPass<'rpass>, - ) { - render_pass.push_debug_group("tilemap collision renderer"); - render_pass.set_pipeline(&graphics_state.pipelines.collision); - - if let Some(bind_group) = &self.bind_group { - render_pass.set_bind_group(0, bind_group, &[]) - } else { - render_pass.set_push_constants( - wgpu::ShaderStages::VERTEX, - 0, - &self.viewport.as_bytes(), - ); - } - - self.instances.draw(render_pass); - render_pass.pop_debug_group(); - } -} - -pub fn create_bind_group_layout( - render_state: &luminol_egui_wgpu::RenderState, -) -> wgpu::BindGroupLayout { - let mut builder = BindGroupLayoutBuilder::new(); - - if !crate::push_constants_supported(render_state) { - viewport::add_to_bind_group_layout(&mut builder); - } - - builder.build(&render_state.device, Some("collision bind group layout")) -} diff --git a/crates/graphics/src/collision/vertex.rs b/crates/graphics/src/data/mod.rs similarity index 50% rename from crates/graphics/src/collision/vertex.rs rename to crates/graphics/src/data/mod.rs index f40bd2e0..c545c84f 100644 --- a/crates/graphics/src/collision/vertex.rs +++ b/crates/graphics/src/data/mod.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Lily Lyons +// Copyright (C) 2024 Lily Lyons // // This file is part of Luminol. // @@ -14,23 +14,22 @@ // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +mod quad; +pub use quad::Quad; + +mod vertex; +pub use vertex::Vertex; -#[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, PartialEq)] -pub struct Vertex { - pub position: glam::Vec3, - /// 1: down, 2: left, 4: right, 8: up - pub direction: u32, -} +mod viewport; +pub use viewport::Viewport; -impl Vertex { - const ATTRIBS: [wgpu::VertexAttribute; 2] = - wgpu::vertex_attr_array![0 => Float32x3, 1 => Uint32]; - pub const fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &Self::ATTRIBS, - } - } -} +mod transform; +pub use transform::Transform; diff --git a/crates/graphics/src/quad.rs b/crates/graphics/src/data/quad.rs similarity index 89% rename from crates/graphics/src/quad.rs rename to crates/graphics/src/data/quad.rs index 79b9e3dc..61334bd0 100644 --- a/crates/graphics/src/quad.rs +++ b/crates/graphics/src/data/quad.rs @@ -21,12 +21,11 @@ use wgpu::util::DeviceExt; pub struct Quad { pub pos: egui::Rect, pub tex_coords: egui::Rect, - pub z: f32, } impl Quad { - pub const fn new(pos: egui::Rect, tex_coords: egui::Rect, z: f32) -> Self { - Self { pos, tex_coords, z } + pub const fn new(pos: egui::Rect, tex_coords: egui::Rect) -> Self { + Self { pos, tex_coords } } fn norm_tex_coords(self, extents: wgpu::Extent3d) -> Self { @@ -41,12 +40,12 @@ impl Quad { } fn into_corners(self) -> [Vertex; 4] { - let Self { pos, tex_coords, z } = self; + let Self { pos, tex_coords } = self; let top_left = { let position = pos.left_top(); let tex_coords = tex_coords.left_top(); Vertex { - position: glam::vec3(position.x, position.y, z), + position: glam::vec2(position.x, position.y), tex_coords: glam::vec2(tex_coords.x, tex_coords.y), } }; @@ -54,7 +53,7 @@ impl Quad { let position = pos.right_top(); let tex_coords = tex_coords.right_top(); Vertex { - position: glam::vec3(position.x, position.y, z), + position: glam::vec2(position.x, position.y), tex_coords: glam::vec2(tex_coords.x, tex_coords.y), } }; @@ -62,7 +61,7 @@ impl Quad { let position = pos.right_bottom(); let tex_coords = tex_coords.right_bottom(); Vertex { - position: glam::vec3(position.x, position.y, z), + position: glam::vec2(position.x, position.y), tex_coords: glam::vec2(tex_coords.x, tex_coords.y), } }; @@ -70,7 +69,7 @@ impl Quad { let position = pos.left_bottom(); let tex_coords = tex_coords.left_bottom(); Vertex { - position: glam::vec3(position.x, position.y, z), + position: glam::vec2(position.x, position.y), tex_coords: glam::vec2(tex_coords.x, tex_coords.y), } }; diff --git a/crates/graphics/src/data/transform.rs b/crates/graphics/src/data/transform.rs new file mode 100644 index 00000000..70caf2c9 --- /dev/null +++ b/crates/graphics/src/data/transform.rs @@ -0,0 +1,101 @@ +// Copyright (C) 2024 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use wgpu::util::DeviceExt; + +use crate::{BindGroupLayoutBuilder, GraphicsState}; + +#[derive(Debug)] +pub struct Transform { + data: Data, + uniform: wgpu::Buffer, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +struct Data { + position: glam::Vec2, + scale: glam::Vec2, +} + +impl Transform { + pub fn new(graphics_state: &GraphicsState, position: glam::Vec2, scale: glam::Vec2) -> Self { + let data = Data { position, scale }; + + let uniform = graphics_state.render_state.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("transform buffer"), + contents: bytemuck::bytes_of(&data), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }, + ); + + Self { data, uniform } + } + + pub fn new_position(graphics_state: &GraphicsState, position: glam::Vec2) -> Self { + Self::new(graphics_state, position, glam::Vec2::ONE) + } + + pub fn unit(graphics_state: &GraphicsState) -> Self { + Self::new(graphics_state, glam::Vec2::ZERO, glam::Vec2::ONE) + } + + pub fn set_position( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + position: glam::Vec2, + ) { + self.data.position = position; + self.regen_buffer(render_state); + } + + pub fn set_scale(&mut self, render_state: &luminol_egui_wgpu::RenderState, scale: glam::Vec2) { + self.data.scale = scale; + self.regen_buffer(render_state); + } + + fn regen_buffer(&mut self, render_state: &luminol_egui_wgpu::RenderState) { + render_state + .queue + .write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.data)); + } + + pub fn as_buffer(&self) -> &wgpu::Buffer { + &self.uniform + } + + pub fn add_to_bind_group_layout(layout_builder: &mut BindGroupLayoutBuilder) { + layout_builder.append( + wgpu::ShaderStages::VERTEX_FRAGMENT, + wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + None, + ); + } +} diff --git a/crates/graphics/src/vertex.rs b/crates/graphics/src/data/vertex.rs similarity index 92% rename from crates/graphics/src/vertex.rs rename to crates/graphics/src/data/vertex.rs index efaf09c9..7c99b8f7 100644 --- a/crates/graphics/src/vertex.rs +++ b/crates/graphics/src/data/vertex.rs @@ -18,13 +18,13 @@ #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, PartialEq)] pub struct Vertex { - pub position: glam::Vec3, + pub position: glam::Vec2, pub tex_coords: glam::Vec2, } impl Vertex { const ATTRIBS: [wgpu::VertexAttribute; 2] = - wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2]; + wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2]; pub const fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as wgpu::BufferAddress, diff --git a/crates/graphics/src/data/viewport.rs b/crates/graphics/src/data/viewport.rs new file mode 100644 index 00000000..9d06c749 --- /dev/null +++ b/crates/graphics/src/data/viewport.rs @@ -0,0 +1,98 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use wgpu::util::DeviceExt; + +use crate::{BindGroupLayoutBuilder, GraphicsState}; + +#[derive(Debug)] +pub struct Viewport { + data: Data, + uniform: wgpu::Buffer, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C, align(16))] +struct Data { + viewport_size: glam::Vec2, + viewport_translation: glam::Vec2, + viewport_scale: glam::Vec2, + _pad: [u32; 2], +} + +impl Viewport { + pub fn new(graphics_state: &GraphicsState, viewport_size: glam::Vec2) -> Self { + let data = Data { + viewport_size, + viewport_translation: glam::Vec2::ZERO, + viewport_scale: glam::Vec2::ONE, + _pad: [0; 2], + }; + let uniform = graphics_state.render_state.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("viewport buffer"), + contents: bytemuck::bytes_of(&data), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }, + ); + + Self { data, uniform } + } + + pub fn set_size(&mut self, render_state: &luminol_egui_wgpu::RenderState, size: glam::Vec2) { + self.data.viewport_size = size; + self.regen_buffer(render_state); + } + + pub fn set( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + size: glam::Vec2, + translation: glam::Vec2, + scale: glam::Vec2, + ) { + self.data.viewport_size = size; + self.data.viewport_translation = translation; + self.data.viewport_scale = scale; + self.regen_buffer(render_state); + } + + pub fn as_buffer(&self) -> &wgpu::Buffer { + &self.uniform + } + + fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) { + render_state + .queue + .write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.data)); + } + + pub fn add_to_bind_group_layout( + layout_builder: &mut BindGroupLayoutBuilder, + ) -> &mut BindGroupLayoutBuilder { + layout_builder.append( + wgpu::ShaderStages::VERTEX_FRAGMENT, + wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + None, + ) + } +} diff --git a/crates/graphics/src/event.rs b/crates/graphics/src/event.rs index beb12ce0..7da37819 100644 --- a/crates/graphics/src/event.rs +++ b/crates/graphics/src/event.rs @@ -16,53 +16,106 @@ // along with Luminol. If not, see . use color_eyre::eyre::Context; -use image::EncodableLayout; -use itertools::Itertools; -use wgpu::util::DeviceExt; -use std::sync::Arc; - -use fragile::Fragile; - -use crate::{quad::Quad, sprite::Sprite, tiles::Atlas, viewport::Viewport, GraphicsState}; +use crate::{Atlas, GraphicsState, Quad, Renderable, Sprite, Transform, Viewport}; pub struct Event { - sprite: Arc, - viewport: Arc, + pub sprite: Sprite, pub sprite_size: egui::Vec2, } -// wgpu types are not Send + Sync on webassembly, so we use fragile to make sure we never access any wgpu resources across thread boundaries -pub struct Callback { - sprite: Fragile>, - graphics_state: Fragile>, -} +impl Event { + // code smell, fix + pub fn new_map( + graphics_state: &GraphicsState, + filesystem: &impl luminol_filesystem::FileSystem, + viewport: &Viewport, + event: &luminol_data::rpg::Event, + atlas: &Atlas, + ) -> color_eyre::Result> { + let Some(page) = event.pages.first() else { + color_eyre::eyre::bail!("event does not have first page"); + }; + + let mut is_placeholder = false; + let texture = if let Some(ref filename) = page.graphic.character_name { + let texture = graphics_state + .texture_loader + .load_now_dir(filesystem, "Graphics/Characters", filename) + .wrap_err_with(|| format!("Error loading event character graphic {filename:?}")); + match texture { + Ok(t) => t, + Err(e) => { + graphics_state.send_texture_error(e); + is_placeholder = true; + graphics_state.texture_loader.placeholder_texture() + } + } + } else if page.graphic.tile_id.is_some() { + atlas.atlas_texture.clone() + } else { + return Ok(None); + }; -impl Callback { - pub fn paint<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - let sprite = self.sprite.get(); - let graphics_state = self.graphics_state.get(); + let (quad, sprite_size) = if let Some(id) = page.graphic.tile_id { + // Why does this have to be + 1? + let quad = atlas.calc_quad((id + 1) as i16); - sprite.draw(graphics_state, render_pass); - } -} + (quad, egui::vec2(32., 32.)) + } else if is_placeholder { + let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(32., 32.0)); + let quad = Quad::new(rect, rect); -impl luminol_egui_wgpu::CallbackTrait for Callback { - fn paint<'a>( - &'a self, - _info: egui::PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'a>, - _callback_resources: &'a luminol_egui_wgpu::CallbackResources, - ) { - self.paint(render_pass) + (quad, egui::vec2(32., 32.)) + } else { + let cw = texture.width() as f32 / 4.; + let ch = texture.height() as f32 / 4.; + let pos = egui::Rect::from_min_size( + egui::pos2( + 0., //(event.x as f32 * 32.) + (16. - (cw / 2.)), + 0., //(event.y as f32 * 32.) + (32. - ch), + ), + egui::vec2(cw, ch), + ); + + // Reduced by 0.01 px on all sides to reduce texture bleeding + let tex_coords = egui::Rect::from_min_size( + egui::pos2( + page.graphic.pattern as f32 * cw + 0.01, + (page.graphic.direction as f32 - 2.) / 2. * ch + 0.01, + ), + egui::vec2(cw - 0.02, ch - 0.02), + ); + let quad = Quad::new(pos, tex_coords); + + (quad, egui::vec2(cw, ch)) + }; + + let x = event.x as f32 * 32. + (32. - sprite_size.x) / 2.; + let y = event.y as f32 * 32. + (32. - sprite_size.y); + let transform = Transform::new_position(graphics_state, glam::vec2(x, y)); + + let sprite = Sprite::new( + graphics_state, + quad, + page.graphic.character_hue, + page.graphic.opacity, + page.graphic.blend_type, + &texture, + viewport, + transform, + ); + + Ok(Some(Self { + sprite, + sprite_size, + })) } -} -impl Event { - // code smell, fix - pub fn new( + pub fn new_standalone( graphics_state: &GraphicsState, filesystem: &impl luminol_filesystem::FileSystem, + viewport: &Viewport, event: &luminol_data::rpg::Event, atlas: &Atlas, ) -> color_eyre::Result> { @@ -70,6 +123,7 @@ impl Event { color_eyre::eyre::bail!("event does not have first page"); }; + let mut is_placeholder = false; let texture = if let Some(ref filename) = page.graphic.character_name { let texture = graphics_state .texture_loader @@ -79,50 +133,8 @@ impl Event { Ok(t) => t, Err(e) => { graphics_state.send_texture_error(e); - - let placeholder_char_texture = graphics_state - .texture_loader - .get("placeholder_char_texture") - .unwrap_or_else(|| { - let placeholder_img = graphics_state.placeholder_img(); - - graphics_state.texture_loader.register_texture( - "placeholder_char_texture", - graphics_state.render_state.device.create_texture_with_data( - &graphics_state.render_state.queue, - &wgpu::TextureDescriptor { - label: Some("placeholder_char_texture"), - size: wgpu::Extent3d { - width: 128, - height: 128, - depth_or_array_layers: 1, - }, - dimension: wgpu::TextureDimension::D2, - mip_level_count: 1, - sample_count: 1, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - wgpu::util::TextureDataOrder::LayerMajor, - &itertools::iproduct!(0..128, 0..128, 0..4) - .map(|(y, x, c)| { - // Tile the placeholder image - placeholder_img.as_bytes()[(c - + (x % placeholder_img.width()) * 4 - + (y % placeholder_img.height()) - * 4 - * placeholder_img.width()) - as usize] - }) - .collect_vec(), - ), - ) - }); - - placeholder_char_texture + is_placeholder = true; + graphics_state.texture_loader.placeholder_texture() } } } else if page.graphic.tile_id.is_some() { @@ -131,13 +143,16 @@ impl Event { return Ok(None); }; - let (quads, viewport, sprite_size) = if let Some(id) = page.graphic.tile_id { + let (quad, sprite_size) = if let Some(id) = page.graphic.tile_id { // Why does this have to be + 1? let quad = atlas.calc_quad((id + 1) as i16); - let viewport = Arc::new(Viewport::new(graphics_state, 32., 32.)); + (quad, egui::vec2(32., 32.)) + } else if is_placeholder { + let rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(32., 32.0)); + let quad = Quad::new(rect, rect); - (quad, viewport, egui::vec2(32., 32.)) + (quad, egui::vec2(32., 32.)) } else { let cw = texture.width() as f32 / 4.; let ch = texture.height() as f32 / 4.; @@ -157,54 +172,47 @@ impl Event { ), egui::vec2(cw - 0.02, ch - 0.02), ); - let quad = Quad::new(pos, tex_coords, 0.0); + let quad = Quad::new(pos, tex_coords); - let viewport = Arc::new(Viewport::new(graphics_state, cw, ch)); - - (quad, viewport, egui::vec2(cw, ch)) + (quad, egui::vec2(cw, ch)) }; - let sprite = Arc::new(Sprite::new( + let transform = Transform::unit(graphics_state); + + let sprite = Sprite::new( graphics_state, - viewport.clone(), - quads, - texture, - page.graphic.blend_type, + quad, page.graphic.character_hue, page.graphic.opacity, - )); + page.graphic.blend_type, + &texture, + viewport, + transform, + ); Ok(Some(Self { sprite, - viewport, sprite_size, })) } - pub fn sprite(&self) -> &Sprite { - &self.sprite + pub fn set_position(&mut self, render_state: &luminol_egui_wgpu::RenderState, x: i32, y: i32) { + let x = x as f32 * 32. + (32. - self.sprite_size.x) / 2.; + let y = y as f32 * 32. + (32. - self.sprite_size.y); + self.sprite + .transform + .set_position(render_state, glam::vec2(x, y)); } - pub fn set_proj(&self, render_state: &luminol_egui_wgpu::RenderState, proj: glam::Mat4) { - self.viewport.set_proj(render_state, proj); + pub fn sprite(&self) -> &Sprite { + &self.sprite } +} - pub fn callback(&self, graphics_state: Arc) -> Callback { - Callback { - sprite: Fragile::new(self.sprite.clone()), - graphics_state: Fragile::new(graphics_state), - } - } +impl Renderable for Event { + type Prepared = ::Prepared; - pub fn paint( - &self, - graphics_state: Arc, - painter: &egui::Painter, - rect: egui::Rect, - ) { - painter.add(luminol_egui_wgpu::Callback::new_paint_callback( - rect, - self.callback(graphics_state), - )); + fn prepare(&mut self, graphics_state: &std::sync::Arc) -> Self::Prepared { + self.sprite.prepare(graphics_state) } } diff --git a/crates/graphics/src/grid/display.rs b/crates/graphics/src/grid/display.rs deleted file mode 100644 index a864f819..00000000 --- a/crates/graphics/src/grid/display.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -use crossbeam::atomic::AtomicCell; -use wgpu::util::DeviceExt; - -use crate::{BindGroupLayoutBuilder, GraphicsState}; - -#[derive(Debug)] -pub struct Display { - data: AtomicCell, - uniform: Option, -} - -#[repr(C, align(16))] -#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] -pub struct Data { - viewport_size_in_pixels: [f32; 2], - pixels_per_point: f32, - inner_thickness_in_points: f32, -} - -impl Display { - pub fn new(graphics_state: &GraphicsState) -> Self { - let display = Data { - viewport_size_in_pixels: [0., 0.], - pixels_per_point: 1., - inner_thickness_in_points: 1., - }; - - let uniform = (!graphics_state.push_constants_supported()).then(|| { - graphics_state.render_state.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("grid display buffer"), - contents: bytemuck::bytes_of(&display), - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, - }, - ) - }); - - Display { - data: AtomicCell::new(display), - uniform, - } - } - - pub fn as_bytes(&self) -> [u8; std::mem::size_of::()] { - bytemuck::cast(self.data.load()) - } - - pub fn as_buffer(&self) -> Option<&wgpu::Buffer> { - self.uniform.as_ref() - } - - pub fn set_inner_thickness( - &self, - render_state: &luminol_egui_wgpu::RenderState, - inner_thickness_in_points: f32, - ) { - let data = self.data.load(); - if data.inner_thickness_in_points != inner_thickness_in_points { - self.data.store(Data { - inner_thickness_in_points, - ..data - }); - self.regen_buffer(render_state); - } - } - - pub(super) fn update_viewport_size( - &self, - render_state: &luminol_egui_wgpu::RenderState, - info: &egui::PaintCallbackInfo, - ) { - let viewport_size = info.viewport_in_pixels(); - let viewport_size = [ - viewport_size.width_px as f32, - viewport_size.height_px as f32, - ]; - let pixels_per_point = info.pixels_per_point.max(1.).floor(); - let data = self.data.load(); - if data.viewport_size_in_pixels != viewport_size - || data.pixels_per_point != pixels_per_point - { - self.data.store(Data { - viewport_size_in_pixels: viewport_size, - pixels_per_point, - ..data - }); - self.regen_buffer(render_state); - } - } - - fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) { - if let Some(uniform) = &self.uniform { - render_state - .queue - .write_buffer(uniform, 0, bytemuck::bytes_of(&self.data.load())); - } - } -} - -pub fn add_to_bind_group_layout( - layout_builder: &mut BindGroupLayoutBuilder, -) -> &mut BindGroupLayoutBuilder { - layout_builder.append( - wgpu::ShaderStages::FRAGMENT, - wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - None, - ) -} diff --git a/crates/graphics/src/grid/grid.wgsl b/crates/graphics/src/grid/grid.wgsl deleted file mode 100644 index 49645607..00000000 --- a/crates/graphics/src/grid/grid.wgsl +++ /dev/null @@ -1,85 +0,0 @@ -struct VertexInput { - @location(0) position: vec2, -} - -struct InstanceInput { - @location(1) tile_position: vec2, -} - -struct VertexOutput { - @builtin(position) clip_position: vec4, - @location(0) position: vec2, - // The fragment shader sees this as the position of the provoking vertex, - // which is set to the vertex at the right angle of every triangle - @location(1) @interpolate(flat) vertex_position: vec2, -} - -struct Viewport { - proj: mat4x4, -} - -struct Display { - viewport_size_in_pixels: vec2, - pixels_per_point: f32, - inner_thickness_in_points: f32, -} - -#if USE_PUSH_CONSTANTS == true -struct PushConstants { - viewport: Viewport, - display: Display, -} -var push_constants: PushConstants; -#else -@group(0) @binding(0) -var viewport: Viewport; -@group(0) @binding(1) -var display: Display; -#endif - -@vertex -fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput { - var out: VertexOutput; - -#if USE_PUSH_CONSTANTS == true - let viewport = push_constants.viewport; -#endif - - out.position = (viewport.proj * vec4((vertex.position + instance.tile_position) * 32., 0., 1.)).xy; - out.vertex_position = out.position; - out.clip_position = vec4(out.position, 0., 1.); - return out; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { -#if USE_PUSH_CONSTANTS == true - let display = push_constants.display; -#endif - - if display.viewport_size_in_pixels.x == 0. || display.viewport_size_in_pixels.y == 0. { - discard; - } - - var color: f32; - var alpha: f32; - - let diff = abs(input.position - input.vertex_position) * (display.viewport_size_in_pixels / 2.); - - let adjusted_outer_thickness = 1.001 * display.pixels_per_point; - let adjusted_inner_thickness = display.inner_thickness_in_points * adjusted_outer_thickness; - - if diff.x < adjusted_outer_thickness + adjusted_inner_thickness || diff.y < adjusted_outer_thickness + adjusted_inner_thickness { - if diff.x < adjusted_inner_thickness || diff.y < adjusted_inner_thickness { - color = 0.1; - } else { - color = 0.7; - } - alpha = 0.25; - } else { - color = 0.; - alpha = 0.; - } - - return vec4(color, color, color, alpha); -} diff --git a/crates/graphics/src/grid/instance.rs b/crates/graphics/src/grid/instance.rs deleted file mode 100644 index 8b401f29..00000000 --- a/crates/graphics/src/grid/instance.rs +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -use super::Vertex; -use itertools::Itertools; -use wgpu::util::DeviceExt; - -#[derive(Debug)] -pub struct Instances { - instance_buffer: wgpu::Buffer, - vertex_buffer: wgpu::Buffer, - - map_width: usize, - map_height: usize, -} - -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] -pub struct Instance { - position: [f32; 2], -} - -impl Instances { - pub fn new( - render_state: &luminol_egui_wgpu::RenderState, - map_width: usize, - map_height: usize, - ) -> Self { - let instances = Self::calculate_instances(map_width, map_height); - let instance_buffer = - render_state - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("tilemap grid instance buffer"), - contents: bytemuck::cast_slice(&instances), - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - }); - - let vertices = Self::calculate_vertices(render_state); - let vertex_buffer = - render_state - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("tilemap grid vertex buffer"), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - }); - - Self { - instance_buffer, - vertex_buffer, - - map_width, - map_height, - } - } - - fn calculate_instances(map_width: usize, map_height: usize) -> Vec { - (0..map_height) - .cartesian_product(0..map_width) - .map(|(map_y, map_x)| Instance { - position: [map_x as f32, map_y as f32], - }) - .collect_vec() - } - - fn calculate_vertices(render_state: &luminol_egui_wgpu::RenderState) -> [Vertex; 6] { - // OpenGL and WebGL use the last vertex in each triangle as the provoking vertex, and - // Direct3D, Metal, Vulkan and WebGPU use the first vertex in each triangle - if render_state.adapter.get_info().backend == wgpu::Backend::Gl { - [ - Vertex { - position: glam::vec2(1., 0.), - }, - Vertex { - position: glam::vec2(0., 1.), - }, - Vertex { - position: glam::vec2(0., 0.), // Provoking vertex - }, - Vertex { - position: glam::vec2(0., 1.), - }, - Vertex { - position: glam::vec2(1., 0.), - }, - Vertex { - position: glam::vec2(1., 1.), // Provoking vertex - }, - ] - } else { - [ - Vertex { - position: glam::vec2(0., 0.), // Provoking vertex - }, - Vertex { - position: glam::vec2(1., 0.), - }, - Vertex { - position: glam::vec2(0., 1.), - }, - Vertex { - position: glam::vec2(1., 1.), // Provoking vertex - }, - Vertex { - position: glam::vec2(0., 1.), - }, - Vertex { - position: glam::vec2(1., 0.), - }, - ] - } - } - - pub fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { - render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - - // Calculate the start and end index of the buffer, as well as the amount of instances. - let start_index = 0; - let end_index = self.map_width * self.map_height; - let count = (end_index - start_index) as u32; - - // Convert the indexes into actual offsets. - let start = (start_index * std::mem::size_of::()) as wgpu::BufferAddress; - let end = (end_index * std::mem::size_of::()) as wgpu::BufferAddress; - - render_pass.set_vertex_buffer(1, self.instance_buffer.slice(start..end)); - - render_pass.draw(0..6, 0..count); - } - - pub const fn desc() -> wgpu::VertexBufferLayout<'static> { - const ARRAY: &[wgpu::VertexAttribute] = &wgpu::vertex_attr_array![1 => Float32x2]; - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Instance, - attributes: ARRAY, - } - } -} diff --git a/crates/graphics/src/grid/mod.rs b/crates/graphics/src/grid/mod.rs deleted file mode 100644 index ad0b8e9c..00000000 --- a/crates/graphics/src/grid/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -use std::sync::Arc; - -use crate::{ - viewport::{self, Viewport}, - BindGroupBuilder, BindGroupLayoutBuilder, GraphicsState, -}; - -use display::Display; -use instance::Instances; -use vertex::Vertex; - -pub mod display; -mod instance; -pub(crate) mod shader; -mod vertex; - -#[derive(Debug)] -pub struct Grid { - pub instances: Instances, - pub display: display::Display, - pub viewport: Arc, - - pub bind_group: Option, -} - -impl Grid { - pub fn new( - graphics_state: &GraphicsState, - viewport: Arc, - map_width: usize, - map_height: usize, - ) -> Self { - let instances = Instances::new(&graphics_state.render_state, map_width, map_height); - let display = Display::new(graphics_state); - - let bind_group = (!graphics_state.push_constants_supported()).then(|| { - let mut bind_group_builder = BindGroupBuilder::new(); - bind_group_builder.append_buffer(viewport.as_buffer().unwrap()); - bind_group_builder.append_buffer(display.as_buffer().unwrap()); - bind_group_builder.build( - &graphics_state.render_state.device, - Some("grid bind group"), - &graphics_state.bind_group_layouts.grid, - ) - }); - - Self { - instances, - display, - viewport, - bind_group, - } - } - - pub fn draw<'rpass>( - &'rpass self, - graphics_state: &'rpass GraphicsState, - info: &egui::PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'rpass>, - ) { - #[repr(C)] - #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] - struct VertexPushConstant { - viewport: [u8; 64], - display: [u8; 16], - } - - render_pass.push_debug_group("tilemap grid renderer"); - render_pass.set_pipeline(&graphics_state.pipelines.grid); - - self.display - .update_viewport_size(&graphics_state.render_state, info); - - if let Some(bind_group) = &self.bind_group { - render_pass.set_bind_group(0, bind_group, &[]) - } else { - render_pass.set_push_constants( - wgpu::ShaderStages::VERTEX, - 0, - &self.viewport.as_bytes(), - ); - render_pass.set_push_constants( - wgpu::ShaderStages::FRAGMENT, - 64, - &self.display.as_bytes(), - ); - } - - self.instances.draw(render_pass); - render_pass.pop_debug_group(); - } -} - -pub fn create_bind_group_layout( - render_state: &luminol_egui_wgpu::RenderState, -) -> wgpu::BindGroupLayout { - let mut builder = BindGroupLayoutBuilder::new(); - - if !crate::push_constants_supported(render_state) { - viewport::add_to_bind_group_layout(&mut builder); - display::add_to_bind_group_layout(&mut builder); - } - - builder.build(&render_state.device, Some("grid bind group layout")) -} diff --git a/crates/graphics/src/lib.rs b/crates/graphics/src/lib.rs index 67fd6559..0d873176 100644 --- a/crates/graphics/src/lib.rs +++ b/crates/graphics/src/lib.rs @@ -19,77 +19,49 @@ pub mod binding_helpers; pub use binding_helpers::{BindGroupBuilder, BindGroupLayoutBuilder}; -pub mod collision; -pub mod grid; -pub mod quad; -pub mod sprite; -pub mod tiles; -pub mod vertex; -pub mod viewport; +pub mod loaders; +pub use loaders::texture::Texture; + +// Building blocks that make up more complex parts (i.e. the map view, or events) +pub mod primitives; +pub use primitives::{ + collision::Collision, grid::Grid, sprite::Sprite, tiles::Atlas, tiles::Tiles, +}; + +pub mod data; +pub use data::*; pub mod event; pub mod map; pub mod plane; - -pub mod atlas_loader; - -pub mod texture_loader; +pub mod tilepicker; pub use event::Event; pub use map::Map; pub use plane::Plane; - -pub use texture_loader::Texture; +pub use tilepicker::Tilepicker; pub struct GraphicsState { - pub texture_loader: texture_loader::Loader, - pub atlas_loader: atlas_loader::Loader, + pub texture_loader: loaders::texture::Loader, + pub atlas_loader: loaders::atlas::Loader, pub render_state: luminol_egui_wgpu::RenderState, pub nearest_sampler: wgpu::Sampler, - pipelines: Pipelines, - bind_group_layouts: BindGroupLayouts, + pipelines: primitives::Pipelines, + bind_group_layouts: primitives::BindGroupLayouts, texture_error_tx: crossbeam::channel::Sender, texture_error_rx: crossbeam::channel::Receiver, } -pub struct BindGroupLayouts { - sprite: wgpu::BindGroupLayout, - tiles: wgpu::BindGroupLayout, - collision: wgpu::BindGroupLayout, - grid: wgpu::BindGroupLayout, -} - -pub struct Pipelines { - sprites: std::collections::HashMap, - tiles: wgpu::RenderPipeline, - collision: wgpu::RenderPipeline, - grid: wgpu::RenderPipeline, -} - impl GraphicsState { pub fn new(render_state: luminol_egui_wgpu::RenderState) -> Self { - let bind_group_layouts = BindGroupLayouts { - sprite: sprite::create_bind_group_layout(&render_state), - tiles: tiles::create_bind_group_layout(&render_state), - collision: collision::create_bind_group_layout(&render_state), - grid: grid::create_bind_group_layout(&render_state), - }; - - let pipelines = Pipelines { - sprites: sprite::shader::create_sprite_shaders(&render_state, &bind_group_layouts), - tiles: tiles::shader::create_render_pipeline(&render_state, &bind_group_layouts), - collision: collision::shader::create_render_pipeline( - &render_state, - &bind_group_layouts, - ), - grid: grid::shader::create_render_pipeline(&render_state, &bind_group_layouts), - }; - - let texture_loader = texture_loader::Loader::new(render_state.clone()); - let atlas_cache = atlas_loader::Loader::default(); + let bind_group_layouts = primitives::BindGroupLayouts::new(&render_state); + let pipelines = primitives::Pipelines::new(&render_state, &bind_group_layouts); + + let texture_loader = loaders::texture::Loader::new(render_state.clone()); + let atlas_cache = loaders::atlas::Loader::default(); let nearest_sampler = render_state .device @@ -119,10 +91,6 @@ impl GraphicsState { } } - pub fn push_constants_supported(&self) -> bool { - push_constants_supported(&self.render_state) - } - pub fn send_texture_error(&self, error: color_eyre::Report) { self.texture_error_tx .try_send(error) @@ -132,17 +100,36 @@ impl GraphicsState { pub fn texture_errors(&self) -> impl Iterator + '_ { self.texture_error_rx.try_iter() } +} + +pub trait Renderable { + type Prepared: Drawable; + fn prepare(&mut self, graphics_state: &std::sync::Arc) -> Self::Prepared; +} + +pub trait Drawable { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>); +} - pub fn placeholder_img(&self) -> image::RgbaImage { - image::load_from_memory(include_bytes!("../data/placeholder.png")) - .expect("assets/placeholder.png is not a valid image") - .to_rgba8() +pub struct Painter { + prepared: fragile::Fragile, +} + +impl Painter { + pub fn new(prepared: T) -> Self { + Self { + prepared: fragile::Fragile::new(prepared), + } } } -pub fn push_constants_supported(render_state: &luminol_egui_wgpu::RenderState) -> bool { - render_state - .device - .features() - .contains(wgpu::Features::PUSH_CONSTANTS) +impl luminol_egui_wgpu::CallbackTrait for Painter { + fn paint<'a>( + &'a self, + _: egui::PaintCallbackInfo, + render_pass: &mut wgpu::RenderPass<'a>, + _: &'a luminol_egui_wgpu::CallbackResources, + ) { + self.prepared.get().draw(render_pass); + } } diff --git a/crates/graphics/src/atlas_loader.rs b/crates/graphics/src/loaders/atlas.rs similarity index 97% rename from crates/graphics/src/atlas_loader.rs rename to crates/graphics/src/loaders/atlas.rs index da22da04..7507b1b6 100644 --- a/crates/graphics/src/atlas_loader.rs +++ b/crates/graphics/src/loaders/atlas.rs @@ -14,7 +14,7 @@ // // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use crate::{tiles::Atlas, GraphicsState}; +use crate::{Atlas, GraphicsState}; #[derive(Default)] pub struct Loader { diff --git a/crates/graphics/src/loaders/mod.rs b/crates/graphics/src/loaders/mod.rs new file mode 100644 index 00000000..f959b64b --- /dev/null +++ b/crates/graphics/src/loaders/mod.rs @@ -0,0 +1,26 @@ +// Copyright (C) 2024 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +pub mod atlas; +pub mod texture; diff --git a/crates/graphics/src/texture_loader.rs b/crates/graphics/src/loaders/texture.rs similarity index 58% rename from crates/graphics/src/texture_loader.rs rename to crates/graphics/src/loaders/texture.rs index 35d0e613..e330f4db 100644 --- a/crates/graphics/src/texture_loader.rs +++ b/crates/graphics/src/loaders/texture.rs @@ -31,6 +31,10 @@ use wgpu::util::DeviceExt; pub struct Loader { loaded_textures: DashMap>, + placeholder_texture: Arc, + blank_autotile_texture: Arc, + placeholder_image: image::RgbaImage, + render_state: luminol_egui_wgpu::RenderState, } @@ -58,13 +62,27 @@ fn load_wgpu_texture_from_path( let file = filesystem.read(path)?; let texture_data = image::load_from_memory(&file)?.to_rgba8(); - Ok(device.create_texture_with_data( + Ok(load_wgpu_texture_from_image( + &texture_data, + device, + queue, + Some(path), + )) +} + +fn load_wgpu_texture_from_image( + image: &image::RgbaImage, + device: &wgpu::Device, + queue: &wgpu::Queue, + label: Option<&str>, +) -> wgpu::Texture { + device.create_texture_with_data( queue, &wgpu::TextureDescriptor { - label: Some(path), + label, size: wgpu::Extent3d { - width: texture_data.width(), - height: texture_data.height(), + width: image.width(), + height: image.height(), depth_or_array_layers: 1, }, dimension: wgpu::TextureDimension::D2, @@ -77,8 +95,30 @@ fn load_wgpu_texture_from_path( view_formats: &[], }, wgpu::util::TextureDataOrder::LayerMajor, - &texture_data, - )) + image, + ) +} + +fn register_native_texture( + render_state: luminol_egui_wgpu::RenderState, + texture: wgpu::Texture, + label: Option<&str>, +) -> Arc { + let view = texture.create_view(&wgpu::TextureViewDescriptor { + label, + ..Default::default() + }); + let texture_id = render_state.renderer.write().register_native_texture( + &render_state.device, + &view, + wgpu::FilterMode::Nearest, + ); + Arc::new(Texture { + texture, + view, + texture_id, + render_state, + }) } impl Texture { @@ -101,9 +141,56 @@ impl Texture { impl Loader { pub fn new(render_state: luminol_egui_wgpu::RenderState) -> Self { + let placeholder_image = + image::load_from_memory(luminol_macros::include_asset!("assets/placeholder.png")) + .expect("assets/placeholder.png is not a valid image") + .to_rgba8(); + + let placeholder_texture = load_wgpu_texture_from_image( + &placeholder_image, + &render_state.device, + &render_state.queue, + Some("assets/placeholder.png"), + ); + let placeholder_texture = register_native_texture( + render_state.clone(), + placeholder_texture, + Some("placeholder texture"), + ); + + let blank_autotile_texture = render_state + .device + .create_texture(&wgpu::TextureDescriptor { + label: Some("blank autotile texture"), + size: wgpu::Extent3d { + width: crate::primitives::tiles::AUTOTILE_FRAME_COLS + * crate::primitives::tiles::TILE_SIZE, + height: crate::primitives::tiles::AUTOTILE_ROWS + * crate::primitives::tiles::TILE_SIZE, + depth_or_array_layers: 1, + }, + dimension: wgpu::TextureDimension::D2, + mip_level_count: 1, + sample_count: 1, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let blank_autotile_texture = register_native_texture( + render_state.clone(), + blank_autotile_texture, + Some("blank autotile texture"), + ); + Self { loaded_textures: DashMap::with_capacity(64), + placeholder_texture, + blank_autotile_texture, + placeholder_image, + render_state, } } @@ -142,26 +229,8 @@ impl Loader { ) -> Arc { let path = path.into(); - let view = texture.create_view(&wgpu::TextureViewDescriptor { - label: Some(path.as_str()), - ..Default::default() - }); - - // todo maybe use custom sampler descriptor? - // would allow for better texture names in debuggers - let texture_id = self.render_state.renderer.write().register_native_texture( - &self.render_state.device, - &view, - wgpu::FilterMode::Nearest, - ); - - let texture = Arc::new(Texture { - texture, - view, - texture_id, - - render_state: self.render_state.clone(), - }); + let texture = + register_native_texture(self.render_state.clone(), texture, Some(path.as_str())); self.loaded_textures.insert(path, texture.clone()); texture } @@ -179,4 +248,16 @@ impl Loader { pub fn clear(&self) { self.loaded_textures.clear(); } + + pub fn placeholder_texture(&self) -> Arc { + self.placeholder_texture.clone() + } + + pub fn blank_autotile_texture(&self) -> Arc { + self.blank_autotile_texture.clone() + } + + pub fn placeholder_image(&self) -> &image::RgbaImage { + &self.placeholder_image + } } diff --git a/crates/graphics/src/map.rs b/crates/graphics/src/map.rs index 808e7005..ac5efbcf 100644 --- a/crates/graphics/src/map.rs +++ b/crates/graphics/src/map.rs @@ -16,124 +16,30 @@ // along with Luminol. If not, see . use color_eyre::eyre::Context; -use image::EncodableLayout; use itertools::Itertools; -use wgpu::util::DeviceExt; - -use std::sync::Arc; - -use std::time::Duration; - -use fragile::Fragile; use crate::{ - collision::Collision, grid::Grid, tiles::Tiles, viewport::Viewport, GraphicsState, Plane, + Atlas, Collision, Drawable, Event, GraphicsState, Grid, Plane, Renderable, Tiles, Transform, + Viewport, }; pub struct Map { - resources: Arc, - viewport: Arc, + pub tiles: Tiles, + pub panorama: Option, + pub fog: Option, + pub collision: Collision, + pub grid: Grid, + pub events: luminol_data::OptionVec, + pub atlas: Atlas, + + pub viewport: Viewport, ani_time: Option, pub fog_enabled: bool, pub pano_enabled: bool, pub coll_enabled: bool, pub grid_enabled: bool, - pub enabled_layers: Vec, -} - -struct Resources { - tiles: Tiles, - panorama: Option, - fog: Option, - collision: Collision, - grid: Grid, -} - -// wgpu types are not Send + Sync on webassembly, so we use fragile to make sure we never access any wgpu resources across thread boundaries -pub struct Callback { - resources: Fragile>, - graphics_state: Fragile>, - - pano_enabled: bool, - enabled_layers: Vec, - selected_layer: Option, -} - -pub struct OverlayCallback { - resources: Fragile>, - graphics_state: Fragile>, - - fog_enabled: bool, - coll_enabled: bool, - grid_enabled: bool, -} - -impl Callback { - pub fn paint<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) { - let resources = self.resources.get(); - let graphics_state = self.graphics_state.get(); - - if self.pano_enabled { - if let Some(panorama) = &resources.panorama { - panorama.draw(graphics_state, render_pass); - } - } - - resources.tiles.draw( - graphics_state, - &self.enabled_layers, - self.selected_layer, - render_pass, - ); - } -} - -impl OverlayCallback { - pub fn paint<'a>( - &'a self, - info: egui::PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'a>, - ) { - let resources = self.resources.get(); - let graphics_state = self.graphics_state.get(); - - if self.fog_enabled { - if let Some(fog) = &resources.fog { - fog.draw(graphics_state, render_pass); - } - } - - if self.coll_enabled { - resources.collision.draw(graphics_state, render_pass); - } - - if self.grid_enabled { - resources.grid.draw(graphics_state, &info, render_pass); - } - } -} - -impl luminol_egui_wgpu::CallbackTrait for Callback { - fn paint<'a>( - &'a self, - _info: egui::PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'a>, - _callback_resources: &'a luminol_egui_wgpu::CallbackResources, - ) { - self.paint(render_pass); - } -} - -impl luminol_egui_wgpu::CallbackTrait for OverlayCallback { - fn paint<'a>( - &'a self, - info: egui::PaintCallbackInfo, - render_pass: &mut wgpu::RenderPass<'a>, - _callback_resources: &'a luminol_egui_wgpu::CallbackResources, - ) { - self.paint(info, render_pass); - } + pub event_enabled: bool, } impl Map { @@ -148,20 +54,31 @@ impl Map { .atlas_loader .load_atlas(graphics_state, filesystem, tileset)?; - let viewport = Arc::new(Viewport::new( + let viewport = Viewport::new( graphics_state, - map.width as f32 * 32., - map.height as f32 * 32., - )); + glam::vec2(map.width as f32 * 32., map.height as f32 * 32.), + ); - let tiles = Tiles::new(graphics_state, viewport.clone(), atlas, &map.data); + let tiles = Tiles::new( + graphics_state, + &map.data, + &atlas, + &viewport, + Transform::unit(graphics_state), + ); let grid = Grid::new( graphics_state, - viewport.clone(), - map.data.xsize(), - map.data.ysize(), + &viewport, + Transform::unit(graphics_state), + map.data.xsize() as u32, + map.data.ysize() as u32, + ); + let collision = Collision::new( + graphics_state, + &viewport, + Transform::unit(graphics_state), + passages, ); - let collision = Collision::new(graphics_state, viewport.clone(), passages); let panorama = if let Some(ref panorama_name) = tileset.panorama_name { let texture = graphics_state @@ -171,53 +88,13 @@ impl Map { .unwrap_or_else(|e| { graphics_state.send_texture_error(e); - graphics_state - .texture_loader - .get("placeholder_tile_texture") - .unwrap_or_else(|| { - let placeholder_img = graphics_state.placeholder_img(); - - graphics_state.texture_loader.register_texture( - "placeholder_tile_texture", - graphics_state.render_state.device.create_texture_with_data( - &graphics_state.render_state.queue, - &wgpu::TextureDescriptor { - label: Some("placeholder_tile_texture"), - size: wgpu::Extent3d { - width: 32, - height: 32, - depth_or_array_layers: 1, - }, - dimension: wgpu::TextureDimension::D2, - mip_level_count: 1, - sample_count: 1, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - wgpu::util::TextureDataOrder::LayerMajor, - &itertools::iproduct!(0..32, 0..32, 0..4) - .map(|(y, x, c)| { - // Tile the placeholder image - placeholder_img.as_bytes()[(c - + (x % placeholder_img.width()) * 4 - + (y % placeholder_img.height()) - * 4 - * placeholder_img.width()) - as usize] - }) - .collect_vec(), - ), - ) - }) + graphics_state.texture_loader.placeholder_texture() }); Some(Plane::new( graphics_state, - viewport.clone(), - texture, + &viewport, + &texture, tileset.panorama_hue, 100, luminol_data::BlendMode::Normal, @@ -236,53 +113,13 @@ impl Map { .unwrap_or_else(|e| { graphics_state.send_texture_error(e); - graphics_state - .texture_loader - .get("placeholder_tile_texture") - .unwrap_or_else(|| { - let placeholder_img = graphics_state.placeholder_img(); - - graphics_state.texture_loader.register_texture( - "placeholder_tile_texture", - graphics_state.render_state.device.create_texture_with_data( - &graphics_state.render_state.queue, - &wgpu::TextureDescriptor { - label: Some("placeholder_tile_texture"), - size: wgpu::Extent3d { - width: 32, - height: 32, - depth_or_array_layers: 1, - }, - dimension: wgpu::TextureDimension::D2, - mip_level_count: 1, - sample_count: 1, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - wgpu::util::TextureDataOrder::LayerMajor, - &itertools::iproduct!(0..32, 0..32, 0..4) - .map(|(y, x, c)| { - // Tile the placeholder image - placeholder_img.as_bytes()[(c - + (x % placeholder_img.width()) * 4 - + (y % placeholder_img.height()) - * 4 - * placeholder_img.width()) - as usize] - }) - .collect_vec(), - ), - ) - }) + graphics_state.texture_loader.placeholder_texture() }); Some(Plane::new( graphics_state, - viewport.clone(), - texture, + &viewport, + &texture, tileset.fog_hue, tileset.fog_zoom, tileset.fog_blend_type, @@ -294,15 +131,25 @@ impl Map { None }; + let events = map + .events + .iter() + .map(|(id, event)| { + Event::new_map(graphics_state, filesystem, &viewport, event, &atlas) + .map(|opt_e| opt_e.map(|e| (id, e))) + }) + .flatten_ok() + .try_collect()?; + Ok(Self { - resources: std::sync::Arc::new(Resources { - tiles, - panorama, - fog, - collision, - grid, - }), + tiles, + panorama, + fog, + collision, + grid, + events, viewport, + atlas, ani_time: None, @@ -310,7 +157,7 @@ impl Map { pano_enabled: true, coll_enabled: false, grid_enabled: true, - enabled_layers: vec![true; map.data.zsize()], + event_enabled: true, }) } @@ -320,9 +167,7 @@ impl Map { tile_id: i16, position: (usize, usize, usize), ) { - self.resources - .tiles - .set_tile(render_state, tile_id, position); + self.tiles.set_tile(render_state, tile_id, position); } pub fn set_passage( @@ -331,88 +176,91 @@ impl Map { passage: i16, position: (usize, usize), ) { - self.resources - .collision - .set_passage(render_state, passage, position); - } - - pub fn set_proj(&self, render_state: &luminol_egui_wgpu::RenderState, proj: glam::Mat4) { - self.viewport.set_proj(render_state, proj); + self.collision.set_passage(render_state, passage, position); } - pub fn callback( - &self, - graphics_state: Arc, - selected_layer: Option, - ) -> Callback { - Callback { - resources: Fragile::new(self.resources.clone()), - graphics_state: Fragile::new(graphics_state), - pano_enabled: self.pano_enabled, - enabled_layers: self.enabled_layers.clone(), - selected_layer, - } - } - - pub fn paint( - &mut self, - graphics_state: Arc, - painter: &egui::Painter, - selected_layer: Option, - rect: egui::Rect, - ) { - let time = painter.ctx().input(|i| i.time); + pub fn update_animation(&mut self, render_state: &luminol_egui_wgpu::RenderState, time: f64) { if let Some(ani_time) = self.ani_time { if time - ani_time >= 16. / 60. { self.ani_time = Some(time); - self.resources - .tiles - .autotiles - .inc_ani_index(&graphics_state.render_state); + self.tiles.autotiles.inc_ani_index(render_state); } } else { self.ani_time = Some(time); } + } +} + +pub struct Prepared { + tiles: ::Prepared, + panorama: Option<::Prepared>, + fog: Option<::Prepared>, + collision: Option<::Prepared>, + grid: Option<::Prepared>, + events: Vec<::Prepared>, +} - painter - .ctx() - .request_repaint_after(Duration::from_secs_f64(16. / 60.)); +impl Renderable for Map { + type Prepared = Prepared; + + fn prepare(&mut self, graphics_state: &std::sync::Arc) -> Self::Prepared { + let tiles = self.tiles.prepare(graphics_state); + let panorama = self + .panorama + .as_mut() + .filter(|_| self.pano_enabled) + .map(|pano| pano.prepare(graphics_state)); + let fog = self + .fog + .as_mut() + .filter(|_| self.fog_enabled) + .map(|fog| fog.prepare(graphics_state)); + let collision = self + .coll_enabled + .then(|| self.collision.prepare(graphics_state)); + let grid = self.grid_enabled.then(|| self.grid.prepare(graphics_state)); + let events = if self.event_enabled { + self.events + .iter_mut() + .map(|(_, event)| event.prepare(graphics_state)) + .collect() + } else { + vec![] + }; - painter.add(luminol_egui_wgpu::Callback::new_paint_callback( - rect, - self.callback(graphics_state, selected_layer), - )); + Prepared { + tiles, + panorama, + fog, + collision, + grid, + events, + } } +} - pub fn overlay_callback( - &self, - graphics_state: Arc, - grid_inner_thickness: f32, - ) -> OverlayCallback { - self.resources - .grid - .display - .set_inner_thickness(&graphics_state.render_state, grid_inner_thickness); +impl Drawable for Prepared { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + if let Some(ref pano) = self.panorama { + pano.draw(render_pass); + } - OverlayCallback { - resources: Fragile::new(self.resources.clone()), - graphics_state: Fragile::new(graphics_state), - fog_enabled: self.fog_enabled, - coll_enabled: self.coll_enabled, - grid_enabled: self.grid_enabled, + self.tiles.draw(render_pass); + + for event in &self.events { + event.draw(render_pass); } - } - pub fn paint_overlay( - &self, - graphics_state: Arc, - painter: &egui::Painter, - grid_inner_thickness: f32, - rect: egui::Rect, - ) { - painter.add(luminol_egui_wgpu::Callback::new_paint_callback( - rect, - self.overlay_callback(graphics_state, grid_inner_thickness), - )); + if let Some(ref fog) = self.fog { + fog.draw(render_pass); + } + + if let Some(ref collision) = self.collision { + collision.draw(render_pass); + } + + if let Some(ref grid) = self.grid { + grid.draw(render_pass); + } } } diff --git a/crates/graphics/src/plane.rs b/crates/graphics/src/plane.rs index bab294b2..69b948f1 100644 --- a/crates/graphics/src/plane.rs +++ b/crates/graphics/src/plane.rs @@ -15,11 +15,10 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use crate::{quad::Quad, sprite::Sprite, viewport::Viewport, GraphicsState, Texture}; -use std::sync::Arc; +use crate::{GraphicsState, Quad, Renderable, Sprite, Texture, Transform, Viewport}; pub struct Plane { - sprite: Sprite, + pub sprite: Sprite, } impl Plane { @@ -27,8 +26,8 @@ impl Plane { #[allow(clippy::too_many_arguments)] pub fn new( graphics_state: &GraphicsState, - viewport: Arc, - texture: Arc, + viewport: &Viewport, + texture: &Texture, hue: i32, zoom: i32, blend_mode: luminol_data::BlendMode, @@ -48,27 +47,27 @@ impl Plane { let quad = Quad::new( egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(map_width, map_height)), tex_coords, - 0.0, ); let sprite = Sprite::new( graphics_state, - viewport, quad, - texture, - blend_mode, hue, opacity, + blend_mode, + texture, + viewport, + Transform::unit(graphics_state), ); Self { sprite } } +} + +impl Renderable for Plane { + type Prepared = ::Prepared; - pub fn draw<'rpass>( - &'rpass self, - graphics_state: &'rpass GraphicsState, - render_pass: &mut wgpu::RenderPass<'rpass>, - ) { - self.sprite.draw(graphics_state, render_pass); + fn prepare(&mut self, graphics_state: &std::sync::Arc) -> Self::Prepared { + self.sprite.prepare(graphics_state) } } diff --git a/crates/graphics/src/collision/instance.rs b/crates/graphics/src/primitives/collision/instance.rs similarity index 57% rename from crates/graphics/src/collision/instance.rs rename to crates/graphics/src/primitives/collision/instance.rs index 48b4babe..f09b9092 100644 --- a/crates/graphics/src/collision/instance.rs +++ b/crates/graphics/src/primitives/collision/instance.rs @@ -15,14 +15,12 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use super::Vertex; use itertools::Itertools; use wgpu::util::DeviceExt; #[derive(Debug)] pub struct Instances { instance_buffer: wgpu::Buffer, - vertex_buffer: wgpu::Buffer, map_width: usize, map_height: usize, @@ -31,7 +29,7 @@ pub struct Instances { #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] struct Instance { - position: [f32; 3], + position: [f32; 2], passage: u32, } @@ -50,19 +48,8 @@ impl Instances { usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, }); - let vertices = Self::calculate_vertices(); - let vertex_buffer = - render_state - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("tilemap collision vertex buffer"), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - }); - Self { instance_buffer, - vertex_buffer, map_width: passages.xsize(), map_height: passages.ysize(), @@ -81,7 +68,7 @@ impl Instances { &self.instance_buffer, offset as wgpu::BufferAddress, bytemuck::bytes_of(&Instance { - position: [position.0 as f32, position.1 as f32, 0.0], + position: [position.0 as f32, position.1 as f32], passage: passage as u32, }), ) @@ -99,80 +86,14 @@ impl Instances { let map_y = (index / passages.xsize()) % passages.ysize(); Instance { - position: [ - map_x as f32, - map_y as f32, - 0., // We don't do a depth buffer. z doesn't matter - ], + position: [map_x as f32, map_y as f32], passage: passage as u32, } }) .collect_vec() } - fn calculate_vertices() -> [Vertex; 12] { - let rect = egui::Rect::from_min_size(egui::pos2(0., 0.), egui::vec2(32., 32.)); - let center = glam::vec3(rect.center().x, rect.center().y, 0.); - let top_left = glam::vec3(rect.left_top().x, rect.left_top().y, 0.); - let top_right = glam::vec3(rect.right_top().x, rect.right_top().y, 0.); - let bottom_left = glam::vec3(rect.left_bottom().x, rect.left_bottom().y, 0.); - let bottom_right = glam::vec3(rect.right_bottom().x, rect.right_bottom().y, 0.); - - [ - Vertex { - position: center, - direction: 1, - }, - Vertex { - position: bottom_left, - direction: 1, - }, - Vertex { - position: bottom_right, - direction: 1, - }, - Vertex { - position: center, - direction: 2, - }, - Vertex { - position: top_left, - direction: 2, - }, - Vertex { - position: bottom_left, - direction: 2, - }, - Vertex { - position: center, - direction: 4, - }, - Vertex { - position: bottom_right, - direction: 4, - }, - Vertex { - position: top_right, - direction: 4, - }, - Vertex { - position: center, - direction: 8, - }, - Vertex { - position: top_right, - direction: 8, - }, - Vertex { - position: top_left, - direction: 8, - }, - ] - } - pub fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { - render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - // Calculate the start and end index of the buffer, as well as the amount of instances. let start_index = 0; let end_index = self.map_width * self.map_height; @@ -182,14 +103,14 @@ impl Instances { let start = (start_index * std::mem::size_of::()) as wgpu::BufferAddress; let end = (end_index * std::mem::size_of::()) as wgpu::BufferAddress; - render_pass.set_vertex_buffer(1, self.instance_buffer.slice(start..end)); + render_pass.set_vertex_buffer(0, self.instance_buffer.slice(start..end)); render_pass.draw(0..12, 0..count); } pub const fn desc() -> wgpu::VertexBufferLayout<'static> { const ARRAY: &[wgpu::VertexAttribute] = - &wgpu::vertex_attr_array![2 => Float32x3, 3 => Uint32]; + &wgpu::vertex_attr_array![0 => Float32x2, 1 => Uint32]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Instance, diff --git a/crates/graphics/src/primitives/collision/mod.rs b/crates/graphics/src/primitives/collision/mod.rs new file mode 100644 index 00000000..ea3ff51d --- /dev/null +++ b/crates/graphics/src/primitives/collision/mod.rs @@ -0,0 +1,226 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use std::sync::Arc; + +use crate::{ + BindGroupBuilder, BindGroupLayoutBuilder, Drawable, GraphicsState, Renderable, Transform, + Viewport, +}; + +use instance::Instances; +use itertools::Itertools; + +mod instance; +pub(crate) mod shader; + +#[derive(Debug)] +pub struct Collision { + pub transform: Transform, + // in an Arc so we can use it in rendering + instances: Arc, + bind_group: Arc, +} + +#[derive(Debug, Clone)] +pub enum CollisionType { + /// An event + Event, + /// A tile whose ID is less than 48 (i.e. a blank autotile) + BlankTile, + /// A tile whose ID is greater than or equal to 48 + Tile, +} + +impl Collision { + pub fn new( + graphics_state: &GraphicsState, + viewport: &Viewport, + transform: Transform, + passages: &luminol_data::Table2, + ) -> Self { + let instances = Instances::new(&graphics_state.render_state, passages); + + let mut bind_group_builder = BindGroupBuilder::new(); + bind_group_builder.append_buffer(viewport.as_buffer()); + bind_group_builder.append_buffer(transform.as_buffer()); + let bind_group = bind_group_builder.build( + &graphics_state.render_state.device, + Some("collision bind group"), + &graphics_state.bind_group_layouts.collision, + ); + + Self { + transform, + instances: Arc::new(instances), + bind_group: Arc::new(bind_group), + } + } + + pub fn set_passage( + &self, + render_state: &luminol_egui_wgpu::RenderState, + passage: i16, + position: (usize, usize), + ) { + self.instances.set_passage(render_state, passage, position) + } + + /// Determines the passage values for every position on the map, running `f(x, y, passage)` for + /// every position. + /// + /// `layers` should be an iterator over the enabled layer numbers of the map from top to bottom. + pub fn calculate_passages( + passages: &luminol_data::Table1, + priorities: &luminol_data::Table1, + tiles: &luminol_data::Table3, + events: Option<&luminol_data::OptionVec>, + layers: impl Iterator + Clone, + mut f: impl FnMut(usize, usize, i16), + ) { + let tileset_size = passages.len().min(priorities.len()); + + let mut event_map = if let Some(events) = events { + events + .iter() + .filter_map(|(_, event)| { + let page = event.pages.first()?; + if page.through { + return None; + } + let tile_event = + page.graphic + .tile_id + .map_or((15, 1, CollisionType::Event), |id| { + let tile_id = id + 1; + if tile_id >= tileset_size { + (0, 0, CollisionType::Event) + } else { + (passages[tile_id], priorities[tile_id], CollisionType::Event) + } + }); + Some(((event.x as usize, event.y as usize), tile_event)) + }) + .collect() + } else { + std::collections::HashMap::new() + }; + + for (y, x) in (0..tiles.ysize()).cartesian_product(0..tiles.xsize()) { + let tile_event = event_map.remove(&(x, y)); + + f( + x, + y, + Self::calculate_passage(tile_event.into_iter().chain(layers.clone().map(|z| { + let tile_id = tiles[(x, y, z)].try_into().unwrap_or_default(); + let collision_type = if tile_id < 48 { + CollisionType::BlankTile + } else { + CollisionType::Tile + }; + if tile_id >= tileset_size { + (0, 0, collision_type) + } else { + (passages[tile_id], priorities[tile_id], collision_type) + } + }))), + ); + } + } + + /// Determines the passage value for a position on the map given an iterator over the + /// `(passage, priority, collision_type)` values for the tiles in each layer on that position. + /// The iterator should iterate over the layers from top to bottom. + pub fn calculate_passage( + layers: impl Iterator + Clone, + ) -> i16 { + let mut computed_passage = 0; + + for direction in [1, 2, 4, 8] { + let mut at_least_one_layer_not_blank = false; + let mut layers = layers.clone().peekable(); + while let Some((passage, priority, collision_type)) = layers.next() { + if matches!( + collision_type, + CollisionType::Tile | CollisionType::BlankTile + ) { + if matches!(collision_type, CollisionType::BlankTile) + && (at_least_one_layer_not_blank || layers.peek().is_some()) + { + continue; + } else { + at_least_one_layer_not_blank = true; + } + } + if passage & direction != 0 { + computed_passage |= direction; + break; + } else if priority == 0 { + break; + } + } + } + + computed_passage + } +} + +pub struct Prepared { + bind_group: Arc, + instances: Arc, + graphics_state: Arc, +} + +impl Renderable for Collision { + type Prepared = Prepared; + + fn prepare(&mut self, graphics_state: &Arc) -> Self::Prepared { + let bind_group = Arc::clone(&self.bind_group); + let graphics_state = Arc::clone(graphics_state); + let instances = Arc::clone(&self.instances); + + Prepared { + bind_group, + instances, + graphics_state, + } + } +} + +impl Drawable for Prepared { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + render_pass.push_debug_group("tilemap collision renderer"); + render_pass.set_pipeline(&self.graphics_state.pipelines.collision); + + render_pass.set_bind_group(0, &self.bind_group, &[]); + + self.instances.draw(render_pass); + render_pass.pop_debug_group(); + } +} + +pub fn create_bind_group_layout( + render_state: &luminol_egui_wgpu::RenderState, +) -> wgpu::BindGroupLayout { + let mut builder = BindGroupLayoutBuilder::new(); + + Viewport::add_to_bind_group_layout(&mut builder); + Transform::add_to_bind_group_layout(&mut builder); + + builder.build(&render_state.device, Some("collision bind group layout")) +} diff --git a/crates/graphics/src/collision/shader.rs b/crates/graphics/src/primitives/collision/shader.rs similarity index 54% rename from crates/graphics/src/collision/shader.rs rename to crates/graphics/src/primitives/collision/shader.rs index d367292c..8b4bbf7f 100644 --- a/crates/graphics/src/collision/shader.rs +++ b/crates/graphics/src/primitives/collision/shader.rs @@ -16,37 +16,25 @@ // along with Luminol. If not, see . use super::instance::Instances; -use super::Vertex; pub fn create_render_pipeline( + composer: &mut naga_oil::compose::Composer, render_state: &luminol_egui_wgpu::RenderState, - bind_group_layouts: &crate::BindGroupLayouts, -) -> wgpu::RenderPipeline { - let push_constants_supported = crate::push_constants_supported(render_state); + bind_group_layouts: &crate::primitives::BindGroupLayouts, +) -> Result { + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/translation.wgsl"), + file_path: "translation.wgsl", + ..Default::default() + })?; - let mut composer = naga_oil::compose::Composer::default().with_capabilities( - push_constants_supported - .then_some(naga::valid::Capabilities::PUSH_CONSTANT) - .unwrap_or_default(), - ); - - let result = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { - source: include_str!("collision.wgsl"), + let module = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { + source: include_str!("../shaders/collision.wgsl"), file_path: "collision.wgsl", shader_type: naga_oil::compose::ShaderType::Wgsl, - shader_defs: std::collections::HashMap::from([( - "USE_PUSH_CONSTANTS".to_string(), - naga_oil::compose::ShaderDefValue::Bool(push_constants_supported), - )]), + shader_defs: std::collections::HashMap::new(), additional_imports: &[], - }); - let module = match result { - Ok(module) => module, - Err(e) => { - let error = e.emit_to_string(&composer); - panic!("{error}"); - } - }; + })?; let shader_module = render_state .device @@ -55,41 +43,16 @@ pub fn create_render_pipeline( source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)), }); - let push_constant_ranges: &[_] = if push_constants_supported { - &[ - // Viewport - wgpu::PushConstantRange { - stages: wgpu::ShaderStages::VERTEX, - range: 0..64, - }, - ] - } else { - &[] - }; - let label = if push_constants_supported { - "Tilemap Collision Render Pipeline Layout (push constants)" - } else { - "Tilemap Collision Render Pipeline Layout (uniforms)" - }; - - let collision_bgl: &wgpu::BindGroupLayout = &bind_group_layouts.collision; - let bind_group_layout_slice = std::slice::from_ref(&collision_bgl); - let bind_group_layouts: &[&wgpu::BindGroupLayout] = if push_constants_supported { - &[] - } else { - bind_group_layout_slice - }; - let pipeline_layout = render_state .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(label), - bind_group_layouts, - push_constant_ranges, + label: Some("Tilemap Collision Render Pipeline Layout"), + bind_group_layouts: &[&bind_group_layouts.collision], + push_constant_ranges: &[], }); - render_state + Ok(render_state .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Tilemap Collision Render Pipeline"), @@ -97,7 +60,7 @@ pub fn create_render_pipeline( vertex: wgpu::VertexState { module: &shader_module, entry_point: "vs_main", - buffers: &[Vertex::desc(), Instances::desc()], + buffers: &[Instances::desc()], }, fragment: Some(wgpu::FragmentState { module: &shader_module, @@ -111,5 +74,5 @@ pub fn create_render_pipeline( depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, - }) + })) } diff --git a/crates/graphics/src/primitives/grid/display.rs b/crates/graphics/src/primitives/grid/display.rs new file mode 100644 index 00000000..dab357e3 --- /dev/null +++ b/crates/graphics/src/primitives/grid/display.rs @@ -0,0 +1,100 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use wgpu::util::DeviceExt; + +use crate::{BindGroupLayoutBuilder, GraphicsState}; + +#[derive(Debug)] +pub struct Display { + data: Data, + uniform: wgpu::Buffer, +} + +#[repr(C, align(16))] +#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Data { + pixels_per_point: f32, + inner_thickness_in_points: f32, + map_size: [u32; 2], +} + +impl Display { + pub fn new(graphics_state: &GraphicsState, map_width: u32, map_height: u32) -> Self { + let data = Data { + pixels_per_point: 1., + inner_thickness_in_points: 1., + map_size: [map_width, map_height], + }; + + let uniform = graphics_state.render_state.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("grid display buffer"), + contents: bytemuck::bytes_of(&data), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }, + ); + + Display { data, uniform } + } + + pub fn as_buffer(&self) -> &wgpu::Buffer { + &self.uniform + } + + pub fn set_inner_thickness( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + inner_thickness_in_points: f32, + ) { + if self.data.inner_thickness_in_points != inner_thickness_in_points { + self.data.inner_thickness_in_points = inner_thickness_in_points; + self.regen_buffer(render_state); + } + } + + pub fn set_pixels_per_point( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + pixels_per_point: f32, + ) { + if self.data.pixels_per_point != pixels_per_point { + self.data.pixels_per_point = pixels_per_point; + self.regen_buffer(render_state); + } + } + + fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) { + render_state + .queue + .write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.data)); + } +} + +pub fn add_to_bind_group_layout( + layout_builder: &mut BindGroupLayoutBuilder, +) -> &mut BindGroupLayoutBuilder { + layout_builder.append( + wgpu::ShaderStages::VERTEX_FRAGMENT, + wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + None, + ) +} diff --git a/crates/graphics/src/grid/vertex.rs b/crates/graphics/src/primitives/grid/instance.rs similarity index 58% rename from crates/graphics/src/grid/vertex.rs rename to crates/graphics/src/primitives/grid/instance.rs index 28f5c2a3..6e2686da 100644 --- a/crates/graphics/src/grid/vertex.rs +++ b/crates/graphics/src/primitives/grid/instance.rs @@ -15,19 +15,19 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -#[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, PartialEq)] -pub struct Vertex { - pub position: glam::Vec2, +#[derive(Debug, Clone, Copy)] +pub struct Instances { + map_size: u32, } -impl Vertex { - const ATTRIBS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x2]; - pub const fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &Self::ATTRIBS, +impl Instances { + pub fn new(map_width: u32, map_height: u32) -> Self { + Self { + map_size: map_width * map_height, } } + + pub fn draw(self, render_pass: &mut wgpu::RenderPass<'_>) { + render_pass.draw(0..6, 0..self.map_size); + } } diff --git a/crates/graphics/src/primitives/grid/mod.rs b/crates/graphics/src/primitives/grid/mod.rs new file mode 100644 index 00000000..97af2ea2 --- /dev/null +++ b/crates/graphics/src/primitives/grid/mod.rs @@ -0,0 +1,115 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use std::sync::Arc; + +use crate::{ + BindGroupBuilder, BindGroupLayoutBuilder, Drawable, GraphicsState, Renderable, Transform, + Viewport, +}; + +use display::Display; +use instance::Instances; + +pub mod display; +mod instance; +pub(crate) mod shader; + +#[derive(Debug)] +pub struct Grid { + pub instances: Instances, + pub display: display::Display, + pub transform: Transform, + // in an Arc so we can use it in rendering + pub bind_group: Arc, +} + +impl Grid { + pub fn new( + graphics_state: &GraphicsState, + viewport: &Viewport, + transform: Transform, + map_width: u32, + map_height: u32, + ) -> Self { + let instances = Instances::new(map_width, map_height); + let display = Display::new(graphics_state, map_width, map_height); + + let mut bind_group_builder = BindGroupBuilder::new(); + bind_group_builder.append_buffer(viewport.as_buffer()); + bind_group_builder.append_buffer(transform.as_buffer()); + bind_group_builder.append_buffer(display.as_buffer()); + let bind_group = bind_group_builder.build( + &graphics_state.render_state.device, + Some("grid bind group"), + &graphics_state.bind_group_layouts.grid, + ); + + Self { + instances, + display, + transform, + bind_group: Arc::new(bind_group), + } + } +} + +pub struct Prepared { + bind_group: Arc, + instances: Instances, + graphics_state: Arc, +} + +impl Renderable for Grid { + type Prepared = Prepared; + + fn prepare(&mut self, graphics_state: &Arc) -> Self::Prepared { + let bind_group = Arc::clone(&self.bind_group); + let graphics_state = Arc::clone(graphics_state); + let instances = self.instances; + + Prepared { + bind_group, + instances, + graphics_state, + } + } +} + +impl Drawable for Prepared { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + render_pass.push_debug_group("tilemap grid renderer"); + render_pass.set_pipeline(&self.graphics_state.pipelines.grid); + + render_pass.set_bind_group(0, &self.bind_group, &[]); + + self.instances.draw(render_pass); + render_pass.pop_debug_group(); + } +} + +pub fn create_bind_group_layout( + render_state: &luminol_egui_wgpu::RenderState, +) -> wgpu::BindGroupLayout { + let mut builder = BindGroupLayoutBuilder::new(); + + Viewport::add_to_bind_group_layout(&mut builder); + Transform::add_to_bind_group_layout(&mut builder); + display::add_to_bind_group_layout(&mut builder); + + builder.build(&render_state.device, Some("grid bind group layout")) +} diff --git a/crates/graphics/src/grid/shader.rs b/crates/graphics/src/primitives/grid/shader.rs similarity index 51% rename from crates/graphics/src/grid/shader.rs rename to crates/graphics/src/primitives/grid/shader.rs index a377713f..ca64f8aa 100644 --- a/crates/graphics/src/grid/shader.rs +++ b/crates/graphics/src/primitives/grid/shader.rs @@ -15,39 +15,33 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use super::display; -use super::instance::Instances; -use super::Vertex; - pub fn create_render_pipeline( + composer: &mut naga_oil::compose::Composer, render_state: &luminol_egui_wgpu::RenderState, - bind_group_layouts: &crate::BindGroupLayouts, -) -> wgpu::RenderPipeline { - let push_constants_supported = crate::push_constants_supported(render_state); + bind_group_layouts: &crate::primitives::BindGroupLayouts, +) -> Result { + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/translation.wgsl"), + file_path: "translation.wgsl", + ..Default::default() + })?; - let mut composer = naga_oil::compose::Composer::default().with_capabilities( - push_constants_supported - .then_some(naga::valid::Capabilities::PUSH_CONSTANT) - .unwrap_or_default(), - ); + let shader_defs = if render_state.adapter.get_info().backend == wgpu::Backend::Gl { + std::collections::HashMap::from([( + "LUMINOL_BACKEND_GL".to_string(), + naga_oil::compose::ShaderDefValue::Bool(true), + )]) + } else { + std::collections::HashMap::default() + }; - let result = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { - source: include_str!("grid.wgsl"), + let module = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { + source: include_str!("../shaders/grid.wgsl"), file_path: "grid.wgsl", shader_type: naga_oil::compose::ShaderType::Wgsl, - shader_defs: std::collections::HashMap::from([( - "USE_PUSH_CONSTANTS".to_string(), - naga_oil::compose::ShaderDefValue::Bool(push_constants_supported), - )]), + shader_defs, additional_imports: &[], - }); - let module = match result { - Ok(module) => module, - Err(e) => { - let error = e.emit_to_string(&composer); - panic!("{error}"); - } - }; + })?; let shader_module = render_state .device @@ -56,46 +50,16 @@ pub fn create_render_pipeline( source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)), }); - let push_constant_ranges: &[_] = if push_constants_supported { - &[ - // Vertex - wgpu::PushConstantRange { - stages: wgpu::ShaderStages::VERTEX, - range: 0..64, - }, - // Fragment - wgpu::PushConstantRange { - stages: wgpu::ShaderStages::FRAGMENT, - range: 64..64 + std::mem::size_of::() as u32, - }, - ] - } else { - &[] - }; - let label = if push_constants_supported { - "Tilemap Grid Render Pipeline Layout (push constants)" - } else { - "Tilemap Grid Render Pipeline Layout (uniforms)" - }; - - let grid_bgl: &wgpu::BindGroupLayout = &bind_group_layouts.grid; - let bind_group_layout_slice = std::slice::from_ref(&grid_bgl); - let bind_group_layouts: &[&wgpu::BindGroupLayout] = if push_constants_supported { - &[] - } else { - bind_group_layout_slice - }; - let pipeline_layout = render_state .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(label), - bind_group_layouts, - push_constant_ranges, + label: Some("Tilemap Grid Render Pipeline Layout"), + bind_group_layouts: &[&bind_group_layouts.grid], + push_constant_ranges: &[], }); - render_state + Ok(render_state .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Tilemap Grid Render Pipeline"), @@ -103,7 +67,7 @@ pub fn create_render_pipeline( vertex: wgpu::VertexState { module: &shader_module, entry_point: "vs_main", - buffers: &[Vertex::desc(), Instances::desc()], + buffers: &[], }, fragment: Some(wgpu::FragmentState { module: &shader_module, @@ -117,5 +81,5 @@ pub fn create_render_pipeline( depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, - }) + })) } diff --git a/crates/graphics/src/primitives/mod.rs b/crates/graphics/src/primitives/mod.rs new file mode 100644 index 00000000..dbcd28db --- /dev/null +++ b/crates/graphics/src/primitives/mod.rs @@ -0,0 +1,89 @@ +// Copyright (C) 2024 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +pub mod collision; +pub mod grid; +pub mod sprite; +pub mod tiles; + +pub struct BindGroupLayouts { + sprite: wgpu::BindGroupLayout, + tiles: wgpu::BindGroupLayout, + collision: wgpu::BindGroupLayout, + grid: wgpu::BindGroupLayout, +} + +pub struct Pipelines { + sprites: std::collections::HashMap, + tiles: wgpu::RenderPipeline, + collision: wgpu::RenderPipeline, + grid: wgpu::RenderPipeline, +} + +impl BindGroupLayouts { + pub fn new(render_state: &luminol_egui_wgpu::RenderState) -> Self { + Self { + sprite: sprite::create_bind_group_layout(render_state), + tiles: tiles::create_bind_group_layout(render_state), + collision: collision::create_bind_group_layout(render_state), + grid: grid::create_bind_group_layout(render_state), + } + } +} + +macro_rules! create_pipelines { +( + $render_state:ident, $bind_group_layouts:ident, + $($name:ident: $fun:path),* +) => {{ + let mut composer = naga_oil::compose::Composer::default(); + $( + let $name = match $fun(&mut composer, $render_state, $bind_group_layouts) { + Ok(p) => p, + Err(err) => { + let err = err.emit_to_string(&composer); + panic!("Error creating {} render pipeline:\n{err}", stringify!($name)) + } + }; + )* + Pipelines { + $($name,)* + } +}}; +} + +impl Pipelines { + pub fn new( + render_state: &luminol_egui_wgpu::RenderState, + bind_group_layouts: &BindGroupLayouts, + ) -> Self { + create_pipelines! { + render_state, bind_group_layouts, + sprites: sprite::shader::create_sprite_shaders, + tiles: tiles::shader::create_render_pipeline, + collision: collision::shader::create_render_pipeline, + grid: grid::shader::create_render_pipeline + } + } +} diff --git a/crates/graphics/src/primitives/shaders/collision.wgsl b/crates/graphics/src/primitives/shaders/collision.wgsl new file mode 100644 index 00000000..0fbd938a --- /dev/null +++ b/crates/graphics/src/primitives/shaders/collision.wgsl @@ -0,0 +1,65 @@ +#import luminol::translation as Trans // 🏳️‍⚧️ + +struct InstanceInput { + @location(0) tile_position: vec2, + @location(1) passage: u32, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, +} + +@group(0) @binding(0) +var viewport: Trans::Viewport; +@group(0) @binding(1) +var transform: Trans::Transform; + +const VERTEX_POSITIONS = array( + vec2f(16., 16.), + vec2f(0., 32.), + vec2f(32., 32.), + + vec2f(16., 16.), + vec2f(0., 0.), + vec2f(0., 32.), + + vec2f(16., 16.), + vec2f(32., 32.), + vec2f(32., 0.), + + vec2f(16., 16.), + vec2f(32., 0.), + vec2f(0., 0.), +); + +const VERTEX_DIRECTIONS = array( + 1, + 2, + 4, + 8, +); + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32, instance: InstanceInput) -> VertexOutput { + var out: VertexOutput; + + var vertex_directions = VERTEX_DIRECTIONS; + let vertex_direction = vertex_directions[vertex_index / 3]; + + if (instance.passage & vertex_direction) == 0u { + return out; + } + + var vertex_positions = VERTEX_POSITIONS; + let vertex_position = vertex_positions[vertex_index] + (instance.tile_position * 32.); + let normalized_pos = Trans::translate_vertex(vertex_position, viewport, transform); + + out.clip_position = vec4(normalized_pos, 0.0, 1.0); + + return out; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + return vec4(1., 0., 0., 0.4); +} diff --git a/crates/graphics/src/primitives/shaders/gamma.wgsl b/crates/graphics/src/primitives/shaders/gamma.wgsl new file mode 100644 index 00000000..217c3217 --- /dev/null +++ b/crates/graphics/src/primitives/shaders/gamma.wgsl @@ -0,0 +1,14 @@ +#define_import_path luminol::gamma + +// 0-1 sRGB gamma from 0-1 linear +fn from_linear_rgb(rgb: vec3) -> vec3 { + let cutoff = rgb < vec3(0.0031308); + let lower = rgb * vec3(12.92); + let higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); + return select(higher, lower, cutoff); +} + +// 0-1 sRGBA gamma from 0-1 linear +fn from_linear_rgba(linear_rgba: vec4) -> vec4 { + return vec4(from_linear_rgb(linear_rgba.rgb), linear_rgba.a); +} \ No newline at end of file diff --git a/crates/graphics/src/primitives/shaders/grid.wgsl b/crates/graphics/src/primitives/shaders/grid.wgsl new file mode 100644 index 00000000..29508c85 --- /dev/null +++ b/crates/graphics/src/primitives/shaders/grid.wgsl @@ -0,0 +1,93 @@ +#import luminol::translation as Trans // 🏳️‍⚧️ + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) position: vec2, + // The fragment shader sees this as the position of the provoking vertex, + // which is set to the vertex at the right angle of every triangle + @location(1) @interpolate(flat) vertex_position: vec2, +} + +struct Display { + pixels_per_point: f32, + inner_thickness_in_points: f32, + map_size: vec2, +} + +// OpenGL and WebGL use the last vertex in each triangle as the provoking vertex, and +// Direct3D, Metal, Vulkan and WebGPU use the first vertex in each triangle +#ifdef LUMINOL_BACKEND_GL +const QUAD_VERTICES: array = array( + vec2f(1., 0.), + vec2f(0., 1.), + vec2f(0., 0.), // Provoking vertex + + vec2f(0., 1.), + vec2f(1., 0.), + vec2f(1., 1.), // Provoking vertex +); +#else +const QUAD_VERTICES: array, 6> = array, 6>( + vec2(0., 0.), // Provoking vertex + vec2(1., 0.), + vec2(0., 1.), + + vec2(1., 1.), // Provoking vertex + vec2(0., 1.), + vec2(1., 0.), +); +#endif + +@group(0) @binding(0) +var viewport: Trans::Viewport; +@group(0) @binding(1) +var transform: Trans::Transform; +@group(0) @binding(2) +var display: Display; + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32, @builtin(instance_index) instance_index: u32) -> VertexOutput { + var out: VertexOutput; + + var quad_vertices = QUAD_VERTICES; + let tile_position = vec2( + f32(instance_index % display.map_size.x), + f32(instance_index / display.map_size.x) + ); + let vertex_position = (quad_vertices[vertex_index] + tile_position) * 32.; + let normalized_pos = Trans::translate_vertex(vertex_position, viewport, transform); + + out.position = normalized_pos; + out.vertex_position = normalized_pos; + out.clip_position = vec4(normalized_pos, 0., 1.); + return out; +} + +@fragment +fn fs_main(input: VertexOutput) -> @location(0) vec4 { + if viewport.viewport_size.x == 0. || viewport.viewport_size.y == 0. { + discard; + } + + var color: f32; + var alpha: f32; + + let diff = abs(input.position - input.vertex_position) * (viewport.viewport_size / 2.); + + let adjusted_outer_thickness = 1.001 * display.pixels_per_point; + let adjusted_inner_thickness = display.inner_thickness_in_points * adjusted_outer_thickness; + + if diff.x < adjusted_outer_thickness + adjusted_inner_thickness || diff.y < adjusted_outer_thickness + adjusted_inner_thickness { + if diff.x < adjusted_inner_thickness || diff.y < adjusted_inner_thickness { + color = 0.1; + } else { + color = 0.7; + } + alpha = 0.25; + } else { + color = 0.; + alpha = 0.; + } + + return vec4(color, color, color, alpha); +} diff --git a/crates/graphics/src/primitives/shaders/hue.wgsl b/crates/graphics/src/primitives/shaders/hue.wgsl new file mode 100644 index 00000000..d45ccc3c --- /dev/null +++ b/crates/graphics/src/primitives/shaders/hue.wgsl @@ -0,0 +1,23 @@ +#define_import_path luminol::hue + +fn rgb_to_hsv(c: vec3) -> vec3 { + let K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + let p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + let q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + let d = q.x - min(q.w, q.y); + + // Avoid divide - by - zero situations by adding a very tiny delta. + // Since we always deal with underlying 8 - Bit color values, this + // should never mask a real value + let eps = 1.0e-10; + + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + eps)), d / (q.x + eps), q.x); +} + +fn hsv_to_rgb(c: vec3) -> vec3 { + let K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + let p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + + return c.z * mix(K.xxx, clamp(p - K.xxx, vec3(0.0), vec3(1.0)), c.y); +} \ No newline at end of file diff --git a/crates/graphics/src/primitives/shaders/sprite.wgsl b/crates/graphics/src/primitives/shaders/sprite.wgsl new file mode 100644 index 00000000..42cc4331 --- /dev/null +++ b/crates/graphics/src/primitives/shaders/sprite.wgsl @@ -0,0 +1,64 @@ +#import luminol::gamma as Gamma +#import luminol::hue as Hue +#import luminol::translation as Trans // 🏳️‍⚧️ + +// Vertex shader +struct VertexInput { + @location(0) position: vec2, + @location(1) tex_coords: vec2, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, +} + +struct Graphic { + hue: f32, + opacity: f32, + opacity_multiplier: f32, + _padding: u32, +} + +@group(0) @binding(0) +var t_diffuse: texture_2d; +@group(0) @binding(1) +var s_diffuse: sampler; + +@group(0) @binding(2) +var viewport: Trans::Viewport; +@group(0) @binding(3) +var transform: Trans::Transform; +@group(0) @binding(4) +var graphic: Graphic; + +@vertex +fn vs_main( + model: VertexInput, +) -> VertexOutput { + var out: VertexOutput; + out.tex_coords = model.tex_coords; + + out.clip_position = vec4(Trans::translate_vertex(model.position, viewport, transform), 0.0, 1.0); + + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + var tex_sample = textureSample(t_diffuse, s_diffuse, in.tex_coords); + + tex_sample.a *= graphic.opacity * graphic.opacity_multiplier; + if tex_sample.a <= 0. { + discard; + } + + if graphic.hue > 0.0 { + var hsv = Hue::rgb_to_hsv(tex_sample.rgb); + + hsv.x += graphic.hue; + tex_sample = vec4(Hue::hsv_to_rgb(hsv), tex_sample.a); + } + + return Gamma::from_linear_rgba(tex_sample); +} diff --git a/crates/graphics/src/tiles/tilemap.wgsl b/crates/graphics/src/primitives/shaders/tilemap.wgsl similarity index 59% rename from crates/graphics/src/tiles/tilemap.wgsl rename to crates/graphics/src/primitives/shaders/tilemap.wgsl index eb713247..5583dc70 100644 --- a/crates/graphics/src/tiles/tilemap.wgsl +++ b/crates/graphics/src/primitives/shaders/tilemap.wgsl @@ -1,25 +1,17 @@ -struct VertexInput { - @location(0) position: vec3, - @location(1) tex_coords: vec2, -} +#import luminol::gamma as Gamma +#import luminol::translation as Trans // 🏳️‍⚧️ struct InstanceInput { - @location(2) tile_position: vec3, - @location(3) tile_id: u32, - @location(4) layer: u32, + @location(0) tile_id: u32, + @builtin(instance_index) index: u32 } struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) tex_coords: vec2, - @location(1) @interpolate(flat) layer: u32, // todo: look into using multiple textures? } -struct Viewport { - proj: mat4x4, -} - struct Autotiles { frame_counts: array, 2>, animation_index: u32, @@ -31,38 +23,59 @@ var atlas: texture_2d; @group(0) @binding(1) var atlas_sampler: sampler; -#if USE_PUSH_CONSTANTS == true -struct PushConstants { - viewport: Viewport, - autotiles: Autotiles, +struct Display { opacity: f32, + map_size: vec2, } -var push_constants: PushConstants; -#else + @group(0) @binding(2) -var viewport: Viewport; +var viewport: Trans::Viewport; @group(0) @binding(3) -var autotiles: Autotiles; +var transform: Trans::Transform; @group(0) @binding(4) -var opacity: array, 1>; -#endif +var autotiles: Autotiles; +@group(0) @binding(5) +var display: Display; + +const VERTEX_POSITIONS = array( + vec2f(0.0, 0.0), + vec2f(32.0, 0.0), + vec2f(0.0, 32.0), + + vec2f(32.0, 0.0), + vec2f(0.0, 32.0), + vec2f(32.0, 32.0), +); +const TEX_COORDS = array( + // slightly smaller than 32x32 to reduce bleeding from adjacent pixels in the atlas + vec2f(0.01, 0.01), + vec2f(31.99, 0.01), + vec2f(0.01, 31.99), + + vec2f(31.99, 0.01), + vec2f(0.01, 31.99), + vec2f(31.99, 31.99), +); @vertex -fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput { +fn vs_main(@builtin(vertex_index) vertex_index: u32, instance: InstanceInput) -> VertexOutput { var out: VertexOutput; - out.layer = instance.layer; - -#if USE_PUSH_CONSTANTS == true - let viewport = push_constants.viewport; - let autotiles = push_constants.autotiles; -#endif if instance.tile_id < #AUTOTILE_ID_AMOUNT { return out; } - let position = viewport.proj * vec4(vertex.position.xy + (instance.tile_position.xy * f32(#TILE_SIZE)), 0.0, 1.0); - out.clip_position = vec4(position.xy, instance.tile_position.z, 1.0); + let layer_instance_index = instance.index % (display.map_size.x * display.map_size.y); + let tile_position = vec2( + f32(layer_instance_index % display.map_size.x), + f32(layer_instance_index / display.map_size.x) + ); + + var vertex_positions = VERTEX_POSITIONS; + let vertex_position = vertex_positions[vertex_index] + (tile_position * 32.0); + let normalized_pos = Trans::translate_vertex(vertex_position, viewport, transform); + + out.clip_position = vec4(normalized_pos, 0.0, 1.0); // we don't set the z because we have no z buffer let is_autotile = instance.tile_id < #TOTAL_AUTOTILE_ID_AMOUNT; @@ -94,50 +107,30 @@ fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput { if is_autotile { let autotile_type = instance.tile_id / #AUTOTILE_ID_AMOUNT - 1; -// we get an error about non constant indexing without this. -// not sure why -#if USE_PUSH_CONSTANTS == true - let frame_count = push_constants.autotiles.frame_counts[autotile_type / 4][autotile_type % 4]; -#else let frame_count = autotiles.frame_counts[autotile_type / 4][autotile_type % 4]; -#endif let frame = autotiles.animation_index % frame_count; atlas_tile_position.x += f32(frame * #AUTOTILE_FRAME_WIDTH); } - let tex_size = vec2(textureDimensions(atlas)); - out.tex_coords = vertex.tex_coords + (atlas_tile_position / tex_size); - return out; -} + let tex_size = vec2(textureDimensions(atlas)); + var vertex_tex_coords = TEX_COORDS; + let vertex_tex_coord = vertex_tex_coords[vertex_index] / tex_size; -// 0-1 sRGB gamma from 0-1 linear -fn gamma_from_linear_rgb(rgb: vec3) -> vec3 { - let cutoff = rgb < vec3(0.0031308); - let lower = rgb * vec3(12.92); - let higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); - return select(higher, lower, cutoff); -} + out.tex_coords = vertex_tex_coord + (atlas_tile_position / tex_size); -// 0-1 sRGBA gamma from 0-1 linear -fn gamma_from_linear_rgba(linear_rgba: vec4) -> vec4 { - return vec4(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a); + return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4 { var color = textureSample(atlas, atlas_sampler, input.tex_coords); -#if USE_PUSH_CONSTANTS == true - let layer_opacity = push_constants.opacity; -#else - let layer_opacity = opacity[input.layer / 4u][input.layer % 4u]; -#endif - color.a *= layer_opacity; + color.a *= display.opacity; if color.a <= 0.0 { discard; } - return gamma_from_linear_rgba(color); + return Gamma::from_linear_rgba(color); } diff --git a/crates/graphics/src/primitives/shaders/translation.wgsl b/crates/graphics/src/primitives/shaders/translation.wgsl new file mode 100644 index 00000000..14f355f2 --- /dev/null +++ b/crates/graphics/src/primitives/shaders/translation.wgsl @@ -0,0 +1,20 @@ +#define_import_path luminol::translation + +struct Transform { + position: vec2f, + scale: vec2f, +} + +struct Viewport { + viewport_size: vec2f, // size of the viewport in pixels + viewport_translation: vec2f, // additional translation in pixels + viewport_scale: vec2f, // additional scale in pixels + _pad: vec2u // 16 byte alignment (webgl requires 16 byte alignment for uniform buffers) +} + +fn translate_vertex(position: vec2f, viewport: Viewport, transform: Transform) -> vec2f { + let position_vp = position * transform.scale + transform.position; + let position_px = position_vp * viewport.viewport_scale + viewport.viewport_translation; + let position_norm = position_px / viewport.viewport_size * 2.0 - 1.0; + return vec2f(position_norm.x, -position_norm.y); // flip y-axis +} \ No newline at end of file diff --git a/crates/graphics/src/sprite/graphic.rs b/crates/graphics/src/primitives/sprite/graphic.rs similarity index 56% rename from crates/graphics/src/sprite/graphic.rs rename to crates/graphics/src/primitives/sprite/graphic.rs index df74b081..aca6d955 100644 --- a/crates/graphics/src/sprite/graphic.rs +++ b/crates/graphics/src/primitives/sprite/graphic.rs @@ -15,15 +15,14 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use crossbeam::atomic::AtomicCell; use wgpu::util::DeviceExt; use crate::{BindGroupLayoutBuilder, GraphicsState}; #[derive(Debug)] pub struct Graphic { - data: AtomicCell, - uniform: Option, + data: Data, + uniform: wgpu::Buffer, } #[repr(C)] @@ -46,84 +45,66 @@ impl Graphic { _padding: 0, }; - let uniform = (!graphics_state.push_constants_supported()).then(|| { - graphics_state.render_state.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("tilemap sprite graphic buffer"), - contents: bytemuck::cast_slice(&[data]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }, - ) - }); - - Self { - data: AtomicCell::new(data), - uniform, - } + let uniform = graphics_state.render_state.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("tilemap sprite graphic buffer"), + contents: bytemuck::bytes_of(&data), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }, + ); + + Self { data, uniform } } pub fn hue(&self) -> i32 { - (self.data.load().hue * 360.) as i32 + (self.data.hue * 360.) as i32 } - pub fn set_hue(&self, render_state: &luminol_egui_wgpu::RenderState, hue: i32) { + pub fn set_hue(&mut self, render_state: &luminol_egui_wgpu::RenderState, hue: i32) { let hue = (hue % 360) as f32 / 360.0; - let data = self.data.load(); - if data.hue != hue { - self.data.store(Data { hue, ..data }); + if self.data.hue != hue { + self.data.hue = hue; self.regen_buffer(render_state); } } pub fn opacity(&self) -> i32 { - (self.data.load().opacity * 255.) as i32 + (self.data.opacity * 255.) as i32 } - pub fn set_opacity(&self, render_state: &luminol_egui_wgpu::RenderState, opacity: i32) { + pub fn set_opacity(&mut self, render_state: &luminol_egui_wgpu::RenderState, opacity: i32) { let opacity = opacity as f32 / 255.0; - let data = self.data.load(); - if data.opacity != opacity { - self.data.store(Data { opacity, ..data }); + if self.data.opacity != opacity { + self.data.opacity = opacity; self.regen_buffer(render_state); } } pub fn opacity_multiplier(&self) -> f32 { - self.data.load().opacity_multiplier + self.data.opacity_multiplier } pub fn set_opacity_multiplier( - &self, + &mut self, render_state: &luminol_egui_wgpu::RenderState, opacity_multiplier: f32, ) { - let data = self.data.load(); - - if data.opacity_multiplier != opacity_multiplier { - self.data.store(Data { - opacity_multiplier, - ..data - }); + if self.data.opacity_multiplier != opacity_multiplier { + self.data.opacity_multiplier = opacity_multiplier; self.regen_buffer(render_state); } } - pub fn as_bytes(&self) -> [u8; std::mem::size_of::()] { - bytemuck::cast(self.data.load()) - } - - pub fn as_buffer(&self) -> Option<&wgpu::Buffer> { - self.uniform.as_ref() + pub fn as_buffer(&self) -> &wgpu::Buffer { + &self.uniform } fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) { - if let Some(uniform) = &self.uniform { - render_state - .queue - .write_buffer(uniform, 0, bytemuck::cast_slice(&[self.data.load()])); - } + render_state + .queue + .write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.data)); } } diff --git a/crates/graphics/src/sprite/mod.rs b/crates/graphics/src/primitives/sprite/mod.rs similarity index 60% rename from crates/graphics/src/sprite/mod.rs rename to crates/graphics/src/primitives/sprite/mod.rs index ec9097bf..00cd25fb 100644 --- a/crates/graphics/src/sprite/mod.rs +++ b/crates/graphics/src/primitives/sprite/mod.rs @@ -17,9 +17,8 @@ use std::sync::Arc; use crate::{ - quad::Quad, - viewport::{self, Viewport}, - BindGroupBuilder, BindGroupLayoutBuilder, GraphicsState, Texture, + BindGroupBuilder, BindGroupLayoutBuilder, Drawable, GraphicsState, Quad, Renderable, Texture, + Transform, Viewport, }; pub(crate) mod graphic; @@ -27,24 +26,27 @@ pub(crate) mod shader; mod vertices; pub struct Sprite { - pub texture: Arc, pub graphic: graphic::Graphic, - pub vertices: vertices::Vertices, + pub transform: Transform, pub blend_mode: luminol_data::BlendMode, - pub viewport: Arc, - pub bind_group: wgpu::BindGroup, + // stored in an Arc so we can use it in rendering + vertices: Arc, + bind_group: Arc, } impl Sprite { + #[allow(clippy::too_many_arguments)] pub fn new( graphics_state: &GraphicsState, - viewport: Arc, quad: Quad, - texture: Arc, - blend_mode: luminol_data::BlendMode, hue: i32, opacity: i32, + blend_mode: luminol_data::BlendMode, + // arranged in order of use in bind group + texture: &Texture, + viewport: &Viewport, + transform: Transform, ) -> Self { let vertices = vertices::Vertices::from_quads(&graphics_state.render_state, &[quad], texture.size()); @@ -53,12 +55,11 @@ impl Sprite { let mut bind_group_builder = BindGroupBuilder::new(); bind_group_builder .append_texture_view(&texture.view) - .append_sampler(&graphics_state.nearest_sampler); - if !graphics_state.push_constants_supported() { - bind_group_builder - .append_buffer(viewport.as_buffer().unwrap()) - .append_buffer(graphic.as_buffer().unwrap()); - } + .append_sampler(&graphics_state.nearest_sampler) + .append_buffer(viewport.as_buffer()) + .append_buffer(transform.as_buffer()) + .append_buffer(graphic.as_buffer()); + let bind_group = bind_group_builder.build( &graphics_state.render_state.device, Some("sprite bind group"), @@ -66,47 +67,46 @@ impl Sprite { ); Self { - texture, graphic, - vertices, blend_mode, - viewport, + transform, - bind_group, + vertices: Arc::new(vertices), + bind_group: Arc::new(bind_group), } } +} - pub fn reupload_verts(&self, render_state: &luminol_egui_wgpu::RenderState, quads: &[Quad]) { - let vertices = Quad::into_vertices(quads, self.texture.size()); - render_state.queue.write_buffer( - &self.vertices.vertex_buffer, - 0, - bytemuck::cast_slice(&vertices), - ); +pub struct Prepared { + bind_group: Arc, + vertices: Arc, + graphics_state: Arc, + blend_mode: luminol_data::BlendMode, +} + +impl Renderable for Sprite { + type Prepared = Prepared; + + fn prepare(&mut self, graphics_state: &Arc) -> Self::Prepared { + let bind_group = Arc::clone(&self.bind_group); + let graphics_state = Arc::clone(graphics_state); + let vertices = Arc::clone(&self.vertices); + + Prepared { + bind_group, + vertices, + graphics_state, + blend_mode: self.blend_mode, + } } +} - pub fn draw<'rpass>( - &'rpass self, - graphics_state: &'rpass GraphicsState, - render_pass: &mut wgpu::RenderPass<'rpass>, - ) { +impl Drawable for Prepared { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { render_pass.push_debug_group("sprite render"); - render_pass.set_pipeline(&graphics_state.pipelines.sprites[&self.blend_mode]); + render_pass.set_pipeline(&self.graphics_state.pipelines.sprites[&self.blend_mode]); render_pass.set_bind_group(0, &self.bind_group, &[]); - if graphics_state.push_constants_supported() { - render_pass.set_push_constants( - wgpu::ShaderStages::VERTEX, - 0, - &self.viewport.as_bytes(), - ); - render_pass.set_push_constants( - wgpu::ShaderStages::FRAGMENT, - 64, - &self.graphic.as_bytes(), - ); - } - self.vertices.draw(render_pass); render_pass.pop_debug_group(); } @@ -132,10 +132,9 @@ pub fn create_bind_group_layout( None, ); - if !crate::push_constants_supported(render_state) { - viewport::add_to_bind_group_layout(&mut builder); - graphic::add_to_bind_group_layout(&mut builder); - } + Viewport::add_to_bind_group_layout(&mut builder); + Transform::add_to_bind_group_layout(&mut builder); + graphic::add_to_bind_group_layout(&mut builder); builder.build(&render_state.device, Some("sprite bind group layout")) } diff --git a/crates/graphics/src/primitives/sprite/shader.rs b/crates/graphics/src/primitives/sprite/shader.rs new file mode 100644 index 00000000..a7369377 --- /dev/null +++ b/crates/graphics/src/primitives/sprite/shader.rs @@ -0,0 +1,144 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use std::collections::HashMap; + +use naga_oil::compose::ComposerError; + +use crate::{primitives::BindGroupLayouts, Vertex}; + +fn create_shader( + composer: &mut naga_oil::compose::Composer, + render_state: &luminol_egui_wgpu::RenderState, + bind_group_layouts: &BindGroupLayouts, + target: wgpu::BlendState, +) -> Result { + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/translation.wgsl"), + file_path: "translation.wgsl", + ..Default::default() + })?; + + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/hue.wgsl"), + file_path: "hue.wgsl", + ..Default::default() + })?; + + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/gamma.wgsl"), + file_path: "gamma.wgsl", + ..Default::default() + })?; + + let module = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { + source: include_str!("../shaders/sprite.wgsl"), + file_path: "sprite.wgsl", + shader_type: naga_oil::compose::ShaderType::Wgsl, + shader_defs: HashMap::new(), + additional_imports: &[], + })?; + + let shader_module = render_state + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Sprite Shader Module"), + source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)), + }); + + let pipeline_layout = + render_state + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Sprite Pipeline Layout"), + bind_group_layouts: &[&bind_group_layouts.sprite], + push_constant_ranges: &[], + }); + + Ok(render_state + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Tilemap Sprite Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: "vs_main", + buffers: &[Vertex::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + blend: Some(target), + ..render_state.target_format.into() + })], + }), + primitive: wgpu::PrimitiveState { + // polygon_mode: wgpu::PolygonMode::Line, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + })) +} + +const BLEND_ADD: wgpu::BlendState = wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, +}; +const BLEND_SUBTRACT: wgpu::BlendState = wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::ReverseSubtract, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::Zero, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::ReverseSubtract, + }, +}; + +pub fn create_sprite_shaders( + composer: &mut naga_oil::compose::Composer, + render_state: &luminol_egui_wgpu::RenderState, + bind_group_layouts: &BindGroupLayouts, +) -> Result, ComposerError> { + [ + ( + luminol_data::BlendMode::Normal, + wgpu::BlendState::ALPHA_BLENDING, + ), + (luminol_data::BlendMode::Add, BLEND_ADD), + (luminol_data::BlendMode::Subtract, BLEND_SUBTRACT), + ] + .into_iter() + .map(|(mode, target)| { + let shader = create_shader(composer, render_state, bind_group_layouts, target)?; + Ok((mode, shader)) + }) + .collect() +} diff --git a/crates/graphics/src/sprite/vertices.rs b/crates/graphics/src/primitives/sprite/vertices.rs similarity index 98% rename from crates/graphics/src/sprite/vertices.rs rename to crates/graphics/src/primitives/sprite/vertices.rs index 85f39592..46e1c685 100644 --- a/crates/graphics/src/sprite/vertices.rs +++ b/crates/graphics/src/primitives/sprite/vertices.rs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use crate::quad::Quad; +use crate::Quad; #[derive(Debug)] pub struct Vertices { diff --git a/crates/graphics/src/tiles/atlas.rs b/crates/graphics/src/primitives/tiles/atlas.rs similarity index 84% rename from crates/graphics/src/tiles/atlas.rs rename to crates/graphics/src/primitives/tiles/atlas.rs index 79f83bfd..ea4dcf45 100644 --- a/crates/graphics/src/tiles/atlas.rs +++ b/crates/graphics/src/primitives/tiles/atlas.rs @@ -21,7 +21,7 @@ use itertools::Itertools; use wgpu::util::DeviceExt; use super::autotile_ids::AUTOTILES; -use crate::{quad::Quad, GraphicsState, Texture}; +use crate::{GraphicsState, Quad, Texture}; pub const MAX_SIZE: u32 = 8192; // Max texture size in one dimension pub const TILE_SIZE: u32 = 32; // Tiles are 32x32 @@ -60,33 +60,19 @@ impl Atlas { filesystem: &impl luminol_filesystem::FileSystem, tileset: &luminol_data::rpg::Tileset, ) -> Atlas { - let tileset_img = tileset.tileset_name.as_ref().map(|tileset_name| { - filesystem + let tileset_img = tileset.tileset_name.as_ref().and_then(|tileset_name| { + let result = filesystem .read(camino::Utf8Path::new("Graphics/Tilesets").join(tileset_name)) .and_then(|file| image::load_from_memory(&file).map_err(|e| e.into())) - .wrap_err_with(|| format!("Error loading atlas tileset {tileset_name:?}")) - .unwrap_or_else(|e| { + .wrap_err_with(|| format!("Error loading atlas tileset {tileset_name:?}")); + // we don't actually need to unwrap this to a placeholder image because we fill in the atlas texture with the placeholder image. + match result { + Ok(img) => Some(img.into_rgba8()), + Err(e) => { graphics_state.send_texture_error(e); - let width = 256; - let height = 256; - let placeholder_img = graphics_state.placeholder_img(); - image::RgbaImage::from_raw( - width, - height, - itertools::iproduct!(0..height, 0..width, 0..4) - .map(|(y, x, c)| { - // Tile the placeholder image - placeholder_img.as_bytes()[(c - + (x % placeholder_img.width()) * 4 - + (y % placeholder_img.height()) * 4 * placeholder_img.width()) - as usize] - }) - .collect_vec(), - ) - .unwrap() - .into() - }) - .to_rgba8() + None + } + } }); let tileset_height = tileset_img @@ -99,33 +85,7 @@ impl Atlas { .iter() .map(|s| { if s.is_empty() { - let blank_autotile_texture = graphics_state - .texture_loader - .get("blank_autotile_texture") - .unwrap_or_else(|| { - graphics_state.texture_loader.register_texture( - "blank_autotile_texture", - graphics_state.render_state.device.create_texture( - &wgpu::TextureDescriptor { - label: Some("blank_autotile_texture"), - size: wgpu::Extent3d { - width: AUTOTILE_FRAME_COLS * TILE_SIZE, - height: AUTOTILE_ROWS * TILE_SIZE, - depth_or_array_layers: 1, - }, - dimension: wgpu::TextureDimension::D2, - mip_level_count: 1, - sample_count: 1, - format: wgpu::TextureFormat::Rgba8UnormSrgb, - usage: wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }, - ), - ) - }); - Some(blank_autotile_texture) + Some(graphics_state.texture_loader.blank_autotile_texture()) } else { graphics_state .texture_loader @@ -193,8 +153,7 @@ impl Atlas { height = MAX_SIZE; } - let placeholder_img = graphics_state.placeholder_img(); - + let placeholder_img = graphics_state.texture_loader.placeholder_image(); let atlas_texture = graphics_state.render_state.device.create_texture_with_data( &graphics_state.render_state.queue, &wgpu::TextureDescriptor { @@ -214,6 +173,7 @@ impl Atlas { view_formats: &[], }, wgpu::util::TextureDataOrder::LayerMajor, + // we can avoid this collect_vec() by mapping a buffer and then copying that to a texture. it'd also allow us to copy everything easier too. do we want to do this? &itertools::iproduct!(0..height, 0..width, 0..4) .map(|(y, x, c)| { // Tile the placeholder image to fill the atlas @@ -381,7 +341,6 @@ impl Atlas { atlas_tile_position + egui::vec2(0.01, 0.01), egui::vec2(TILE_SIZE as f32 - 0.02, TILE_SIZE as f32 - 0.02), ), - 0.0, ) } } diff --git a/crates/graphics/src/primitives/tiles/autotile_ids.rs b/crates/graphics/src/primitives/tiles/autotile_ids.rs new file mode 100644 index 00000000..19833293 --- /dev/null +++ b/crates/graphics/src/primitives/tiles/autotile_ids.rs @@ -0,0 +1,70 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +/// Hardcoded list of tiles from r48 and old python Luminol. +/// There seems to be very little pattern in autotile IDs so this is sadly +/// the best we can do. +pub const AUTOTILES: [[u32; 4]; 48] = [ + [26, 27, 32, 33], + [4, 27, 32, 33], + [26, 5, 32, 33], + [4, 5, 32, 33], + [26, 27, 32, 11], + [4, 27, 32, 11], + [26, 5, 32, 11], + [4, 5, 32, 11], + [26, 27, 10, 33], + [4, 27, 10, 33], + [26, 5, 10, 33], + [4, 5, 10, 33], + [26, 27, 10, 11], + [4, 27, 10, 11], + [26, 5, 10, 11], + [4, 5, 10, 11], + [24, 25, 30, 31], + [24, 5, 30, 31], + [24, 25, 30, 11], + [24, 5, 30, 11], + [14, 15, 20, 21], + [14, 15, 20, 11], + [14, 15, 10, 21], + [14, 15, 10, 11], + [28, 29, 34, 35], + [28, 29, 10, 35], + [4, 29, 34, 35], + [4, 29, 10, 35], + [38, 39, 44, 45], + [4, 39, 44, 45], + [38, 5, 44, 45], + [4, 5, 44, 45], + [24, 29, 30, 35], + [14, 15, 44, 45], + [12, 13, 18, 19], + [12, 13, 18, 11], + [16, 17, 22, 23], + [16, 17, 10, 23], + [40, 41, 46, 47], + [4, 41, 46, 47], + [36, 37, 42, 43], + [36, 5, 42, 43], + [12, 17, 18, 23], + [12, 13, 42, 43], + [36, 41, 42, 47], + [16, 17, 46, 47], + [12, 17, 42, 47], + [0, 1, 6, 7], +]; diff --git a/crates/graphics/src/tiles/autotiles.rs b/crates/graphics/src/primitives/tiles/autotiles.rs similarity index 59% rename from crates/graphics/src/tiles/autotiles.rs rename to crates/graphics/src/primitives/tiles/autotiles.rs index a763aa3d..d4d0701c 100644 --- a/crates/graphics/src/tiles/autotiles.rs +++ b/crates/graphics/src/primitives/tiles/autotiles.rs @@ -15,15 +15,14 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . -use crossbeam::atomic::AtomicCell; use wgpu::util::DeviceExt; use crate::{BindGroupLayoutBuilder, GraphicsState}; #[derive(Debug)] pub struct Autotiles { - data: AtomicCell, - uniform: Option, + data: Data, + uniform: wgpu::Buffer, } #[repr(C, align(16))] @@ -38,7 +37,7 @@ struct Data { impl Autotiles { pub fn new(graphics_state: &GraphicsState, atlas: &super::Atlas) -> Self { - let autotiles = Data { + let data = Data { autotile_frames: atlas.autotile_frames, max_frame_count: atlas.autotile_width / super::atlas::AUTOTILE_FRAME_WIDTH, ani_index: 0, @@ -46,45 +45,30 @@ impl Autotiles { _end_padding: 0, }; - let uniform = (!graphics_state.push_constants_supported()).then(|| { - graphics_state.render_state.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("tilemap autotile buffer"), - contents: bytemuck::cast_slice(&[autotiles]), - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, - }, - ) - }); + let uniform = graphics_state.render_state.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("tilemap autotile buffer"), + contents: bytemuck::bytes_of(&data), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }, + ); - Autotiles { - data: AtomicCell::new(autotiles), - uniform, - } + Autotiles { data, uniform } } - pub fn inc_ani_index(&self, render_state: &luminol_egui_wgpu::RenderState) { - let data = self.data.load(); - self.data.store(Data { - ani_index: data.ani_index.wrapping_add(1), - ..data - }); + pub fn inc_ani_index(&mut self, render_state: &luminol_egui_wgpu::RenderState) { + self.data.ani_index = self.data.ani_index.wrapping_add(1); self.regen_buffer(render_state); } - pub fn as_bytes(&self) -> [u8; std::mem::size_of::()] { - bytemuck::cast(self.data.load()) - } - - pub fn as_buffer(&self) -> Option<&wgpu::Buffer> { - self.uniform.as_ref() + pub fn as_buffer(&self) -> &wgpu::Buffer { + &self.uniform } fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) { - if let Some(uniform) = &self.uniform { - render_state - .queue - .write_buffer(uniform, 0, bytemuck::cast_slice(&[self.data.load()])); - } + render_state + .queue + .write_buffer(&self.uniform, 0, bytemuck::bytes_of(&self.data)); } } diff --git a/crates/graphics/src/primitives/tiles/display.rs b/crates/graphics/src/primitives/tiles/display.rs new file mode 100644 index 00000000..b0805912 --- /dev/null +++ b/crates/graphics/src/primitives/tiles/display.rs @@ -0,0 +1,179 @@ +// Copyright (C) 2024 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use wgpu::util::DeviceExt; + +use crate::{BindGroupLayoutBuilder, GraphicsState}; + +#[derive(Debug)] +pub struct Display { + data: LayerData, + uniform: wgpu::Buffer, +} + +#[derive(Debug)] +struct LayerData { + data: Vec, + min_alignment_size: u32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Data { + opacity: f32, + _pad: [u8; 4], + map_size: [u32; 2], +} + +impl Data { + fn aligned_size_of(min_alignment_size: u32) -> usize { + wgpu::util::align_to( + std::mem::size_of::(), + (min_alignment_size as usize).max(std::mem::align_of::()), + ) + } +} + +impl LayerData { + fn range_of_layer(&self, layer: usize) -> std::ops::Range { + let data_size = Data::aligned_size_of(self.min_alignment_size); + let start = layer * data_size; + let end = start + std::mem::size_of::(); + start..end + } + + fn bytes_of_layer(&self, layer: usize) -> &[u8] { + let range = self.range_of_layer(layer); + &self.data[range] + } + + fn bytes_of_layer_mut(&mut self, layer: usize) -> &mut [u8] { + let range = self.range_of_layer(layer); + &mut self.data[range] + } + + fn read_data_at(&self, layer: usize) -> &Data { + bytemuck::from_bytes(self.bytes_of_layer(layer)) + } + + fn read_data_at_mut(&mut self, layer: usize) -> &mut Data { + bytemuck::from_bytes_mut(self.bytes_of_layer_mut(layer)) + } +} + +impl Display { + pub fn new( + graphics_state: &GraphicsState, + map_width: u32, + map_height: u32, + layers: usize, + ) -> Self { + let limits = graphics_state.render_state.device.limits(); + let min_alignment_size = limits.min_uniform_buffer_offset_alignment; + + let data_size = Data::aligned_size_of(min_alignment_size); + let mut layer_data = LayerData { + data: vec![0; data_size * layers], + min_alignment_size, + }; + + for layer in 0..layers { + *layer_data.read_data_at_mut(layer) = Data { + opacity: 1.0, + _pad: [0; 4], + map_size: [map_width, map_height], + }; + } + + let uniform = graphics_state.render_state.device.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("tilemap display buffer"), + contents: bytemuck::cast_slice(&layer_data.data), + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, + }, + ); + + Self { + data: layer_data, + uniform, + } + } + + pub fn as_buffer(&self) -> &wgpu::Buffer { + &self.uniform + } + + pub fn bytes_of_layer(&self, layer: usize) -> &[u8] { + self.data.bytes_of_layer(layer) + } + + pub fn opacity(&self, layer: usize) -> f32 { + self.data.read_data_at(layer).opacity + } + + pub fn set_opacity( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + opacity: f32, + layer: usize, + ) { + let layer_data = self.data.read_data_at_mut(layer); + if layer_data.opacity != opacity { + layer_data.opacity = opacity; + self.regen_buffer(render_state, &self.data.data); + } + } + + pub fn aligned_layer_size(&self) -> usize { + Data::aligned_size_of(self.data.min_alignment_size) + } + + pub fn layer_offsets(&self) -> Vec { + (0..self.data.data.len() / self.aligned_layer_size()) + .map(|layer| self.layer_offset(layer)) + .collect() + } + + pub fn layer_offset(&self, layer: usize) -> u32 { + self.data.range_of_layer(layer).start as u32 + } + + fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState, data: &[u8]) { + render_state.queue.write_buffer(self.as_buffer(), 0, data); + } +} + +pub fn add_to_bind_group_layout( + layout_builder: &mut BindGroupLayoutBuilder, +) -> &mut BindGroupLayoutBuilder { + layout_builder.append( + wgpu::ShaderStages::VERTEX_FRAGMENT, + wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: None, + }, + None, + ) +} diff --git a/crates/graphics/src/tiles/instance.rs b/crates/graphics/src/primitives/tiles/instance.rs similarity index 69% rename from crates/graphics/src/tiles/instance.rs rename to crates/graphics/src/primitives/tiles/instance.rs index 0daad676..3ccc5f74 100644 --- a/crates/graphics/src/tiles/instance.rs +++ b/crates/graphics/src/primitives/tiles/instance.rs @@ -18,12 +18,9 @@ use itertools::Itertools; use wgpu::util::DeviceExt; -use crate::quad::Quad; - #[derive(Debug)] pub struct Instances { instance_buffer: wgpu::Buffer, - vertex_buffer: wgpu::Buffer, map_width: usize, map_height: usize, @@ -32,23 +29,13 @@ pub struct Instances { #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] struct Instance { - position: [f32; 3], tile_id: u32, // force this to be an u32 to avoid padding issues - layer: u32, } -const TILE_QUAD: Quad = Quad::new( - egui::Rect::from_min_max(egui::pos2(0., 0.), egui::pos2(32., 32.0)), - // slightly smaller than 32x32 to reduce bleeding from adjacent pixels in the atlas - egui::Rect::from_min_max(egui::pos2(0.01, 0.01), egui::pos2(31.99, 31.99)), - 0.0, -); - impl Instances { pub fn new( render_state: &luminol_egui_wgpu::RenderState, map_data: &luminol_data::Table3, - atlas_size: wgpu::Extent3d, ) -> Self { let instances = Self::calculate_instances(map_data); let instance_buffer = @@ -60,11 +47,8 @@ impl Instances { usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, }); - let (vertex_buffer, _) = Quad::into_buffer(render_state, &[TILE_QUAD], atlas_size); - Self { instance_buffer, - vertex_buffer, map_width: map_data.xsize(), map_height: map_data.ysize(), @@ -86,9 +70,7 @@ impl Instances { &self.instance_buffer, offset as wgpu::BufferAddress, bytemuck::bytes_of(&Instance { - position: [position.0 as f32, position.1 as f32, 0.0], tile_id: tile_id as u32, - layer: position.2 as u32, }), ) } @@ -97,34 +79,16 @@ impl Instances { map_data .iter() .copied() - .enumerate() // Previously we'd filter out tiles that would not display (anything < 48). // However, storing the entire map like this makes it easier to edit tiles without remaking the entire buffer. // It's a memory tradeoff for a lot of performance. - .map(|(index, tile_id)| { - // We reset the x every xsize elements. - let map_x = index % map_data.xsize(); - // We reset the y every ysize elements, but only increment it every xsize elements. - let map_y = (index / map_data.xsize()) % map_data.ysize(); - // We increment the z every xsize * ysize elements. - let map_z = index / (map_data.xsize() * map_data.ysize()); - - Instance { - position: [ - map_x as f32, - map_y as f32, - 0., // We don't do a depth buffer. z doesn't matter - ], - tile_id: tile_id as u32, - layer: map_z as u32, - } + .map(|tile_id| Instance { + tile_id: tile_id as u32, }) .collect_vec() } pub fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>, layer: usize) { - render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); - // Calculate the start and end index of the buffer, as well as the amount of instances. let start_index = layer * self.map_width * self.map_height; let end_index = (layer + 1) * self.map_width * self.map_height; @@ -134,14 +98,13 @@ impl Instances { let start = (start_index * std::mem::size_of::()) as wgpu::BufferAddress; let end = (end_index * std::mem::size_of::()) as wgpu::BufferAddress; - render_pass.set_vertex_buffer(1, self.instance_buffer.slice(start..end)); + render_pass.set_vertex_buffer(0, self.instance_buffer.slice(start..end)); render_pass.draw(0..6, 0..count); } pub const fn desc() -> wgpu::VertexBufferLayout<'static> { - const ARRAY: &[wgpu::VertexAttribute] = - &wgpu::vertex_attr_array![2 => Float32x3, 3 => Uint32, 4 => Uint32]; + const ARRAY: &[wgpu::VertexAttribute] = &wgpu::vertex_attr_array![0 => Uint32]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Instance, diff --git a/crates/graphics/src/primitives/tiles/mod.rs b/crates/graphics/src/primitives/tiles/mod.rs new file mode 100644 index 00000000..fcb50e59 --- /dev/null +++ b/crates/graphics/src/primitives/tiles/mod.rs @@ -0,0 +1,187 @@ +// Copyright (C) 2023 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . + +use std::sync::Arc; + +use crate::{ + BindGroupBuilder, BindGroupLayoutBuilder, Drawable, GraphicsState, Renderable, Transform, + Viewport, +}; + +pub use atlas::*; + +use autotiles::Autotiles; +use display::Display; +use instance::Instances; + +mod atlas; +mod autotile_ids; +pub(crate) mod autotiles; +pub(crate) mod display; +mod instance; +pub(crate) mod shader; + +pub struct Tiles { + pub autotiles: Autotiles, + pub display: Display, + pub transform: Transform, + pub enabled_layers: Vec, + pub selected_layer: Option, + + instances: Arc, + bind_group: Arc, +} + +impl Tiles { + pub fn new( + graphics_state: &GraphicsState, + tiles: &luminol_data::Table3, + // in order of use in bind group + atlas: &Atlas, + viewport: &Viewport, + transform: Transform, + ) -> Self { + let autotiles = Autotiles::new(graphics_state, atlas); + let instances = Instances::new(&graphics_state.render_state, tiles); + let display = Display::new( + graphics_state, + tiles.xsize() as u32, + tiles.ysize() as u32, + tiles.zsize(), + ); + + let mut bind_group_builder = BindGroupBuilder::new(); + bind_group_builder + .append_texture_view(&atlas.atlas_texture.view) + .append_sampler(&graphics_state.nearest_sampler) + .append_buffer(viewport.as_buffer()) + .append_buffer(transform.as_buffer()) + .append_buffer(autotiles.as_buffer()) + .append_buffer_with_size(display.as_buffer(), display.aligned_layer_size() as u64); + + let bind_group = bind_group_builder.build( + &graphics_state.render_state.device, + Some("tilemap bind group"), + &graphics_state.bind_group_layouts.tiles, + ); + + Self { + autotiles, + display, + transform, + enabled_layers: vec![true; tiles.zsize()], + selected_layer: None, + + instances: Arc::new(instances), + bind_group: Arc::new(bind_group), + } + } + + pub fn set_tile( + &self, + render_state: &luminol_egui_wgpu::RenderState, + tile_id: i16, + position: (usize, usize, usize), + ) { + self.instances.set_tile(render_state, tile_id, position) + } +} + +pub struct Prepared { + bind_group: Arc, + instances: Arc, + graphics_state: Arc, + + layer_offsets: Vec, + enabled_layers: Vec, +} + +impl Renderable for Tiles { + type Prepared = Prepared; + + fn prepare(&mut self, graphics_state: &Arc) -> Self::Prepared { + let bind_group = Arc::clone(&self.bind_group); + let graphics_state = Arc::clone(graphics_state); + let instances = Arc::clone(&self.instances); + + for layer in 0..self.enabled_layers.len() { + let opacity = if self.selected_layer.is_some_and(|s| s != layer) { + 0.5 + } else { + 1.0 + }; + self.display + .set_opacity(&graphics_state.render_state, opacity, layer); + } + + Prepared { + bind_group, + instances, + graphics_state, + + layer_offsets: self.display.layer_offsets(), + enabled_layers: self.enabled_layers.clone(), + } + } +} + +impl Drawable for Prepared { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + render_pass.push_debug_group("tilemap tiles renderer"); + render_pass.set_pipeline(&self.graphics_state.pipelines.tiles); + + for layer in self + .enabled_layers + .iter() + .enumerate() + .filter_map(|(layer, enabled)| enabled.then_some(layer)) + { + render_pass.set_bind_group(0, &self.bind_group, &[self.layer_offsets[layer]]); + + self.instances.draw(render_pass, layer); + } + render_pass.pop_debug_group(); + } +} + +pub fn create_bind_group_layout( + render_state: &luminol_egui_wgpu::RenderState, +) -> wgpu::BindGroupLayout { + let mut builder = BindGroupLayoutBuilder::new(); + builder + .append( + wgpu::ShaderStages::VERTEX_FRAGMENT, + wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + None, + ) + .append( + wgpu::ShaderStages::FRAGMENT, + wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), + None, + ); + + Viewport::add_to_bind_group_layout(&mut builder); + Transform::add_to_bind_group_layout(&mut builder); + autotiles::add_to_bind_group_layout(&mut builder); + display::add_to_bind_group_layout(&mut builder); + + builder.build(&render_state.device, Some("tilemap bind group layout")) +} diff --git a/crates/graphics/src/tiles/shader.rs b/crates/graphics/src/primitives/tiles/shader.rs similarity index 71% rename from crates/graphics/src/tiles/shader.rs rename to crates/graphics/src/primitives/tiles/shader.rs index 872062dd..a391f187 100644 --- a/crates/graphics/src/tiles/shader.rs +++ b/crates/graphics/src/primitives/tiles/shader.rs @@ -16,29 +16,30 @@ // along with Luminol. If not, see . use super::instance::Instances; -use crate::{vertex::Vertex, BindGroupLayouts}; +use crate::primitives::BindGroupLayouts; pub fn create_render_pipeline( + composer: &mut naga_oil::compose::Composer, render_state: &luminol_egui_wgpu::RenderState, bind_group_layouts: &BindGroupLayouts, -) -> wgpu::RenderPipeline { - let push_constants_supported = crate::push_constants_supported(render_state); +) -> Result { + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/translation.wgsl"), + file_path: "translation.wgsl", + ..Default::default() + })?; - let mut composer = naga_oil::compose::Composer::default().with_capabilities( - push_constants_supported - .then_some(naga::valid::Capabilities::PUSH_CONSTANT) - .unwrap_or_default(), - ); + composer.add_composable_module(naga_oil::compose::ComposableModuleDescriptor { + source: include_str!("../shaders/gamma.wgsl"), + file_path: "gamma.wgsl", + ..Default::default() + })?; - let result = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { - source: include_str!("tilemap.wgsl"), + let module = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { + source: include_str!("../shaders/tilemap.wgsl"), file_path: "tilemap.wgsl", shader_type: naga_oil::compose::ShaderType::Wgsl, shader_defs: std::collections::HashMap::from([ - ( - "USE_PUSH_CONSTANTS".to_string(), - naga_oil::compose::ShaderDefValue::Bool(push_constants_supported), - ), ( "AUTOTILE_ID_AMOUNT".to_string(), naga_oil::compose::ShaderDefValue::UInt(super::atlas::AUTOTILE_ID_AMOUNT), @@ -83,14 +84,7 @@ pub fn create_render_pipeline( ), ]), additional_imports: &[], - }); - let module = match result { - Ok(module) => module, - Err(e) => { - let error = e.emit_to_string(&composer); - panic!("{error}"); - } - }; + })?; let shader_module = render_state .device @@ -99,38 +93,16 @@ pub fn create_render_pipeline( source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)), }); - let push_constant_ranges: &[_] = if push_constants_supported { - &[ - // Viewport + Autotiles - wgpu::PushConstantRange { - stages: wgpu::ShaderStages::VERTEX, - range: 0..(64 + 48), - }, - // Fragment - wgpu::PushConstantRange { - stages: wgpu::ShaderStages::FRAGMENT, - range: (64 + 48)..(64 + 48 + 4), - }, - ] - } else { - &[] - }; - let label = if push_constants_supported { - "Tilemap Render Pipeline Layout (push constants)" - } else { - "Tilemap Render Pipeline Layout (uniforms)" - }; - let pipeline_layout = render_state .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(label), + label: Some("Tilemap Render Pipeline Layout"), bind_group_layouts: &[&bind_group_layouts.tiles], - push_constant_ranges, + push_constant_ranges: &[], }); - render_state + Ok(render_state .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Tilemap Render Pipeline"), @@ -138,7 +110,7 @@ pub fn create_render_pipeline( vertex: wgpu::VertexState { module: &shader_module, entry_point: "vs_main", - buffers: &[Vertex::desc(), Instances::desc()], + buffers: &[Instances::desc()], }, fragment: Some(wgpu::FragmentState { module: &shader_module, @@ -152,5 +124,5 @@ pub fn create_render_pipeline( depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, - }) + })) } diff --git a/crates/graphics/src/sprite/shader.rs b/crates/graphics/src/sprite/shader.rs deleted file mode 100644 index fd705c7f..00000000 --- a/crates/graphics/src/sprite/shader.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -use crate::{vertex::Vertex, BindGroupLayouts}; - -fn create_shader( - render_state: &luminol_egui_wgpu::RenderState, - bind_group_layouts: &BindGroupLayouts, - target: wgpu::BlendState, -) -> wgpu::RenderPipeline { - let push_constants_supported = crate::push_constants_supported(render_state); - - let mut composer = naga_oil::compose::Composer::default().with_capabilities( - push_constants_supported - .then_some(naga::valid::Capabilities::PUSH_CONSTANT) - .unwrap_or_default(), - ); - - let module = composer - .make_naga_module(naga_oil::compose::NagaModuleDescriptor { - source: include_str!("sprite.wgsl"), - file_path: "sprite.wgsl", - shader_type: naga_oil::compose::ShaderType::Wgsl, - shader_defs: std::collections::HashMap::from([( - "USE_PUSH_CONSTANTS".to_string(), - naga_oil::compose::ShaderDefValue::Bool(push_constants_supported), - )]), - additional_imports: &[], - }) - .expect("failed to create sprite shader module"); - - let shader_module = render_state - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Sprite Shader Module"), - source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)), - }); - - let push_constant_ranges: &[_] = if push_constants_supported { - &[ - // Viewport - wgpu::PushConstantRange { - stages: wgpu::ShaderStages::VERTEX, - range: 0..64, - }, - wgpu::PushConstantRange { - stages: wgpu::ShaderStages::FRAGMENT, - range: 64..(64 + 16), - }, - ] - } else { - &[] - }; - let label = if push_constants_supported { - "Sprite Pipeline Layout (push constants)" - } else { - "Sprite Pipeline Layout (uniforms)" - }; - - let pipeline_layout = - render_state - .device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some(label), - bind_group_layouts: &[&bind_group_layouts.sprite], - push_constant_ranges, - }); - - render_state - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Tilemap Sprite Render Pipeline"), - layout: Some(&pipeline_layout), - vertex: wgpu::VertexState { - module: &shader_module, - entry_point: "vs_main", - buffers: &[Vertex::desc()], - }, - fragment: Some(wgpu::FragmentState { - module: &shader_module, - entry_point: "fs_main", - targets: &[Some(wgpu::ColorTargetState { - blend: Some(target), - ..render_state.target_format.into() - })], - }), - primitive: wgpu::PrimitiveState { - // polygon_mode: wgpu::PolygonMode::Line, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }) -} - -pub fn create_sprite_shaders( - render_state: &luminol_egui_wgpu::RenderState, - bind_group_layouts: &BindGroupLayouts, -) -> std::collections::HashMap { - [ - ( - luminol_data::BlendMode::Normal, - wgpu::BlendState::ALPHA_BLENDING, - ), - ( - luminol_data::BlendMode::Add, - wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::One, - operation: wgpu::BlendOperation::Add, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::One, - dst_factor: wgpu::BlendFactor::One, - operation: wgpu::BlendOperation::Add, - }, - }, - ), - ( - luminol_data::BlendMode::Subtract, - wgpu::BlendState { - color: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::SrcAlpha, - dst_factor: wgpu::BlendFactor::One, - operation: wgpu::BlendOperation::ReverseSubtract, - }, - alpha: wgpu::BlendComponent { - src_factor: wgpu::BlendFactor::Zero, - dst_factor: wgpu::BlendFactor::One, - operation: wgpu::BlendOperation::ReverseSubtract, - }, - }, - ), - ] - .into_iter() - .map(|(mode, target)| { - ( - mode, - create_shader(render_state, bind_group_layouts, target), - ) - }) - .collect() -} diff --git a/crates/graphics/src/sprite/sprite.wgsl b/crates/graphics/src/sprite/sprite.wgsl deleted file mode 100644 index d9f66723..00000000 --- a/crates/graphics/src/sprite/sprite.wgsl +++ /dev/null @@ -1,115 +0,0 @@ -// Vertex shader -struct VertexInput { - @location(0) position: vec3, - @location(1) tex_coords: vec2, -} - -struct VertexOutput { - @builtin(position) clip_position: vec4, - @location(0) tex_coords: vec2, -} - -struct Viewport { - proj: mat4x4, -} - -struct Graphic { - hue: f32, - opacity: f32, - opacity_multiplier: f32, - _padding: u32, -} - -@group(0) @binding(0) -var t_diffuse: texture_2d; -@group(0) @binding(1) -var s_diffuse: sampler; - -#if USE_PUSH_CONSTANTS == true -struct PushConstants { - viewport: Viewport, - graphic: Graphic, -} -var push_constants: PushConstants; -#else -@group(0) @binding(2) -var viewport: Viewport; -@group(0) @binding(3) -var graphic: Graphic; -#endif - - -fn rgb_to_hsv(c: vec3) -> vec3 { - let K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); - let p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); - let q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); - - let d = q.x - min(q.w, q.y); - - // Avoid divide - by - zero situations by adding a very tiny delta. - // Since we always deal with underlying 8 - Bit color values, this - // should never mask a real value - let eps = 1.0e-10; - - return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + eps)), d / (q.x + eps), q.x); -} - -fn hsv_to_rgb(c: vec3) -> vec3 { - let K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); - let p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); - - return c.z * mix(K.xxx, clamp(p - K.xxx, vec3(0.0), vec3(1.0)), c.y); -} - -@vertex -fn vs_main( - model: VertexInput, -) -> VertexOutput { - var out: VertexOutput; - out.tex_coords = model.tex_coords; - -#if USE_PUSH_CONSTANTS == true - let viewport = push_constants.viewport; -#endif - - var position = viewport.proj * vec4(model.position.xy, 0.0, 1.0); - - out.clip_position = vec4(position.xy, model.position.z, 1.0); - return out; -} - -// 0-1 sRGB gamma from 0-1 linear -fn gamma_from_linear_rgb(rgb: vec3) -> vec3 { - let cutoff = rgb < vec3(0.0031308); - let lower = rgb * vec3(12.92); - let higher = vec3(1.055) * pow(rgb, vec3(1.0 / 2.4)) - vec3(0.055); - return select(higher, lower, cutoff); -} - -// 0-1 sRGBA gamma from 0-1 linear -fn gamma_from_linear_rgba(linear_rgba: vec4) -> vec4 { - return vec4(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a); -} - -@fragment -fn fs_main(in: VertexOutput) -> @location(0) vec4 { - var tex_sample = textureSample(t_diffuse, s_diffuse, in.tex_coords); - -#if USE_PUSH_CONSTANTS == true - let graphic = push_constants.graphic; -#endif - - tex_sample.a *= graphic.opacity * graphic.opacity_multiplier; - if tex_sample.a <= 0. { - discard; - } - - if graphic.hue > 0.0 { - var hsv = rgb_to_hsv(tex_sample.rgb); - - hsv.x += graphic.hue; - tex_sample = vec4(hsv_to_rgb(hsv), tex_sample.a); - } - - return gamma_from_linear_rgba(tex_sample); -} diff --git a/crates/graphics/src/tilepicker.rs b/crates/graphics/src/tilepicker.rs new file mode 100644 index 00000000..fa0e3ba3 --- /dev/null +++ b/crates/graphics/src/tilepicker.rs @@ -0,0 +1,185 @@ +// Copyright (C) 2024 Lily Lyons +// +// This file is part of Luminol. +// +// Luminol is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Luminol is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Luminol. If not, see . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this Program, or any covered work, by linking or combining +// it with Steamworks API by Valve Corporation, containing parts covered by +// terms of the Steamworks API by Valve Corporation, the licensors of this +// Program grant you additional permission to convey the resulting work. + +use std::sync::Arc; + +use itertools::Itertools; + +use crate::{ + Atlas, Collision, Drawable, GraphicsState, Grid, Renderable, Tiles, Transform, Viewport, +}; + +pub struct Tilepicker { + pub coll_enabled: bool, + pub grid_enabled: bool, + + pub tiles: Tiles, + pub collision: Collision, + pub grid: Grid, + pub atlas: Atlas, + + pub viewport: Viewport, + ani_time: Option, +} + +impl Tilepicker { + pub fn new( + graphics_state: &GraphicsState, + tileset: &luminol_data::rpg::Tileset, + filesystem: &impl luminol_filesystem::FileSystem, + ) -> color_eyre::Result { + let atlas = graphics_state + .atlas_loader + .load_atlas(graphics_state, filesystem, tileset)?; + + let tilepicker_data = (47..(384 + 47)) + .step_by(48) + .chain(384..(atlas.tileset_height as i16 / 32 * 8 + 384)) + .collect_vec(); + let tilepicker_data = luminol_data::Table3::new_data( + 8, + 1 + (atlas.tileset_height / 32) as usize, + 1, + tilepicker_data, + ); + + let viewport = Viewport::new( + graphics_state, + glam::vec2(256., atlas.tileset_height as f32 + 32.), + ); + + let tiles = Tiles::new( + graphics_state, + &tilepicker_data, + &atlas, + &viewport, + Transform::unit(graphics_state), + ); + + let grid = Grid::new( + graphics_state, + &viewport, + Transform::unit(graphics_state), + tilepicker_data.xsize() as u32, + tilepicker_data.ysize() as u32, + ); + + let mut passages = + luminol_data::Table2::new(tilepicker_data.xsize(), tilepicker_data.ysize()); + for x in 0..8 { + passages[(x, 0)] = { + let tile_id = tilepicker_data[(x, 0, 0)].try_into().unwrap_or_default(); + if tile_id >= tileset.passages.len() { + 0 + } else { + tileset.passages[tile_id] + } + }; + } + let length = + (passages.len().saturating_sub(8)).min(tileset.passages.len().saturating_sub(384)); + passages.as_mut_slice()[8..8 + length] + .copy_from_slice(&tileset.passages.as_slice()[384..384 + length]); + let collision = Collision::new( + graphics_state, + &viewport, + Transform::unit(graphics_state), + &passages, + ); + + Ok(Self { + tiles, + collision, + grid, + atlas, + + viewport, + + coll_enabled: false, + grid_enabled: false, + ani_time: None, + }) + } + + pub fn update_animation(&mut self, render_state: &luminol_egui_wgpu::RenderState, time: f64) { + if let Some(ani_time) = self.ani_time { + if time - ani_time >= 16. / 60. { + self.ani_time = Some(time); + self.tiles.autotiles.inc_ani_index(render_state); + } + } else { + self.ani_time = Some(time); + } + } + + pub fn set_position( + &mut self, + render_state: &luminol_egui_wgpu::RenderState, + position: glam::Vec2, + ) { + self.tiles.transform.set_position(render_state, position); + self.collision + .transform + .set_position(render_state, position); + self.grid.transform.set_position(render_state, position); + } +} + +pub struct Prepared { + tiles: ::Prepared, + collision: ::Prepared, + grid: ::Prepared, + + coll_enabled: bool, + grid_enabled: bool, +} + +impl Renderable for Tilepicker { + type Prepared = Prepared; + + fn prepare(&mut self, graphics_state: &Arc) -> Self::Prepared { + Prepared { + tiles: self.tiles.prepare(graphics_state), + collision: self.collision.prepare(graphics_state), + grid: self.grid.prepare(graphics_state), + + coll_enabled: self.coll_enabled, + grid_enabled: self.grid_enabled, + } + } +} + +impl Drawable for Prepared { + fn draw<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + self.tiles.draw(render_pass); + + if self.coll_enabled { + self.collision.draw(render_pass); + } + + if self.grid_enabled { + self.grid.draw(render_pass); + } + } +} diff --git a/crates/graphics/src/tiles/autotile_ids.rs b/crates/graphics/src/tiles/autotile_ids.rs deleted file mode 100644 index 000e6f54..00000000 --- a/crates/graphics/src/tiles/autotile_ids.rs +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -/* -#[derive(Clone, Copy, Debug, Default)] -pub struct Autotile { - pub x: f32, - pub y: f32, -} - -pub const AUTOTILES: [[Autotile; 4]; 48] = [ - [ - Autotile { x: 32.5, y: 64.5 }, - Autotile { x: 48.5, y: 64.5 }, - Autotile { x: 32.5, y: 80.5 }, - Autotile { x: 48.5, y: 80.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 48.5, y: 64.5 }, - Autotile { x: 32.5, y: 80.5 }, - Autotile { x: 48.5, y: 80.5 }, - ], - [ - Autotile { x: 32.5, y: 64.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 32.5, y: 80.5 }, - Autotile { x: 48.5, y: 80.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 32.5, y: 80.5 }, - Autotile { x: 48.5, y: 80.5 }, - ], - [ - Autotile { x: 32.5, y: 64.5 }, - Autotile { x: 48.5, y: 64.5 }, - Autotile { x: 32.5, y: 80.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 48.5, y: 64.5 }, - Autotile { x: 32.5, y: 80.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 32.5, y: 64.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 32.5, y: 80.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 32.5, y: 80.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 32.5, y: 64.5 }, - Autotile { x: 48.5, y: 64.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 48.5, y: 80.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 48.5, y: 64.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 48.5, y: 80.5 }, - ], - [ - Autotile { x: 32.5, y: 64.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 48.5, y: 80.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 48.5, y: 80.5 }, - ], - [ - Autotile { x: 32.5, y: 64.5 }, - Autotile { x: 48.5, y: 64.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 48.5, y: 64.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 32.5, y: 64.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 0.5, y: 64.5 }, - Autotile { x: 16.5, y: 64.5 }, - Autotile { x: 0.5, y: 80.5 }, - Autotile { x: 16.5, y: 80.5 }, - ], - [ - Autotile { x: 0.5, y: 64.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 0.5, y: 80.5 }, - Autotile { x: 16.5, y: 80.5 }, - ], - [ - Autotile { x: 0.5, y: 64.5 }, - Autotile { x: 16.5, y: 64.5 }, - Autotile { x: 0.5, y: 80.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 0.5, y: 64.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 0.5, y: 80.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 32.5, y: 32.5 }, - Autotile { x: 48.5, y: 32.5 }, - Autotile { x: 32.5, y: 48.5 }, - Autotile { x: 48.5, y: 48.5 }, - ], - [ - Autotile { x: 32.5, y: 32.5 }, - Autotile { x: 48.5, y: 32.5 }, - Autotile { x: 32.5, y: 48.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 32.5, y: 32.5 }, - Autotile { x: 48.5, y: 32.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 48.5, y: 48.5 }, - ], - [ - Autotile { x: 32.5, y: 32.5 }, - Autotile { x: 48.5, y: 32.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 64.5, y: 64.5 }, - Autotile { x: 80.5, y: 64.5 }, - Autotile { x: 64.5, y: 80.5 }, - Autotile { x: 80.5, y: 80.5 }, - ], - [ - Autotile { x: 64.5, y: 64.5 }, - Autotile { x: 80.5, y: 64.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 80.5, y: 80.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 80.5, y: 64.5 }, - Autotile { x: 64.5, y: 80.5 }, - Autotile { x: 80.5, y: 80.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 80.5, y: 64.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 80.5, y: 80.5 }, - ], - [ - Autotile { x: 32.5, y: 96.5 }, - Autotile { x: 48.5, y: 96.5 }, - Autotile { x: 32.5, y: 112.5 }, - Autotile { x: 48.5, y: 112.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 48.5, y: 96.5 }, - Autotile { x: 32.5, y: 112.5 }, - Autotile { x: 48.5, y: 112.5 }, - ], - [ - Autotile { x: 32.5, y: 96.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 32.5, y: 112.5 }, - Autotile { x: 48.5, y: 112.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 32.5, y: 112.5 }, - Autotile { x: 48.5, y: 112.5 }, - ], - [ - Autotile { x: 0.5, y: 64.5 }, - Autotile { x: 80.5, y: 64.5 }, - Autotile { x: 0.5, y: 80.5 }, - Autotile { x: 80.5, y: 80.5 }, - ], - [ - Autotile { x: 32.5, y: 32.5 }, - Autotile { x: 48.5, y: 32.5 }, - Autotile { x: 32.5, y: 112.5 }, - Autotile { x: 48.5, y: 112.5 }, - ], - [ - Autotile { x: 0.5, y: 32.5 }, - Autotile { x: 16.5, y: 32.5 }, - Autotile { x: 0.5, y: 48.5 }, - Autotile { x: 16.5, y: 48.5 }, - ], - [ - Autotile { x: 0.5, y: 32.5 }, - Autotile { x: 16.5, y: 32.5 }, - Autotile { x: 0.5, y: 48.5 }, - Autotile { x: 80.5, y: 16.5 }, - ], - [ - Autotile { x: 64.5, y: 32.5 }, - Autotile { x: 80.5, y: 32.5 }, - Autotile { x: 64.5, y: 48.5 }, - Autotile { x: 80.5, y: 48.5 }, - ], - [ - Autotile { x: 64.5, y: 32.5 }, - Autotile { x: 80.5, y: 32.5 }, - Autotile { x: 64.5, y: 16.5 }, - Autotile { x: 80.5, y: 48.5 }, - ], - [ - Autotile { x: 64.5, y: 96.5 }, - Autotile { x: 80.5, y: 96.5 }, - Autotile { x: 64.5, y: 112.5 }, - Autotile { x: 80.5, y: 112.5 }, - ], - [ - Autotile { x: 64.5, y: 0.5 }, - Autotile { x: 80.5, y: 96.5 }, - Autotile { x: 64.5, y: 112.5 }, - Autotile { x: 80.5, y: 112.5 }, - ], - [ - Autotile { x: 0.5, y: 96.5 }, - Autotile { x: 16.5, y: 96.5 }, - Autotile { x: 0.5, y: 112.5 }, - Autotile { x: 16.5, y: 112.5 }, - ], - [ - Autotile { x: 0.5, y: 96.5 }, - Autotile { x: 80.5, y: 0.5 }, - Autotile { x: 0.5, y: 112.5 }, - Autotile { x: 16.5, y: 112.5 }, - ], - [ - Autotile { x: 0.5, y: 32.5 }, - Autotile { x: 80.5, y: 32.5 }, - Autotile { x: 0.5, y: 48.5 }, - Autotile { x: 80.5, y: 48.5 }, - ], - [ - Autotile { x: 0.5, y: 32.5 }, - Autotile { x: 16.5, y: 32.5 }, - Autotile { x: 0.5, y: 112.5 }, - Autotile { x: 16.5, y: 112.5 }, - ], - [ - Autotile { x: 0.5, y: 96.5 }, - Autotile { x: 80.5, y: 96.5 }, - Autotile { x: 0.5, y: 112.5 }, - Autotile { x: 80.5, y: 112.5 }, - ], - [ - Autotile { x: 64.5, y: 32.5 }, - Autotile { x: 80.5, y: 32.5 }, - Autotile { x: 64.5, y: 112.5 }, - Autotile { x: 80.5, y: 112.5 }, - ], - [ - Autotile { x: 0.5, y: 32.5 }, - Autotile { x: 80.5, y: 32.5 }, - Autotile { x: 0.5, y: 112.5 }, - Autotile { x: 80.5, y: 112.5 }, - ], - [ - Autotile { x: 0.5, y: 0.5 }, - Autotile { x: 16.5, y: 0.5 }, - Autotile { x: 0.5, y: 16.5 }, - Autotile { x: 16.5, y: 16.5 }, - ], -]; -*/ - -/// Hardcoded list of tiles from r48 and old python Luminol. -/// There seems to be very little pattern in autotile IDs so this is sadly -/// the best we can do. -pub const AUTOTILES: [[u32; 4]; 48] = [ - [26, 27, 32, 33], - [4, 27, 32, 33], - [26, 5, 32, 33], - [4, 5, 32, 33], - [26, 27, 32, 11], - [4, 27, 32, 11], - [26, 5, 32, 11], - [4, 5, 32, 11], - [26, 27, 10, 33], - [4, 27, 10, 33], - [26, 5, 10, 33], - [4, 5, 10, 33], - [26, 27, 10, 11], - [4, 27, 10, 11], - [26, 5, 10, 11], - [4, 5, 10, 11], - [24, 25, 30, 31], - [24, 5, 30, 31], - [24, 25, 30, 11], - [24, 5, 30, 11], - [14, 15, 20, 21], - [14, 15, 20, 11], - [14, 15, 10, 21], - [14, 15, 10, 11], - [28, 29, 34, 35], - [28, 29, 10, 35], - [4, 29, 34, 35], - [4, 29, 10, 35], - [38, 39, 44, 45], - [4, 39, 44, 45], - [38, 5, 44, 45], - [4, 5, 44, 45], - [24, 29, 30, 35], - [14, 15, 44, 45], - [12, 13, 18, 19], - [12, 13, 18, 11], - [16, 17, 22, 23], - [16, 17, 10, 23], - [40, 41, 46, 47], - [4, 41, 46, 47], - [36, 37, 42, 43], - [36, 5, 42, 43], - [12, 17, 18, 23], - [12, 13, 42, 43], - [36, 41, 42, 47], - [16, 17, 46, 47], - [12, 17, 42, 47], - [0, 1, 6, 7], -]; diff --git a/crates/graphics/src/tiles/mod.rs b/crates/graphics/src/tiles/mod.rs deleted file mode 100644 index 22f89d75..00000000 --- a/crates/graphics/src/tiles/mod.rs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -use std::sync::Arc; - -use crate::{ - viewport::{self, Viewport}, - BindGroupBuilder, BindGroupLayoutBuilder, GraphicsState, -}; - -pub use atlas::Atlas; - -use autotiles::Autotiles; -use instance::Instances; -use opacity::Opacity; - -mod atlas; -mod autotile_ids; -pub(crate) mod autotiles; -mod instance; -pub(crate) mod opacity; -pub(crate) mod shader; - -pub struct Tiles { - pub autotiles: Autotiles, - pub atlas: Atlas, - pub instances: Instances, - pub opacity: Opacity, - pub viewport: Arc, - - pub bind_group: wgpu::BindGroup, -} - -impl Tiles { - pub fn new( - graphics_state: &GraphicsState, - viewport: Arc, - atlas: Atlas, - tiles: &luminol_data::Table3, - ) -> Self { - let autotiles = Autotiles::new(graphics_state, &atlas); - let instances = Instances::new( - &graphics_state.render_state, - tiles, - atlas.atlas_texture.size(), - ); - let opacity = Opacity::new(graphics_state); - - let mut bind_group_builder = BindGroupBuilder::new(); - bind_group_builder - .append_texture_view(&atlas.atlas_texture.view) - .append_sampler(&graphics_state.nearest_sampler); - if !graphics_state.push_constants_supported() { - bind_group_builder - .append_buffer(viewport.as_buffer().unwrap()) - .append_buffer(autotiles.as_buffer().unwrap()) - .append_buffer(opacity.as_buffer().unwrap()); - } - let bind_group = bind_group_builder.build( - &graphics_state.render_state.device, - Some("tilemap bind group"), - &graphics_state.bind_group_layouts.tiles, - ); - - Self { - autotiles, - atlas, - instances, - opacity, - - bind_group, - viewport, - } - } - - pub fn set_tile( - &self, - render_state: &luminol_egui_wgpu::RenderState, - tile_id: i16, - position: (usize, usize, usize), - ) { - self.instances.set_tile(render_state, tile_id, position) - } - - pub fn draw<'rpass>( - &'rpass self, - graphics_state: &'rpass GraphicsState, - enabled_layers: &[bool], - selected_layer: Option, - render_pass: &mut wgpu::RenderPass<'rpass>, - ) { - #[repr(C)] - #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] - struct VertexPushConstant { - viewport: [u8; 64], - autotiles: [u8; 48], - } - - render_pass.push_debug_group("tilemap tiles renderer"); - render_pass.set_pipeline(&graphics_state.pipelines.tiles); - render_pass.set_bind_group(0, &self.bind_group, &[]); - - if graphics_state.push_constants_supported() { - render_pass.set_push_constants( - wgpu::ShaderStages::VERTEX, - 0, - bytemuck::bytes_of(&VertexPushConstant { - viewport: self.viewport.as_bytes(), - autotiles: self.autotiles.as_bytes(), - }), - ); - } - - for (layer, enabled) in enabled_layers.iter().copied().enumerate() { - let opacity = if selected_layer.is_some_and(|s| s != layer) { - 0.5 - } else { - 1.0 - }; - if enabled { - self.opacity - .set_opacity(&graphics_state.render_state, layer, opacity); - if graphics_state.push_constants_supported() { - render_pass.set_push_constants( - wgpu::ShaderStages::FRAGMENT, - 64 + 48, - bytemuck::bytes_of::(&opacity), - ); - } - - self.instances.draw(render_pass, layer); - } - } - render_pass.pop_debug_group(); - } -} - -pub fn create_bind_group_layout( - render_state: &luminol_egui_wgpu::RenderState, -) -> wgpu::BindGroupLayout { - let mut builder = BindGroupLayoutBuilder::new(); - builder - .append( - wgpu::ShaderStages::VERTEX_FRAGMENT, - wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: false }, - view_dimension: wgpu::TextureViewDimension::D2, - multisampled: false, - }, - None, - ) - .append( - wgpu::ShaderStages::FRAGMENT, - wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), - None, - ); - - if !crate::push_constants_supported(render_state) { - viewport::add_to_bind_group_layout(&mut builder); - autotiles::add_to_bind_group_layout(&mut builder); - opacity::add_to_bind_group_layout(&mut builder); - } - - builder.build(&render_state.device, Some("tilemap bind group layout")) -} diff --git a/crates/graphics/src/tiles/opacity.rs b/crates/graphics/src/tiles/opacity.rs deleted file mode 100644 index 65721a11..00000000 --- a/crates/graphics/src/tiles/opacity.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -use crossbeam::atomic::AtomicCell; -use wgpu::util::DeviceExt; - -use crate::{BindGroupLayoutBuilder, GraphicsState}; - -#[derive(Debug)] -pub struct Opacity { - data: AtomicCell<[f32; 4]>, // length has to be a multiple of 4 - uniform: Option, -} - -impl Opacity { - pub fn new(graphics_state: &GraphicsState) -> Self { - let opacity = [1.; 4]; - - let uniform = (!graphics_state.push_constants_supported()).then(|| { - graphics_state.render_state.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("tilemap opacity buffer"), - contents: bytemuck::cast_slice(&[opacity]), - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, - }, - ) - }); - - Self { - data: AtomicCell::new(opacity), - uniform, - } - } - - pub fn opacity(&self, layer: usize) -> f32 { - self.data.load()[layer] - } - - pub fn as_buffer(&self) -> Option<&wgpu::Buffer> { - self.uniform.as_ref() - } - - pub fn set_opacity( - &self, - render_state: &luminol_egui_wgpu::RenderState, - layer: usize, - opacity: f32, - ) { - let mut data = self.data.load(); - if data[layer] != opacity { - data[layer] = opacity; - self.data.store(data); - self.regen_buffer(render_state); - } - } - - fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) { - if let Some(uniform) = &self.uniform { - render_state - .queue - .write_buffer(uniform, 0, bytemuck::cast_slice(&[self.data.load()])); - } - } -} - -pub fn add_to_bind_group_layout( - layout_builder: &mut BindGroupLayoutBuilder, -) -> &mut BindGroupLayoutBuilder { - layout_builder.append( - wgpu::ShaderStages::VERTEX_FRAGMENT, - wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - None, - ) -} diff --git a/crates/graphics/src/viewport.rs b/crates/graphics/src/viewport.rs deleted file mode 100644 index 93744412..00000000 --- a/crates/graphics/src/viewport.rs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (C) 2023 Lily Lyons -// -// This file is part of Luminol. -// -// Luminol is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Luminol is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Luminol. If not, see . - -use crossbeam::atomic::AtomicCell; -use wgpu::util::DeviceExt; - -use crate::{BindGroupLayoutBuilder, GraphicsState}; - -#[derive(Debug)] -pub struct Viewport { - data: AtomicCell, - uniform: Option, -} - -impl Viewport { - pub fn new(graphics_state: &GraphicsState, width: f32, height: f32) -> Self { - Self::new_proj( - graphics_state, - glam::Mat4::orthographic_rh(0.0, width, height, 0.0, -1.0, 1.0), - ) - } - - pub fn new_proj(graphics_state: &GraphicsState, proj: glam::Mat4) -> Self { - let uniform = (!graphics_state.push_constants_supported()).then(|| { - graphics_state.render_state.device.create_buffer_init( - &wgpu::util::BufferInitDescriptor { - label: Some("tilemap viewport buffer"), - contents: bytemuck::cast_slice(&[proj]), - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, - }, - ) - }); - - Self { - data: AtomicCell::new(proj), - uniform, - } - } - - pub fn set_width_height( - &self, - render_state: &luminol_egui_wgpu::RenderState, - width: f32, - height: f32, - ) { - self.set_proj( - render_state, - glam::Mat4::orthographic_rh(0.0, width, height, 0.0, -1.0, 1.0), - ) - } - - pub fn set_proj(&self, render_state: &luminol_egui_wgpu::RenderState, proj: glam::Mat4) { - let data = self.data.load(); - if data != proj { - self.data.store(proj); - self.regen_buffer(render_state); - } - } - - pub fn as_bytes(&self) -> [u8; std::mem::size_of::()] { - bytemuck::cast(self.data.load()) - } - - pub fn as_buffer(&self) -> Option<&wgpu::Buffer> { - self.uniform.as_ref() - } - - fn regen_buffer(&self, render_state: &luminol_egui_wgpu::RenderState) { - if let Some(uniform) = &self.uniform { - render_state - .queue - .write_buffer(uniform, 0, bytemuck::cast_slice(&[self.data.load()])); - } - } -} - -pub fn add_to_bind_group_layout( - layout_builder: &mut BindGroupLayoutBuilder, -) -> &mut BindGroupLayoutBuilder { - layout_builder.append( - wgpu::ShaderStages::VERTEX_FRAGMENT, - wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - None, - ) -} diff --git a/crates/ui/src/tabs/map/mod.rs b/crates/ui/src/tabs/map/mod.rs index 3b2cb79d..a967058d 100644 --- a/crates/ui/src/tabs/map/mod.rs +++ b/crates/ui/src/tabs/map/mod.rs @@ -111,7 +111,7 @@ enum HistoryEntry { /// Contains a deleted event and its corresponding graphic. EventDeleted { event: luminol_data::rpg::Event, - sprites: Option<(luminol_graphics::Event, luminol_graphics::Event)>, + sprite: Option, }, } @@ -133,7 +133,7 @@ impl Tab { let tileset = &tilesets.data[map.tileset_id]; let mut passages = luminol_data::Table2::new(map.data.xsize(), map.data.ysize()); - luminol_graphics::collision::calculate_passages( + luminol_graphics::Collision::calculate_passages( &tileset.passages, &tileset.priorities, &map.data, @@ -250,7 +250,7 @@ impl luminol_core::Tab for Tab { ui.end_row(); for (index, layer) in - self.view.map.enabled_layers.iter_mut().enumerate() + self.view.map.tiles.enabled_layers.iter_mut().enumerate() { ui.columns(1, |columns| { columns[0].selectable_value( @@ -271,7 +271,7 @@ impl luminol_core::Tab for Tab { egui::RichText::new("Events").italics(), ); }); - ui.checkbox(&mut self.view.event_enabled, "👁"); + ui.checkbox(&mut self.view.map.event_enabled, "👁"); ui.end_row(); ui.label(egui::RichText::new("Fog").underline()); @@ -346,8 +346,8 @@ impl luminol_core::Tab for Tab { .persistence_id, ) .show_viewport(ui, |ui, rect| { - self.tilepicker.coll_enabled = self.view.map.coll_enabled; - self.tilepicker.grid_enabled = self.view.map.grid_enabled; + self.tilepicker.view.coll_enabled = self.view.map.coll_enabled; + self.tilepicker.view.grid_enabled = self.view.map.grid_enabled; self.tilepicker.ui(update_state, ui, rect); ui.separator(); }); @@ -370,7 +370,7 @@ impl luminol_core::Tab for Tab { let response = self.view.ui( ui, - &update_state.graphics, + update_state, &map, &self.tilepicker, self.event_drag_info.is_some(), @@ -485,11 +485,11 @@ impl luminol_core::Tab for Tab { // Press delete or backspace to delete the selected event if is_delete_pressed { let event = map.events.remove(selected_event_id); - let sprites = self.view.events.try_remove(selected_event_id).ok(); + let sprite = self.view.map.events.try_remove(selected_event_id).ok(); self.push_to_history( update_state, &mut map, - HistoryEntry::EventDeleted { event, sprites }, + HistoryEntry::EventDeleted { event, sprite }, ); } @@ -609,15 +609,15 @@ impl luminol_core::Tab for Tab { Some(HistoryEntry::EventCreated(id)) => { let event = map.events.remove(id); - let sprites = self.view.events.try_remove(id).ok(); - Some(HistoryEntry::EventDeleted { event, sprites }) + let sprite = self.view.map.events.try_remove(id).ok(); + Some(HistoryEntry::EventDeleted { event, sprite }) } - Some(HistoryEntry::EventDeleted { event, sprites }) => { + Some(HistoryEntry::EventDeleted { event, sprite }) => { let id = event.id; map.events.insert(id, event); - if let Some(sprites) = sprites { - self.view.events.insert(id, sprites); + if let Some(sprite) = sprite { + self.view.map.events.insert(id, sprite); } Some(HistoryEntry::EventCreated(id)) } @@ -658,17 +658,17 @@ impl luminol_core::Tab for Tab { } // Update the collision preview - luminol_graphics::collision::calculate_passages( + luminol_graphics::Collision::calculate_passages( &tileset.passages, &tileset.priorities, &map.data, - if self.view.event_enabled { + if self.view.map.event_enabled { Some(&map.events) } else { None }, (0..map.data.zsize()) - .filter(|&i| self.view.map.enabled_layers[i]) + .filter(|&i| self.view.map.tiles.enabled_layers[i]) .rev(), |x, y, passage| { if self.passages[(x, y)] != passage { diff --git a/src/main.rs b/src/main.rs index 192531bc..6ed6708e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -214,7 +214,6 @@ fn main() { let native_options = luminol_eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_drag_and_drop(true) - .with_transparent(true) .with_icon(egui::IconData { width: image.width(), height: image.height(), @@ -223,16 +222,9 @@ fn main() { .with_app_id("astrabit.luminol"), wgpu_options: luminol_egui_wgpu::WgpuConfiguration { supported_backends: wgpu::util::backend_bits_from_env() - .unwrap_or(wgpu::Backends::PRIMARY), - device_descriptor: std::sync::Arc::new(|_| wgpu::DeviceDescriptor { - label: Some("luminol device descriptor"), - required_features: wgpu::Features::PUSH_CONSTANTS, - required_limits: wgpu::Limits { - max_push_constant_size: 128, - ..wgpu::Limits::default() - }, - }), - power_preference: wgpu::util::power_preference_from_env().unwrap_or_default(), + .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::SECONDARY), + power_preference: wgpu::util::power_preference_from_env() + .unwrap_or(wgpu::PowerPreference::LowPower), ..Default::default() }, persist_window: true,